JSON stringify in Node JS not serializing array of objects - json

I am using sails.js (node js framework).
I am trying to JSON.stringify one of the objects, but when I do that it omits one of the fields (rooms array below).
Here is what console.log(object) gives me:
[ { rooms: [ [Object], [Object] ],
state: '53df76c278999310248072c6',
name: 'Sydney Center',
menuItems: null,
createdAt: Mon Aug 04 2014 23:42:08 GMT+0300 (Jerusalem Summer Time),
updatedAt: Mon Aug 04 2014 23:42:08 GMT+0300 (Jerusalem Summer Time),
id: '53dff0205c89c03428a31cee' },
{ rooms: [ [Object], [Object], [Object] ],
state: '53df76c278999310248072c6',
createdAt: Mon Aug 04 2014 23:43:21 GMT+0300 (Jerusalem Summer Time),
menuItems: null,
name: 'Batata Center',
updatedAt: Mon Aug 04 2014 23:51:11 GMT+0300 (Jerusalem Summer Time),
id: '53dff06a5c89c03428a31cf3' } ]
JSON output (rooms are not printed):
[
{
"state": "53df76c278999310248072c6",
"name": "Sydney Center",
"menuItems": null,
"createdAt": "2014-08-04T20:42:08.885Z",
"updatedAt": "2014-08-04T20:42:08.885Z",
"id": "53dff0205c89c03428a31cee"
},
{
"state": "53df76c278999310248072c6",
"createdAt": "2014-08-04T20:43:21.999Z",
"menuItems": null,
"name": "Batata Center",
"updatedAt": "2014-08-04T20:51:11.740Z",
"id": "53dff06a5c89c03428a31cf3"
}
]
What might be the problem?
The rooms data seems to be fine.
For the complete function (SailsJS):
getCentersData: function(req, res) {
sails.log.info('Teacher.getCentersData: ', req.user.id);
var userId = req.user.id;
async.auto({
teacher: function(cb) {
Teacher.findOne({ user: userId }).populate('centers').exec(cb);
},
centers: [
'teacher', function(cb, results) {
var allCentersIds = _.pluck(results.teacher.centers, 'id');
Center.findById(allCentersIds).populate('rooms').exec(cb);
}
],
rooms: [
'centers', function(cb, results) {
var allRoomIds = [];
_.each(results.centers, function(center) {
allRoomIds = _.union(allRoomIds, _.pluck(center.rooms, 'id'));
});
Room.findById(allRoomIds).populate('children').exec(cb);
}
],
children: [
'rooms', function(cb, results) {
var allChildrenIds = [];
_.each(results.rooms, function (room) {
allChildrenIds = _.union(allChildrenIds, _.pluck(room.children, 'id'));
});
Child.findById(allChildrenIds).populate('parents').exec(cb);
}
],
parentUsers: ['children', function(cb, results) {
var allParentIds = [];
_.each(results.children, function (child) {
allParentIds = _.union(allParentIds, _.pluck(child.parents, 'id'));
});
Parent.findById(allParentIds).populate('user').exec(cb);
}],
map: ['parentUsers', function (cb, results) {
// map children to parents
var parentsMapper = _.indexBy(results.parentUsers, 'id');
var childrenMappedToParents = _.map(results.children, function (child) {
var _child = child.toObject();
_child.parents = _.map(child.parents, function (parent) {
return parentsMapper[parent.id];
});
return _child;
});
var childrenMapper = _.indexBy(childrenMappedToParents, 'id');
// map rooms to children
var roomsMappedToChildren = _.map(results.rooms, function (room) {
var _room = room.toObject();
_room.children = _.map(room.children, function (child) {
return childrenMapper[child.id];
});
return _room;
});
var roomsMapper = _.indexBy(roomsMappedToChildren, 'id');
// map center to rooms
var centersMappedToRooms = _.map(results.centers, function (center) {
var _center = center.toObject();
_center.rooms = _.map(center.rooms, function (room) {
return roomsMapper[room.id];
});
return _center;
});
sails.log.info('centersMappedToRooms',centersMappedToRooms ); // includes rooms array
sails.log.info('centersMappedToRooms json: ', JSON.stringify(centersMappedToRooms)); // does not include rooms array
return cb(null, centersMappedToRooms);
}]
}, function(err, results) {
if (err) {
return res.serverError(err);
}
// added prints
sails.log.info("results.map: ", results.map);
sails.log.info("JSON.stringify(results.map): ", JSON.stringify(results.map)); // same same, does not print the rooms array
return res.json(results.map);
});
},
EDITED
Schema:
schema: true,
attributes: {
name: {
type: 'string',
required: true,
minLength: 5
},
// Many-To-Many association with Teacher model
teachers: {
collection: 'teacher',
via: 'centers'
},
// One-To-Many association with State model
state: {
model: 'state'
},
// One-To-Many association with Room model
rooms: {
collection: 'room',
via: 'center'
},
// One-To-One association with Principal model
principal: {
model: 'principal'
},
menuItems: {
type: 'array',
defaultsTo: null
}
},

