I'm using npm module traverse to filter data coming from mongodb / mongoose.
I might get this data:
[ { rating: 5,
title: { da: 'Web udvikling', en: 'Web Development' } },
{ rating: 5, title: { da: 'Node.js', en: 'Node.js' } } ]
'da' and 'en' indicates languages. I use traverse to filter mongoose data after current language like this:
var filtered = filterLanguage(lang, results);
// filter json obj by language
var filterLanguage = function(language, obj) {
return traverse(obj).map(function (item) {
if (this.key === language) {
this.parent.update(item);
}
});
};
I then show this in my template:
res.render('index', {
skills: filtered.skills
});
Finally I display it in the jade view:
ul.list-group
each skill, i in skills
if i < 5
li.list-group-item.sidebar-list-item= skill.title
Unfortunately it's displayed with quotes:
<ul>
<li>'Web Development'</li>
<li>'Node.js'</li>
</ul>
These quotes are not there in the unfiltered data (results.skill.title.da). So traverse is adding them. I used the module with 'plain' json and it's working perfectly.
The mongoose data seems plain and simple but of course there are a lot of properties on the prototype. Also traverse stalls if I don't omit '_id' (type bson/objectid) property from result set.
So traverse seems to have problems with mongoose data... Why is this? And how can I fix it?
-- EDIT --
I found a solution:
Before filtering I do this:
var json = JSON.parse(JSON.stringify(results));
var filtered = filterLanguage(lang, json);
This removes the quotes, but I'm not sure exactly what it does. Somehow converting the mongoose result to JSON? An explanation would be highly appreciated.
Fields in Mongoose documents are getters/setters, which seem to confuse either traverse or Jade/Pug.
The shortest method I found that seems to fix all of your issues is pretty ugly:
var filtered = filterLanguage(lang, results.map(r => JSON.parse(JSON.stringify(r))));
A more elaborate version:
var filtered = filterLanguage(lang, results.map(r => {
let j = r.toJSON()
j._id = j._id.toString()
return j;
}));
It would have been helpful to see what is the body of filterLanguage exactly or understand why it's called twice but as it stands, I don't think you need to use the traverse package at all.
A function such as below should do the trick and I even expanded it to work if the data is more tree-like and not as flat as represented in your example.
const reduceByLang = (data, lang) => {
// Look for a `lang` key in obj or
// if not found but still an object, recurse
const reduceByLangObj = (obj) => {
Object.keys(obj).forEach((key) => {
if (obj[key] === null) {
return;
}
if (obj[key][lang]) {
obj[key] = obj[key][lang]; // replace with desired lang
} else if (typeof obj[key] === 'object') {
reduceByLangObj(obj[key]); // recurse
}
});
return obj;
};
if (Array.isArray(data)) {
return data.map(reduceByLangObj);
} else {
return reduceByLangObj(data);
}
};
See example in JS Bin.
Also, if possible at all and if you do this type of selecting very often, I would look into saving the data in a different structure:
{ ratings: x, locales: { en: { title: 'Y' }, { da: { title: 'Z' } } } }
maybe, so that you can pick the selected language easily either in the query itself and/or in the controller.
EDIT: Checking for null.
Related
I'm trying to work around the fact that Datocms doesn't support a where filter in their GraphQL schema. Since there isn't that much data, I figured I could query all of it, and do the find on my end, but ... I'm not succeeding, at least not using "modern" methods.
What I get back when I query all of the data looks like this:
"foo": {
"data": {
"allGiveawayLandingPages": [
{
"lpSection": [
{},
{},
{},
{},
{},
{},
{},
{
"id": "34525949",
"products": [
{
"__typename": "PurchaseCardRecord",
"discountAmount": 50,
"discountAmountPct": null,
"discountEndDate": "2022-11-01T23:00:00+00:00",
"id": "44144096"
},
{
"__typename": "PurchaseCardRecord",
"discountAmount": null,
"discountAmountPct": null,
"discountEndDate": null,
"id": "44144097"
}
]
}
]
}
]
}
}
I need to find the object down in the "products" array by "id". This general question has been asked and answered lots of times, but the only answer I can get to work is from way back in 2013, and it seems to me there aught to be a more modern way to do it.
I'm doing this inside of a try/catch block, which I mention because it seems to be making this hard to debug (I'll come back to this):
export default async function createPaymentIntentHandler(req, res) {
const body = JSON.parse(req.body);
const {
productId,
productType
} = body;
let data;
if ('POST' === req.method) {
try {
switch (productType) {
case 'SeminarRecord':
data = await request({ query: singleSeminarQuery(productId) });
productObjName = 'seminar';
break;
default:
data = await request({ query: singleProductQuery(productId) });
productObjName = 'product';
}
/**
* Here's where I want to do my query / filtering
*/
// ... do more stuff and create Stripe paymentIntent
res.status(200).send({clientSecret: paymentIntent.client_secret})
} catch (error) {
logger.error({error}, 'Create Payment Intent error');
return res.status(400).end(`Create Payment Intent error: ${error.message}`);
}
} else {
res.status(405).end('Method not allowed');
}
}
My first, naive attempt was
const foo = await request({ query: ALL_PURCHASE_CARDS_QUERY });
const card = foo.data.allGiveawayLandingPages.find((page) => {
return page.lpSection.find((section) => {
return section?.products.find((record) => record.id === parentId)
})
});
logger.debug({card}, 'Got card');
In the abstract, aside from the fact that the above is fairly brittle because it relies on the schema not changing, I'd expect some similar sort of ES6 construction to work. This particular one, however, throws, but not in a particularly useful way:
[08:09:18.690] ERROR: Create Payment Intent error
env: "development"
error: {}
That's what I meant by it being hard to debug — I don't know why the error object is empty. But, in any case, that's when I started searching StackOverflow. The first answer which looked promising was this one, which I implemented as
...
const {
productId,
productType,
parentId
} = body;
...
function findCard(parent, id) {
logger.debug({parent}, 'searching in parent')
for (const item of parent) {
if ('PurchaseCardRecord' === item.__typename && item.id === id) return item;
if (item.children?.length) {
const innerResult = findCard(item.children, id);
if (innerResult) return innerResult;
}
}
}
if ('POST' === req.method) {
try {
...
const foo = await request({ query: ALL_PURCHASE_CARDS_QUERY });
const card = findCard(foo, parentId);
logger.debug({card}, 'Got card');
This similarly throws unhelpfully, but my guess is it doesn't work because in the structure, not all children are iterables. Then I found this answer, which uses reduce instead of my original attempt at find, so I took a pass at it:
const card = foo.data.allGiveawayLandingPages.reduce((item) => {
item?.lpSection.reduce((section) => {
section?.products.reduce((record) => {
if ('PurchaseCardRecord' === record.__typename && record.id === parentId) return record;
})
})
})
This is actually the closest I've gotten using ES6 functionality. It doesn't throw an error; however, it's also not returning the matching child object, it's returning the first parent object that contains the match (i.e., it's returning the whole "lpSection" object). Also, it has the same brittleness problem of requiring knowledge of the schema. I'm relatively certain something like this is the right way to go, but I'm just not understanding his original construction:
arr.reduce((a, item) => {
if (a) return a;
if (item.id === id) return item;
I've tried to understand the MDN documentation for Array.reduce, but, I don't know, I must be undercaffeinated or something. The syntax is described as
reduce((previousValue, currentValue) => { /* … */ } )
and then several variations on the theme. I thought it would return all the way up the stack in my construction, but it doesn't. I also tried
const card = foo.data.allGiveawayLandingPages.reduce((accumulator, item) => {
return item?.lpSection.reduce((section) => {
return section?.products.reduce((record) => {
if ('PurchaseCardRecord' === record.__typename && record.id === parentId) return record;
})
})
})
but the result was the same. Finally, not understanding what I'm doing, I went back to an older answer that doesn't use the ES6 methods but relies on recursing the object.
...
function filterCards(object) {
if (object.hasOwnProperty('__typename') && object.hasOwnProperty('id') && ('PurchaseCardRecord' === object.__typename && parentId === object.id)) return object;
for (let i=0; i<Object.keys(object).length; i++) {
if (typeof object[Object.keys(object)[i]] == 'object') {
const o = filterCards(object[Object.keys(object)[i]]);
if (o != null) return o;
}
}
return null;
}
if ('POST' === req.method) {
try {
...
const foo = await request({ query: ALL_PURCHASE_CARDS_QUERY });
const card = filterCards(foo);
logger.debug({card}, 'Got card');
This actually works, but ISTM there should be a more elegant way to solve the problem with modern Javascript. I'm thinking it's some combination of .find, .some, and .reduce. Or maybe just for ... in.
I'll keep poking at this, but if anyone has an elegant/modern answer, I'd appreciate it!
I have state set as follow
const [stories, setStories] = useState([]);
I fetch Data from API in array, and i map the array and set the using setStories as:
setStories(prevState => prevState.concat({user: {name: 'XYZ', profile: 'ABC', stories: [{id: 1, image: 'testing'}];
The above codes are working fine, but i am stuck, when i have to concat the latest story if the id did not matched with fetched data. I have tried below solution but it didnot help:
stories.map(story => {
if(story && story.hasOwnProperty(key)){
//where above key is the user key fetched from the another API, i.e., user key
story?.[key].stories.map(storedStory =>
id(storedStory.id !== fetchedStory.id){
story?.[key].stories.concat({story})}
but the above code did not work, as it only mutate the state and is avoiding re-rendering.
Looking for a clean and efficient method to overcome this. THanks
It's hard to tell what you're trying to accomplish without seeing a full example. But I think your main problem is that you're not using the returned value from map, and from the naming it looks like you're appending the wrong element.
It will help to simplify first.
const newState = stories.map(story => {
if (story?.hasOwnProperty(key)) {
const found = story[key].stories.find(s => s.id === fetchedStory.id);
if (found) {
return story;
} else {
// Let's make a new object with the fetchedStory
// appended into THIS user's stories
return {
...story,
[key]: {
...story[key],
stories: [
...story[key].stories,
// This is supposed to be fetchedStory
// not `story` right??
fetchedStory,
]
}
}
}
} else {
return story;
}
});
setStory(newState);
Edit: You're having a hard time expressing your business logic, and the complexity of the data structure is not helping. So keep simplifying, encapsulate the complex syntax into functions then express your business logic plainly. Ie,
const appendStory = (originalObject, userId, storyToAppend) => {
return {
...originalObject,
[userId]: {
...originalObject[userId],
stories: [
...originalObject[userId].stories,
storyToAppend,
]
}
}
};
const userExistsInList = (users, user) => {
return users?.hasOwnProperty(user);
}
const newStoryAlreadyInStories = (stories, newStory) => {
return stories.find(s => s.id === newStory.id);
}
const newState = stories.map(story => {
if (userExistsInList(story, key)) {
const found = newStoryAlreadyInStories(story[key].stories, fetchedStory);
if (found) {
// User is already in state and the new story is already in the list
// Add business logic here
} else {
// User is already in state and the new story
// is not in their list
// Add business logic here
}
} else {
// User is not in the list yet
// Add business logic here
}
});
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.
I have basically this structure for my data (this.terms):
{
name: 'First Category',
posts: [
{
name: 'Jim James',
tags: [
'nice', 'friendly'
]
},
{
name: 'Bob Ross',
tags: [
'nice', 'talkative'
]
}
]
},
{
name: 'Second Category',
posts: [
{
name: 'Snake Pliskin',
tags: [
'mean', 'hungry'
]
},
{
name: 'Hugo Weaving',
tags: [
'mean', 'angry'
]
}
]
}
I then output computed results so people can filter this.terms by tags.
computed: {
filteredTerms: function() {
let self = this;
let terms = this.terms; // copy original data to new var
if(this.search.tags) {
return terms.filter((term) => {
let updated_term = {}; // copy term to new empty object: This doesn't actually help or fix the problem, but I left it here to show what I've tried.
updated_term = term;
let updated_posts = term.posts.filter((post) => {
if (post.tags.includes(self.search.tags)) {
return post;
}
});
if (updated_posts.length) {
updated_term.posts = updated_posts; // now this.terms is changed even though I'm filtering a copy of it
return updated_term;
}
});
} else {
return this.terms; // should return the original, unmanipulated data
}
}
},
filteredTerms() returns categories with only the matching posts inside it. So a search for "angry" returns just "Second Category" with just "Hugo Weaving" listed.
The problem is, running the computed function changes Second Category in this.terms instead of just in the copy of it (terms) in that function. It no longer contains Snake Pliskin. I've narrowed it down to updated_term.posts = updated_posts. That line seems to also change this.terms. The only thing that I can do is reset the entire data object and start over. This is less than ideal, because it would be loading stuff all the time. I need this.terms to load initially, and remain untouched so I can revert to it after someone clears their search criterea.
I've tried using lodash versions of filter and includes (though I didn't really expect that to make a difference). I've tried using a more complicated way with for loops and .push() instead of filters.
What am I missing? Thanks for taking the time to look at this.
Try to clone the object not to reference it, you should do something like :
let terms = [];
Object.assign(terms,this.terms);
let terms = this.terms;
This does not copy an array, it just holds a reference to this.terms. The reason is because JS objects and arrays are reference types. This is a helpful video: https://www.youtube.com/watch?v=9ooYYRLdg_g
Anyways, copy the array using this.terms.slice(). If it's an object, you can use {...this.terms}.
I updated my compute function with this:
let terms = [];
for (let i = 0; i < this.terms.length; i++) {
const term = this.copyObj(this.terms[i]);
terms.push(term);
}
and made a method (this.copyObj()) so I can use it elsewhere. It looks like this:
copyObj: function (src) {
return Object.assign({}, src);
}
Okay, so I am trying to create a function that allows you to input an array of Objects and it will return an array that removed any duplicate objects that reference the same object in memory. There can be objects with the same properties, but they must be different in-memory objects. I know that objects are stored by reference in JS and this is what I have so far:
const unique = array => {
let set = new Set();
return array.map((v, index) => {
if(set.has(v.id)) {
return false
} else {
set.add(v.id);
return index;
}
}).filter(e=>e).map(e=>array[e]);
}
Any advice is appreciated, I am trying to make this with a very efficient Big-O. Cheers!
EDIT: So many awesome responses. Right now when I run the script with arbitrary object properties (similar to the answers) and I get an empty array. I am still trying to wrap my head around filtering everything out but on for objects that are referenced in memory. I am not positive how JS handles objects with the same exact key/values. Thanks again!
Simple Set will do the trick
let a = {'a':1}
let b = {'a': 1,'b': 2, }
let c = {'a':1}
let arr = [a,b,c,a,a,b,b,c];
function filterSameMemoryObject(input){
return new Set([...input])
}
console.log(...filterSameMemoryObject(arr))
I don't think you need so much of code as you're just comparing memory references you can use === --> equality and sameness .
let a = {'a':1}
console.log(a === a ) // return true for same reference
console.log( {} === {}) // return false for not same reference
I don't see a good reason to do this map-filter-map combination. You can use only filter right away:
const unique = array => {
const set = new Set();
return array.filter(v => {
if (set.has(v.id)) {
return false
} else {
set.add(v.id);
return true;
}
});
};
Also if your array contains the objects that you want to compare by reference, not by their .id, you don't even need to the filtering yourself. You could just write:
const unique = array => Array.from(new Set(array));
The idea of using a Set is nice, but a Map will work even better as then you can do it all in the constructor callback:
const unique = array => [...new Map(array.map(v => [v.id, v])).values()]
// Demo:
var data = [
{ id: 1, name: "obj1" },
{ id: 3, name: "obj3" },
{ id: 1, name: "obj1" }, // dupe
{ id: 2, name: "obj2" },
{ id: 3, name: "obj3" }, // another dupe
];
console.log(unique(data));
Addendum
You speak of items that reference the same object in memory. Such a thing does not happen when your array is initialised as a plain literal, but if you assign the same object to several array entries, then you get duplicate references, like so:
const obj = { id: 1, name: "" };
const data = [obj, obj];
This is not the same thing as:
const data = [{ id: 1, name: "" }, { id: 1, name: "" }];
In the second version you have two different references in your array.
I have assumed that you want to "catch" such duplicates as well. If you only consider duplicate what is presented in the first version (shared references), then this was asked before.