Converting Async Response to Standard Javascript Object - javascript

Imagine your React app gets a response like this:
email_address
first_name
last_name
What's a best practice way to convert these fields to something more common in Javascript:
emailAddress
firstName
lastName
Also keeping mind that there could be nested structures.
I've typically done this immediately when the response is received.
My colleagues seem to think it's fine to keep the snake_case syntax persist through the app.

There may be some edge cases that fail, I could not find anything on github that would do the trick but if you have any errors then please let me know.
It is assuming you only pass object literals to it, maybe you can write some tests and tell me if anything fails:
const snakeToCamel = snakeCased => {
// Use a regular expression to find the underscores + the next letter
return snakeCased.replace(/(_\w)/g, function(match) {
// Convert to upper case and ignore the first char (=the underscore)
return match.toUpperCase().substr(1);
});
};
const toCamel = object => {
if (Array.isArray(object)) {
return object.map(toCamel);
}
if (typeof object === 'object' && object !== null) {
return Object.entries(object).reduce(
(result, [key, value]) => {
result[snakeToCamel(key)] = toCamel(value);
return result;
},
{}
);
}
return object;
};
console.log(
toCamel({
arra_of_things: [
{ thing_one: null },
{ two: { sub_item: 22 } },
],
sub_one: {
sub_two: {
sub_three: {
sub_four: {
sub_four_value: 22,
},
},
},
},
})
);

Related

Construct MongoDB query from GraphQL request

Let's say we query the server with this request, we only want to get the following user's Email, My current implementation requests the whole User object from the MongoDB, which I can imagine is extremely inefficient.
GQL
{
user(id:"34567345637456") {
email
}
}
How would you go about creating a MongoDB filter that would only return those Specified Fields? E.g,
JS object
{
"email": 1
}
My current server is running Node.js, Fastify and Mercurius
which I can imagine is extremely inefficient.
Doing this task is an advanced feature with many pitfalls. I would suggest starting building a simple extraction that read all the fields. This solution works and does not return any additional field to the client.
The pitfalls are:
nested queries
complex object composition
aliasing
multiple queries into one request
Here an example that does what you are looking for.
It manages aliasing and multiple queries.
const Fastify = require('fastify')
const mercurius = require('mercurius')
const app = Fastify({ logger: true })
const schema = `
type Query {
select: Foo
}
type Foo {
a: String
b: String
}
`
const resolvers = {
Query: {
select: async (parent, args, context, info) => {
const currentQueryName = info.path.key
// search the input query AST node
const selection = info.operation.selectionSet.selections.find(
(selection) => {
return (
selection.name.value === currentQueryName ||
selection.alias.value === currentQueryName
)
}
)
// grab the fields requested by the user
const project = selection.selectionSet.selections.map((selection) => {
return selection.name.value
})
// do the query using the projection
const result = {}
project.forEach((fieldName) => {
result[fieldName] = fieldName
})
return result
},
},
}
app.register(mercurius, {
schema,
resolvers,
graphiql: true,
})
app.listen(3000)
Call it using:
query {
one: select {
a
}
two: select {
a
aliasMe:b
}
}
Returns
{
"data": {
"one": {
"a": "a"
},
"two": {
"a": "a",
"aliasMe": "b"
}
}
}
Expanding from #Manuel Spigolon original answer, where he stated that one of the pitfalls of his implementation is that it doesn't work on nested queries and 'multiple queries into one request' which this implementation seeks to fix.
function formFilter(context:any) {
let filter:any = {};
let getValues = (selection:any, parentObj?:string[]) => {
//selection = labelSelection(selection);
selection.map((selection:any) => {
// Check if the parentObj is defined
if(parentObj)
// Merge the two objects
_.merge(filter, [...parentObj, null].reduceRight((obj, next) => {
if(next === null) return ({[selection.name?.value]: 1});
return ({[next]: obj});
}, {}));
// Check for a nested selection set
if(selection.selectionSet?.selections !== undefined){
// If the selection has a selection set, then we need to recurse
if(!parentObj) getValues(selection.selectionSet?.selections, [selection.name.value]);
// If the selection is nested
else getValues(selection.selectionSet?.selections, [...parentObj, selection.name.value]);
}
});
}
// Start the recursive function
getValues(context.operation.selectionSet.selections);
return filter;
}
Input
{
role(id: "61f1ccc79623d445bd2f677f") {
name
users {
user_name
_id
permissions {
roles
}
}
permissions
}
}
Output (JSON.stringify)
{
"role":{
"name":1,
"users":{
"user_name":1,
"_id":1,
"permissions":{
"roles":1
}
},
"permissions":1
}
}

Issues with geting results from filter inside filters