Because Waterline queries return models, not plain javascript objects, they have additional properties and functions on them. One of these is an overridden toJSON() function which removes attributes that have not been populated. What seems to be happening here is that you are attaching objects to a parent model which doesn't know it has children that have been populated so it strips off the values.
The reasoning behind this is so that if you query for all Users and don't populate Rooms you don't get an incorrect result showing an empty rooms array.
I'm not sure what all you are manipulating here but the reason it works if you _.cloneDeep is because it removes the custom toJSON field. This is the recommend strategy when you are mutating a parent object from a query like this.

Related

Sequilize query is returning only one row while using include

Context : I am having this problem were I am doing a query using sequilize an it only return's me an array with one position even though I have more than one field that correspond to the query.
This are my two involved models
This is my group.js model
module.exports = (sequelize, DataTypes) => {
const Group = sequelize.define('Group', {
name: DataTypes.STRING,
limit: DataTypes.STRING,
user_id: DataTypes.INTEGER
});
Group.associate = models => {
Group.belongsTo(models.User, { foreignKey: 'user_id' });
};
Group.associate = models => {
Group.hasMany(models.Movement, { foreignKey: 'group_id' });
};
return Group;
}
This is my movement.js model
module.exports = (sequelize, DataTypes) => {
const Mov = sequelize.define('Movement', {
description: DataTypes.STRING,
value: DataTypes.INTEGER,
group_id: DataTypes.INTEGER
});
Mov.associate = models => {
Mov.hasOne(models.Group, { foreignKey: 'group_id' });
};
return Mov;
}
This is my query (where you will see that I am doing an INNER JOIN to SUM the fields of the Movement table)
router.get('/', verify, async (req, res) => {
try {
const group = await Group.findAll({
attributes: [
'id',
'name',
'limit',
[sequelize.fn('SUM', sequelize.col('Movements.value')), 'total_spent'],
],
include: [{
attributes: [], // this is empty because I want to hide the Movement object in this query (if I want to show the object just remove this)
model: Movement,
required: true
}],
where: {
user_id: req.userId
}
});
if (group.length === 0) return res.status(400).json({ error: "This user has no groups" })
res.status(200).json({ groups: group }) //TODO see why this is onyl return one row
} catch (error) {
console.log(error)
res.status(400).json({ Error: "Error while fetching the groups" });
}
});
Problem is that it only return's one position of the expected array :
{
"groups": [
{
"id": 9,
"name": "rgrgrg",
"limit": 3454354,
"total_spent": "2533"
}
]
}
It should return 2 positions
{
"groups": [
{
"id": 9,
"name": "rgrgrg",
"limit": 3454354,
"total_spent": "2533"
},
{
"id": 9,
"name": "rgrgrg",
"limit": 3454354,
"total_spent": "2533"
}
]
}
This is the query sequilize is giving me:
SELECT `Group`.`id`, `Group`.`name`, `Group`.`limit`, SUM(`Movements`.`value`) AS `total_spent` FROM `Groups` AS `Group` INNER JOIN `Movements` AS `Movements` ON `Group`.`id` = `Movements`.`group_id` WHERE `Group`.`user_id` = 1;
I guess you need to add an appropriate group by clause as follows -
const group = await Group.findAll({
attributes: [
'id',
'name',
'limit',
[sequelize.fn('SUM', sequelize.col('Movements.value')), 'total_spent'],
],
include: [{
attributes: [], // this is empty because I want to hide the Movement object in this query (if I want to show the object just remove this)
model: Movement,
required: true
}],
where: {
user_id: req.userId
},
group: '`Movements`.`group_id`'
});
Many-to-many "through" table with multiple rows of identical foreign key pairs only returns one result?
I just ran into this bug and added this options to the main query:
{
raw: true,
plain: false,
nest: true
}
Then you just merge the query.
It's a workaround, but might help someone.

Try to get key values recursively from JSON in Angular 5

I want to retrieve all the key values from a JSON file. For example in :
{
"total_count": 6,
"incomplete_results": false,
"items": [
{
"url": "https://api.github.com/repos/Samhot/GenIHM/issues/6",
"id": 293237635,
"number": 6,
"title": "Rechercher des documents",
"user": {
"login": "Samhot",
"id": 7148311
]
}
I would like to get :
["total_count", "incomplete_results", "items", "url", "url", "number", "title", "user", "login", "id"]
I have a function which return the content of my JSON in an observable :
getConfig(): Observable<any> {
return this.http.get<any>(this.myURL);
}
After that the data are reformated with .map to get only the keys with the Object.keys() function :
merge()
.pipe(
startWith({}),
switchMap(() => {
return this.getConfig();
}),
map(data => {
return Object.keys(data.items[0]);
}
)
)
.subscribe(data => {
this.dispo = data;
});
My problem is that i get only the keys that are in the level of the JSON I told
(data.items[0]) and not the ascendants or the descendants.
Of course I can create multiple requests but it asks to know in advance the structure of the JSON, what I want is to make it generic ...
How can I do to have an array with with all of my keys regardless of the structure of the JSON ?
Thanks in advance !
You would need to do a recursive function like:
function getDeepKeys(obj) {
const keys = Object.keys(obj);
const childKeys = keys
.map(key => obj[key])
.map(
value =>
Array.isArray(value)
? getDeepKeys(value[0])
: typeof value === "object"
? getDeepKeys(value)
: []
)
.reduce((acc, keys) => [...acc, ...keys], []);
return [...keys, ...childKeys];
}
const obj = {
total_count: 6,
incomplete_results: false,
items: [
{
url: "https://api.github.com/repos/Samhot/GenIHM/issues/6",
id: 293237635,
number: 6,
title: "Rechercher des documents",
user: {
login: "Samhot",
id: 7148311
}
},
{
url: "https://api.github.com/repos/Samhot/GenIHM/issues/6",
id: 293237635,
number: 6,
title: "Rechercher des documents",
user: {
login: "Samhot",
id: 7148311
}
}
]
};
console.log(getDeepKeys(obj));
Which then you would use like map(getDeepKeys). Note that this function assumes all the items in your array have the same schema.

Getting json object data with react

I am attempting to pull data out of json like this, which is imported as "values"
{
"content": {
"person": [
{
"name": "Test"
"age" : "24:
}
]
}
}
I am using .map like below but getting the error .default.map is not a function I believe it is because i have objects not arrays, i've tried a bunch of stuff including object.keys but i'm getting errors all over the place, any direction would be appreciated.
import values from './sample.json'
const vals = values.map((myval, index) => {
const items = person.items.map((item, i) => {
return (
<div>{item.name}</div>
)
})
return (
<div>{items}</div>
)
})
I think your data and code have some errors. But after fixing those and also changing the name from 'person' to 'people' if that's what you are after, here's the code that does what you are trying to do:
var data = {
content: {
people: [
{
name: "Test",
age: 24,
},
{
name: "Foo",
age: 25,
},
],
},
};
var App = React.createClass({
render: function () {
var people = data.content.people.map(function (person) {
return <div>{person.name}</div>;
});
return <div>{people}</div>;
},
});
ReactDOM.render(<App />, document.getElementById("app"));
And here's the JSBin for that: https://jsbin.com/coyalec/2/edit?html,js,output
Update: I'm updating the answer with more detailed example. It now deals with data more generically, like it doesn't assume what are the entries of 'contents' and such, but it knows that each type like 'people' or 'pets' are an array.
var data = {
content: {
people: [
{
name: "Test",
age: 24,
},
{
name: "Foo",
age: 25,
},
],
pets: [
{
name: "Sweety",
age: 3,
},
{
name: "Kitty",
age: 5,
},
],
},
};
var App = React.createClass({
render: function () {
// Get the keys in data.content. This will return ['people', 'pets']
var contentKeys = Object.keys(data.content);
// Now start iterating through these keys and use those keys to
// retrieve the underlying arrays and then extract the name field
var allNames = contentKeys.map((t) =>
data.content[t].map((e) => <div>{e.name}</div>)
);
return <div>{allNames}</div>;
},
});
ReactDOM.render(<App />, document.getElementById("app"));
And here's the latest JSBin: https://jsbin.com/coyalec/4/edit?html,js,output

test broke when changed model with interfaces to classes

Sorry if its a bit long but I wanted to explain myself well.
I have a test for an api call im performing from my service to receive a simple json. (Karma-jasmin/angular2)
this is the call from my service:
#Injectable()
export class MyService {
constructor(private _myApiService:MyAPIService) {
}
public getCarsData():Observable<Car[]> {
return this._myApiService.getCurrentCarData().map(res => res.carList);
}
}
this is getCurrentCarData():
export interface CarsListResponse extends ServerResponse {
carList: Cars[];
}
public getCurrentCarData(): Observable<CarsListResponse> {
let apiURL = 'my/api/url'
return this.http.get(apiURL),
(ret, retJson) => ret.status === 200 ? {carList: retJson.cars.map(element => new Car(element.year, element.make))} : undefined);
}
Car interface is:
export interface Car {
make:string;
year:number;
}
the json Im getting looks like this (part of a mock):
status: 200,
headers: new Headers(HEADERS),
body: {
cars: [
{
"make": "Mercedes",
"year": 2016
},
{
"make": "Audi",
"year": 2017
},
{
"make": "BMW",
"year": 2015
}
]
}
Test: the test is for getCurrentCarData():
let carsResponse = () => {
return { cars: [
{
"make": "Mercedes",
"year": 2016
},
{
"make": "Audi",
"year": 2017
},
{
"make": "BMW",
"year": 2015
}
]}
};
let carsExpectedResponse = () => {
return [
{
"make": "Mercedes",
"year": 2016
},
{
"make": "Audi",
"year": 2017
},
{
"make": "BMW",
"year": 2015
}
]
};
describe('GET Car Data', () => {
it('should handle respond', inject([XHRBackend, api.MyApiService], (mock, myApiService) => {
let c:Connection = null;
mock.connections.subscribe((connection) => {
connection.mock(new Response(new ResponseOptions({
status: 200,
headers: jsoHeaders(),
body: carsResponse()
})));
c = connection;
});
myApiService.getCurrentCarData().subscribe(
res => {
expect(c.request.url).toEqual(`my/api/url`);
expect(res.carList).toEqual(carsExpectedResponse());
},
error => {
expect(false).toBeTruthy();
}
);
}));
});
Ok, so this works! THE problem is when I changed my model from this
interface:
export interface Car {
make:string;
year:number;
}
to this class:
export class Car implements GenType {
make:string;
year:number;
constructor(make:string, year:number) {
this.make = make;
this.year = year;
}
displayFormat:() => string = function () {
return 'someStr'
}
}
export interface GenType {
displayFormat: () => string;
}
so now the error im getting is:
Expected
[MatcherEntityBulk({ displayFormat: Function, make: 'Mercedes', year: 2016 }),
MatcherEntityBulk({ displayFormat: Function, make: 'Audi', year: 2017 }),
MatcherEntityBulk({ displayFormat: Function, make: 'BMW', year: 2015 })]
to equal
[Object({ displayFormat: Function, make: 'Mercedes', year: 2016 }),
Object({ displayFormat: Function, make: 'Audi', year: 2017 }),
Object({ displayFormat: Function, make: 'BMW', year: 2015 })]
So the problem is pretty obvious, but how do I fix this, I mean with the current change what would be a proper way to change that test?
thanks a bunch for the ones that survived to this point :)
toEqual expects carsExpectedResponse() array elements to be instances of certain class (for example, Car). It tests object constructors and expects them to have non-enumerable property constructor that contains a constructor.
It can be a real instance:
let carsExpectedResponse = () => [
new Car(...),
...
];
It can be a fake non-enumerable constructor property:
let carsExpectedResponse = () => [
{
"make": "Mercedes",
"year": 2016
},
...
].map(obj => Object.defineProperty(obj, 'constructor', { value: Car }));
It can be an object constructed with desired prototype chain:
let carsExpectedResponse = () => [
{
"make": "Mercedes",
"year": 2016
},
...
].map(obj => Object.assign(Object.create(Car.prototype), obj));
The last fixture object is probably the most solid, because it doesn't depend on Jasmine internal logic and doesn't depend on constructor logic, too.