Guys i'm trying to filter some info from this array, a piece of it:
{
"entry": [
{
"ent_seq": 1000090,
"k_ele": [
{
"keb": "○"
},
{
"keb": "〇"
}
],
"r_ele": {
"reb": "まる"
},
"sense": [
{
"pos": "&n;",
"xref": "〇〇・まるまる・1",
"gloss": "symbol used as a placeholder (either because a number of other words could be used in that position, or because of censorship)"
},
{
"pos": "&n;",
"xref": "句点",
"gloss": [
"period",
"full stop"
]
},
{
"pos": "&n;",
"xref": "半濁点",
"gloss": [
"maru mark",
"semivoiced sound",
"p-sound"
]
}
]
},
Here we have the 'sense' item, where he can be an array or not, and inside of it the 'gloss' item, an array or not as well.
To do the main search, im doing this:
export const mainSearch = async (req, res) => {
var filterData2 = teste.entry.filter(x => {
if ('sense' in x && Array.isArray(x['sense'])) {
let result = x.sense.filter(sense_item => {
if (Array.isArray(x.sense['gloss'])) {
let result2 = sense_item.gloss.includes(req.params.id)
} else if (x.sense.gloss === req.params.id) return x
})
}
if(result) return x
}
)
if (filterData2) {
console.log(filterData2)
// res.json(filterData2)
}
Where i receive the search item from req.params.id, and i already tried dozens of things, im really stucked here, for the result right now i'm getting an empty array
The aim is if i get a true response for the search, to let the 'first filter' perceive it and go checking the rest.
For the 'k_ele' and 'r_ele' my code works fine too, a piece of it:
if ('k_ele' in x && Array.isArray(x['k_ele'])) {
let result = x.k_ele.some(el =>
el.keb.includes(req.params.id)
)
if (result) return x
} else
if ('k_ele' in x && x.k_ele.keb === req.params.id) return x
I'd suggest that you change your strategy. If the data is hard to filter and loop through, then it might be a good idea to change the data to something that is easier to work with.
In the end you'd want to check if the req.params.id is found in any of the gloss arrays or strings. Therefor it might be a good idea to collect all items in the gloss items into a single array and check if the queried value is found in any of the strings.
const data = [
"symbol used as a placeholder (either because a number of other words could be used in that position, or because of censorship)",
"period",
"full stop",
"maru mark",
"semivoiced sound",
"p-sound"
]
With the data like the example above, you'd only have to evaluate if the string you're looking for is present in the array.
const isFound = data.includes(value)
const teste={"entry":[{"ent_seq":1000090,"k_ele":[{"keb":"○"},{"keb":"〇"}],"r_ele":{"reb":"まる"},"sense":[{"pos":"&n;","xref":"〇〇・まるまる・1","gloss":"symbol used as a placeholder (either because a number of other words could be used in that position, or because of censorship)"},{"pos":"&n;","xref":"句点","gloss":["period","full stop"]},{"pos":"&n;","xref":"半濁点","gloss":["maru mark","semivoiced sound","p-sound"]}]}]};
// Example search request.
var req = {
params: {
id: 'full stop'
}
}
/**
* Check if the sense property is present and is an array.
* Then return an array of all sense objects.
*/
const senseEntries = teste.entry
.filter(entry => 'sense' in entry && Array.isArray(entry.sense))
.flatMap(({
sense
}) => sense)
/**
* Loop over the filtered sense objects and return
* either the gloss array or the gloss string inside of an array.
*/
const glossEntries = senseEntries
.flatMap(({
gloss
}) => Array.isArray(gloss) ? gloss : [gloss])
console.log(glossEntries);
/**
* Now all gloss items are collected in a single array and we can check if the id is found in any of the gloss strings.
*/
const isFound = glossEntries.includes(req.params.id)
console.log(`${req.params.id} in gloss values?`, isFound);
The person that posted an answer earlier gave me some clues, but he deleted it.
About the answer, the initial state of the logic was already too messy, and it was unable to return the true or false that the first 'filter' needed, that was the main problem.
So, i just started over focusing on the 'return' part and was there that things got better, anymore than that is just improvements to the code.
if ('sense' in x && !Array.isArray(x['sense'])) {
if (Array.isArray(x.sense['gloss'])) {
return x.sense.gloss.some(el => typeof(el) == 'string' && el.includes(req.params.id))
} else return typeof(x.sense.gloss) == 'string' && x.sense.gloss.includes(req.params.id)
} else if ('sense' in x && Array.isArray(x['sense'])) {
return x.sense.some((sense_item) => {
if (Array.isArray(sense_item['gloss'])) {
return sense_item.gloss.some(el => typeof(el) == 'string' && el.includes(req.params.id))
} else return typeof(sense_item.gloss) == 'string' && sense_item.gloss.includes(req.params.id)
})
}

How to sort an array of complex objects?

I have this method for sorting an array of objects, but when I have a complex structure, for example:
const data = [ { title: 'Book title', author: 'John', info: { language: english, pages: 500, price: '$50' }} ]
I can't sort the second level of the object 'info: {language: english, pages: 500, price:' $ 50 '}'
My Code:
import { useMemo, useState } from 'react';
interface SortConfigProps {
key: string;
direction: string;
}
export const useSortableData = <T>(items: T[]) => {
const [sortConfig, setSortConfig] = useState<SortConfigProps>(
{} as SortConfigProps,
);
const sortedItems = useMemo(() => {
const sortableItems = [...items];
if (sortConfig) {
sortableItems.sort((a: any, b: any) => {
if (a[sortConfig.key] < b[sortConfig.key]) {
return sortConfig.direction === 'ascending' ? -1 : 1;
}
if (a[sortConfig.key] > b[sortConfig.key]) {
return sortConfig.direction === 'ascending' ? 1 : -1;
}
return 0;
});
}
return sortableItems;
}, [items, sortConfig]);
const requestSort = (key: string) => {
let direction = 'ascending';
if (
sortConfig &&
sortConfig.key === key &&
sortConfig.direction === 'ascending'
) {
direction = 'descending';
}
setSortConfig({ key, direction });
};
return { items: sortedItems, requestSort, sortConfig };
};
There are multiple ways to implement this based on your overall system so for my suggestion, I am going to assume the following:
You want to keep the data as is for performance or some other reasons (i.e. flattening the object through some kind of "normalizer" is not an option).
The key cannot be an object, but has to be a string
Either you or the user can set the key.
There exists a character or a set of characters that can be used in a key string as a delimiter to construct a tree of keys (e.g. a dot in 'info.price', or an arrow in 'info->price'). An important property of the delimiter is that it is not valid to use it in a flat key (i.e. in the last example something like data = [{ 'info->price': '$50' }] is not allowed)
Ok now you just need to implement an accessor to use the complex keys on your object, something similar to Lodash.get. A simple implementation would be something like:
const DELIMITER = '->';
function get(obj, key) {
if (!obj) {
// in case of null or undefined
return obj;
}
if (!key) {
// empty string or something like that
return obj;
}
if (key.includes(DELIMITER)) {
const keyComponents = key.split(DELIMITER);
const firstKeyComponent = keyComponents.shift();
const newKey = keyComponents.join(DELIMITER);
return get(obj[firstKeyComponent], newKey)
}
return obj[key];
}
Emphasis on the simple here, because recalculating keyComponents every time is not ideal. You also might want add extra if cases for how to handle strings or arrays, those could cause problems if a key component is a number.
EDIT: Also maybe use Object.prototype.hasOwnProperty to check if a key is identical to a built in Object function, or better yet check if obj is a function and decide how to handle that scenario.
 
After you have this you can just replace that compare segment of your code with this:
sortableItems.sort((a: any, b: any) => {
const aValue = get(a, sortConfig.key);
const bValue = get(b, sortConfig.key);
if (aValue < bValue) {
return sortConfig.direction === 'ascending' ? -1 : 1;
}
if (aValue] > bValue) {
return sortConfig.direction === 'ascending' ? 1 : -1;
}
return 0;
});
And you're good to go for most cases. I don't know how "wild" your data can get so make sure to test a bunch of scenarios.
The problem you are running into is deep cloning of an object. the spread operator only goes 1 level deep and you are trying to go two. You can use libraries from lodash or other deep cloning. I use the JSON trick.
const [data, setData] = useState(initData);
function newArray() {
return JSON.parse(JSON.stringify(data));
}
Example shows sorting with two levels: https://codesandbox.io/s/serverless-sea-jt220?file=/src/App.js

test case failing due to .map is not a function error

Hi i have a react component expenses-total.js and a corresponding test case expenses-total.test.js as shown below.
expenses-total.js
export default (expenses=[]) => {
if (expenses.length === 0) {
return 0;
} else {
return expenses
.map(expense => expense.amount)
.reduce((sum, val) => sum + val, 0);
}
};
expenses-total.test.js
import selectExpensesTotal from '../../selectors/expenses-total';
const expenses = [
{
id: "1",
description: "gum",
amount: 321,
createdAt: 1000,
note: ""
},
{
id: "2",
description: "rent",
amount: 3212,
createdAt: 4000,
note: ""
},
{
id: "3",
description: "Coffee",
amount: 3214,
createdAt: 5000,
note: ""
}
];
test('Should return 0 if no expenses', ()=>{
const res = selectExpensesTotal([]);
expect(res).toBe(0);
});
test('Should correctly add up a single expense', ()=>{
const res = selectExpensesTotal(expenses[0]);
expect(res).toBe(321);
});
test('Should correctly add up multiple expenses',()=>{
const res = selectExpensesTotal(expenses);
expect(res).toBe(6747);
});
when i run the test case, its getting failed by giving an error
TypeError: expenses.map is not a function
I know the test case is correct but dont know what is wrong with thecomponent.
Could anyone please help me in fixing this error?
The problem is with if (expenses.length === 0) and the test case that uses selectExpensesTotal(expenses[0]):
expenses[0] passes an object, which has no length property, so in the function being tested, expenses.length returns undefined. However, undefined === 0 evaluates to false so your code goes into the else block tries to use .map on the object, which doesn't have that function, thus it throws an error.
In a brief: you can't map over an object.
expenses is an array of objects, so expenses[0] is an object.
Condition expenses.length === 0 evaluates to false, since obviously .length property does not exist on Object.prototype, so the else condition takes place - your function tries to map over an object.
The problem is that expenses[0] is an object (you probably expected it to be an array) and an object does not have a map function. A quick hack would be to add another ifs into the loop to check if expenses is actually an object. So that:
export default (expenses=[]) => {
if (expenses.length === 0) {
return 0;
} else {
if (typeof expenses === 'object') {
return expenses.amount
} else {
return expenses
.map(expense => expense.amount)
.reduce((sum, val) => sum + val, 0);
}
}
};
I hope this help.
To fix this error, you can pass in an array of object into
selectExpensesTotal([expenses[0]])
rather than just an object
selectExpensesTotal(expenses[0])
So your code show look like this:
test('Should correctly add up a single expense', ()=>{
const res = selectExpensesTotal([expenses[0]]);
expect(res).toBe(321);
});
.map function will now work on expenses. Because, this is now an array of object ( works with map function ) and not an object(This does not work with map function)

Get wildcarnames from string template

Is there a quick way (a known way I mean) to get wildcard names from a string template?, something like...
const str = `Hello ${name}, today is ${weekday}!`;
getWildCards(str); // will return ['name', 'weekday']
I'm creating a translation tool and the translation function will not know the wildcards in advance.
EDIT:
Actually, it turns out there's a native way to extract the params from a template literal using tagged template literals, which allow you to parse template literals with a function of the form:
function tag(strings, param1, param2, ..., paramN) { ... }
So, if instead of using variables inside the expressions of your template literal (${ foo }) you use strings (${ 'foo' }), then if you do:
tag`${ 'a' }${ 'b' } - XXX ${ 'c' }`
strings will be ['', '', ' - XXX ', ''] and you will receive 3 params with values 'a', 'b' and 'c'.
You can return whatever you want from a tag function, so a good solution for your use case would be to return a pair [paramsList, closure], where the closure will be a function that receives an object (map) with the params being used in the original string literal and uses them to build the resulting string. Like this:
function extractParams(strings, ...args) {
return [args, dict => {
return strings[0] + args
.map((arg, i) => dict[arg] + strings[i + 1]).join('');
}];
}
const [params, translate] = extractParams`Hello ${ 'name' }, today is ${ 'weekday' }`;
console.log(params);
console.log(translate({ name: 'Foo', weekday: 'Barday' }));
ORIGINAL ANSWER:
Assuming that template string is wrapped into a function so that it doesn't throw a ReferenceError, and that you change a bit the format of your template strings so that the arguments used are always properties from an object, you could use a proxy for that:
Let's say you have something like this:
function getSentence(key, args = {}) {
// Note that for the proxy solution to work well, you need to wrap each of them
// individually. Otherwise you will get a list of all the params from all the
// sentences.
const sentences = {
salutation: (args) => `Hello ${ args.name }, today is ${ args.weekday }!`,
weather: (args) => `Today is ${ args.weather } outside.`,
};
return sentences[key](args) || '';
}
function extractParams(key) {
const params = [];
const proxy = new Proxy({}, {
get: (target, name) => {
params.push(name);
},
});
getSentence(key, proxy);
return params;
}
console.log(extractParams('salutation'));
Anyway, note this will only work if you just have one level depth in your args, otherwise you will need a proxy that returns another proxy that returns another proxy... and keep track of the path (prop.subprop...). They should also return a function that returns a string for the last property that will be interpolated in the resulting string.
function getSentence(key, args = {}) {
// Note that for the proxy solution to work well, you need to wrap each of them
// individually. Otherwise you will get a list of all the params from all the
// sentences.
const sentences = {
salutation: (args) => `Hello ${ args.name }, today is ${ args.a.b.c.d }!`,
weather: (args) => `Today is ${ args.weather } outside.`,
};
return sentences[key](args) || '';
}
function getProxy(params, path = []) {
return new Proxy({}, {
get: (target, name) => {
if (typeof name === 'symbol') {
params.push(path);
return () => ''; // toString();
} else {
return getProxy(params, path.concat(name));
}
},
});
}
function extractParams(key) {
const params = [];
getSentence(key, getProxy(params));
return params;
}
console.log(extractParams('salutation'));

Categories

Resources