Nested arrays in object json to csv

Im using 'json-csv' library to create a csv from a users arrays with nested objects and arrays.
var users = [
{
subscriptions: [
{
package : {
name: 'Grammar'
},
state: 'EXPIRED',
timerange: {
period : 5550
},
transaction:{
amount: 10000
}
},
{
package : {
name: 'GK'
},
state: 'ACTIVE',
timerange: {
period : 30
},
transaction:{
amount: 10340
}
},
],
account:{
balance: 200
},
name: "Johhy Moe",
email: null,
user_id: "123456789",
username: null,
user_type: "facebook",
id: 3,
createdAt: "2016-07-11T08:02:40.000Z",
updatedAt: "2016-07-11T08:02:40.000Z",
},
{
subscriptions: [
{
package : {
name: 'GK'
},
state: 'EXPIRED',
timerange: {
period : 42
},
transaction:{
amount: 5252
}
},
{
package : {
name: 'MATH'
},
state: 'ACTIVE',
timerange: {
period : 25
},
transaction:{
amount: 200
}
}
],
account:{
balance: 1500
},
name: "John Doe",
email: null,
user_id: "123456789",
username: null,
user_type: "facebook",
id: 7,
createdAt: "2016-07-29T06:44:18.000Z",
updatedAt: "2016-07-29T06:44:18.000Z"
},
]
Now i want the generated csv to be like this
USERID,NAME,FBID,ACCOUNT,SUBSCRIPTION,PRICE,STATE,TIMEPERIOD
3,Johhy Moe,123456789,200,Grammar,10000,EXPIRED,5550
3,Johhy Moe,123456789,200,GK,10340,ACTIVE,30
7,John Doe,123456789,1500,GK,5252,EXPIRED,30
7,John Doe,123456789,1500,MATH,200,ACTIVE,25
As you see if there are two objects inside subscription array for each user, i want to repeat that user again but with different subscription data.
I've thought of using the library because my users array can go up to thousands of users with hundreds of subscription.
And i'm at a loss to what i should do.
my Code:
var options= {
fields : [
{
name : 'id',
label : 'USERID'
},
{
name : 'name',
label : 'Name'
},
{
name : 'user_id',
label : 'FBID'
},
{
name : 'account.balance',
label : 'ACCOUNT'
},
{
name: '',
label: 'Subscription'
}
]
}
var source = es.readArray(users)
source
.pipe(jsoncsv.csv(options))
.pipe(res)
I dont want to use a library also. So if someone could provide me with a resource to make my own csv file with strings and also using streams , that would be great. Thanks!!
This will solve your problem. Now you just have to change console.log to fs and write to your file.
var json2csv = function (json, listKeys) {
var str = "";
var prefix = "";
for (var i = 0; i < listKeys.length; i++) {
str += prefix + json[listKeys[i]];
prefix = ",";
}
return str;
};
var async = require('async');
var csvData = ['USERID,NAME,FBID,ACCOUNT,SUBSCRIPTION,PRICE,STATE,TIMEPERIOD'];
async.each(users, function (user, callback) {
var csvRow1 = {
USERID: user.id,
NAME: user.name,
FBID: user.user_id,
ACCOUNT: user.account.balance
};
async.each(user.subscriptions, function (subscription, callback) {
var csvRow2 = JSON.parse(JSON.stringify(csvRow1));
csvRow2.SUBSCRIPTION = subscription.package.name;
csvRow2.PRICE = subscription.transaction.amount;
csvRow2.STATE = subscription.state;
csvRow2.TIMEPERIOD = subscription.timerange.period;
csvData.push(json2csv(csvRow2, ['USERID', 'NAME', 'FBID', 'ACCOUNT', 'SUBSCRIPTION', 'PRICE', 'STATE', 'TIMEPERIOD']));
callback(null);
}, function (err) {
callback(err);
});
}, function (err) {
if (err) {
// return err;
} else {
// return csvData;
}
});