customize json response in graphQL - node.js

i use Express-js and express graphQL module to create my endpoint and web service ;
i am looking for way to create custom response in graphQL my endpoint is simple
select books from database my response is
{
"data": {
"books": [
{
"id": "5b5c02beab8dc1182b2e0a03",
"name": "dasta"
},
{
"id": "5b5c02c0ab8dc1182b2e0a04",
"name": "dasta"
}
]
}
}
but in need something like this
{
"result": "success",
"msg" : "list ...",
"data": [
{
"id": "5b5c02beab8dc1182b2e0a03",
"name": "dasta"
},
{
"id": "5b5c02c0ab8dc1182b2e0a04",
"name": "dasta"
}
]
}
here is my bookType
const BookType = new GraphQLObjectType({
name: 'Book',
fields: () => ({
id: {type: GraphQLID},
name: {type: GraphQLString},
genre: {type: GraphQLString},
author_id: {type: GraphQLString},
author: {
type: AuthorType,
resolve(parent, args) {
return Author.findById(parent.author_id);
}
}
})
});

That's not a legal GraphQL response. As per section 7.1 of the spec, after describing the data, errors, and extensions: top-level keys:
... the top level response map must not contain any entries other than the three described above.
You might put this data into extensions; or make it an explicit part of your GraphQL API; or simply let "success" be implied by the presence of a result and the lack of an error.

Related

How to sync the schema change for old document collection in mongodb with default values

How can I apply the schema changes to sync with default value to all the old data in mongodb
import mongoose from "mongoose";
interface ITodo {
title: string;
description: string;
by: string;
}
interface todoModelInterface extends mongoose.Model<TodoDoc> {
build(attr: ITodo): TodoDoc;
}
interface TodoDoc extends mongoose.Document {
title: string;
description: string;
by: string;
}
const todoSchema = new mongoose.Schema({
title: {
type: String,
required: true,
},
description: {
type: String,
required: true,
},
by: {
type: String,
required: true,
},
});
todoSchema.statics.build = (attr: ITodo) => {
return new Todo(attr);
};
const Todo = mongoose.model<TodoDoc, todoModelInterface>("Todo", todoSchema);
Todo.build({
title: "some title",
description: "some description",
by: "special",
});
Todo.collection.dropIndexes(function () {
Todo.collection.reIndex(function (finished) {
console.log("finished re indexing");
});
});
Todo.collection
.getIndexes()
.then((indexes: any) => {
console.log("indexes:", indexes);
})
.catch(console.error);
export { Todo };
Db:
[{
"_id": {
"$oid": "62cee1eea60e181e412cb0a2"
},
"title": "one",
"description": "one desc"
},{
"_id": {
"$oid": "62cee2bd44026b1f85464d41"
},
"title": "one",
"description": "one desc",
"by": "alphs"
},{
"_id": {
"$oid": "62cee3c8cf1592205dacda3e"
},
"title": "one",
"description": "one desc",
"by": "alphs"
}]
Here the old data still missing the "by" key, similarly if there is nested schema change it may impact the old users, how can we define the default collection for old data in mongodb at runtime without using update query migration?
Have you tried setting the default value for "by". By giving a default value, if the old data is missing a value then the default will kick in and return the default value provided. Read about Mongoose Default: Here. I don't know if this is a good practice but we also use this method when there is change in schema and don't want to run update query.

Mongoose populate 3 deep nested schema with JSON response

I have a find() query that when executed, I can see the json with the nested schemas that I want to see except for the 'artista' attribute only displays the id, instead of the properties I want. See below:
{
"total": 1,
"ordenes": [
{
"artpieces": [
{
"_id": "60c1388f30316c02b9f6351f",
"artista": "60c055736c7ca511055a0e1a",
"nombre": "LILIES"
},
{
"_id": "60c12fca30316c02b9f63519",
"nombre": "GERNICA",
"artista": "60c136bf30316c02b9f6351b"
}
],
"_id": "60c414f9ea108a14ef75a9fb",
"precio": 3000,
"usuario": {
"_id": "609c0068e67e68",
"nombre": "Arturo Filio"
}
}
]
}
The query I use to get the json above:
const [total, ordenes] = await Promise.all([
Orden.countDocuments(),
Orden.find()
.populate("usuario", "nombre")
.populate("artpieces", ["nombre","artista","nombre"])
]);
res.json({
total,
ordenes
});
It's an order schema that has artpieces. Each artpiece (I called it 'producto'), has a name a genre, an author/artist and the user which the order belongs to.
my Schema for the orden.js:
const { Schema, model } = require('mongoose');
const OrdenSchema = Schema({
artpieces: [
{
type: Schema.Types.ObjectId,
required: true,
ref: 'Producto'
}
],
estado: {
type: Boolean,
default: true,
required: true
},
usuario: {
type: Schema.Types.ObjectId,
ref: 'Usuario',
required: true
},
precio: {
type: Number,
required: true
}
})
OrdenSchema.methods.toJSON = function () {
const { __v, estado, ...data} = this.toObject();
return data;
}
module.exports = model('Orden', OrdenSchema);
Last thing I want to mention, I know for a fact that I have the code necessary in the artista.js model to display the name of the artist because I have a similar query to display all the artpieces with each artpiece have a genre and an artist.
That example looks like so (to give context):
{
"total": 4,
"productos": [
{
"precio": 0,
"_id": "60c12fca30316c02b9f63519",
"nombre": "GERNICA",
"categoria": {
"_id": "60c04e3605d3c10ed10389e4",
"nombre": "NEO CUBISMO"
},
"artista": {
"_id": "60c136bf30316c02b9f6351b",
"nombre": "PICASSO"
},
"usuario": {
"_id": "609c8c0068e67e68",
"nombre": "Arturo Filio"
}
}
]
}
What am I doing wrong that I can't get my json result at the top look like the json at the bottom, where the artist attribute is?
Also just to point out, I have checked how to nest populate methods in order SO posts including the path and the ref to the Schema and still haven't been able to get the expected result.

Mongoose NodeJS Express - Edit a specific sub document field

I have the following schemas designed in my Node server
SCHEMAS
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const dataSchema = new Schema({
time: Date,
value: String
});
const nodeSchema = new Schema({
name: String,
description: String,
number: Number,
status: String,
lastSeen: Date,
data: [dataSchema]
});
const siteSchema = new Schema({
code: String,
name: String,
description: String,
totalNodes: Number,
nodes: [nodeSchema]
});
const Site = mongoose.model('site',siteSchema);
module.exports = Site;
They basically look like this. You can see there are two nodes with some demo data.
EXAMPLE
{
"_id": "5fa169473a394829bc485069",
"code": "xfx3090",
"name": "Name of this site",
"description": "Some description",
"totalNodes": 2,
"__v": 0,
"nodes": [
{
"_id": "5fa1af361e085b516066d7e2",
"name": "device name",
"description": "device description",
"number": 1,
"status": "Offline",
"lastSeen": "2020-11-03T19:27:50.062Z",
"data": [
{
"Date": "2019-01-01T00:00:00.000Z",
"value": "12"
},
{
"Date": "2019-01-01T00:00:00.000Z",
"Value": "146"
}
]
},
{
"_id": "5fa1b10f4f24051520f85a58",
"name": "device name",
"description": "device description",
"number": 2,
"status": "Offline",
"lastSeen": "2020-11-03T19:35:43.409Z",
"data": [
{
"Date": "2019-01-01T00:00:00.000Z",
"Value": "555"
}
]
}
]
}
]
My question is how can I update a specific field of a node, in particular how I can update the last seen or the status. It is important to mention that the client making the request will only have access the the site code and the node number. The Object Id's of sites and nodes will not be known.
So far this is what I have, but it only creates one new Object Id for some reason.
Any advice will be appreciated
updateNode: async (req,res,next) => {
const {siteCode} = req.params;
const { nodeNumber } = req.params;
const status = req.body.status;
const nodeStatus = await Site.findOneAndUpdate({'code': siteCode, 'nodes.number':nodeNumber}, { '$set': {'nodes.$.status': {'status':status}}});
res.status(200).json({message: 'success'});
}
You'll need to do it this way.
I have predefined the ._ids.
You can do this dynamically if you want. If you are using express you could just use queries. Example req.query.documentID. The URL to access it will be localhost:p/?documentID=5fa169473a394829bc485069&nodeID=5fa1af361e085b516066d7e2
p in localhost is for port
await Site
.findOne({
"_id": "5fa169473a394829bc485069",
"nodes._id": "5fa1af361e085b516066d7e2"
})
.update({ "lastSeen": Date })
.then(doc => res.json(doc))
.catch(e => console.log(e))
Basically finding a doc with id of 5fa169473a394829bc485069
Then a node with _id of 5fa1af361e085b516066d7e2
And then update() method and { "lastSeen": Date } parameter to Date.
That's it!
EDIT
You'll have to create a VALID MongoDB object by doing this
app.get("/new", async (req, res) => {
let Site = new model({
code: "String",
name: "String",
description: "String",
totalNodes: 2,
nodes: [
{
_id: new mongoose.Types.ObjectId,
name: "String",
description: "String",
number: 1,
status: "offline",
lastSeen: Date.now(),
data: [{ "someData": "someData" }]
},
{
_id: new mongoose.Types.ObjectId,
name: "String",
description: "String",
number: 2,
status: "offline",
lastSeen: Date.now(),
data: [{ "someData": "someData" }]
}
]
});
await Site
.save()
.then(doc => {
console.log(doc);
res.json(doc);
})
.catch(e => console.error(e));
});
Everything is loaded with dummy data. Then you update the data like this.
app.get("/", async (req, res) => {
await model
.findOne({ "code": "String" })
.update({
"nodes.0.status": "online"
})
.then(doc => {
console.log(doc);
res.json(doc);
})
.catch(e => console.error(e));
})
Basically you access the object at the index position 0 ( that means the first post ) like this nodes.0 and then the status of that object will be respectively nodes.0.status. Then you just save the object and that's it!

GraphQL: Resolver for nested object types in graphql

Lets assume in the graphql schema, a UserType object is present:
const UserType = new GraphQLObjectType({
name:'User',
fields:() => ({
id: {type:GraphQLString},
name: {type: GraphQLString},
email: {type: GraphQLString},
age: {type: GraphQLInt},
friends: {type: new GraphQLList(UserType)}
});
});
The following data is present in the database:
{
"Users":[
{
"id": "1",
"name": "John Doe",
"email": "John#gmail.com",
"age": 35,
"friends": [
"3",
"5",
"7"
]
}
]
}
Query:
user {
name
friends {
name
}
}
As can be seen in the above example, the friends with ids are stored in the database.
How do I go about writing a resolver to get the user details (by id) plus details of all the users friends at the same time by sending a single graphql request?
The resolver takes four arguments (obj, args, ctx, info).
The first argument, in this case, obj has the result of the resolver from the parent's object (The parent resolver to the UserType). Therefore if you do obj.friends in the friends field's resolver, you will get the correct result.
Example:
friends: {
type: new GraphQLList(GraphQLString),
resolve: (obj) => {
return obj.friends
}
}

Upsert Using an Array Without Creating Duplicates

I'm having trouble 'upserting' to my array. The code below creates duplicates in my answers array which I definitely do not want and by now it's apparent $push will not work. I have tried using the different methodologies I see on SO for a while now but none are working for me. With this web app, users are allows to view a question on the website and respond with a 'yes' or 'no' response and they are allowed to change(upsert) their response at any one time meaning a sort of upsert takes place on the db at different times. How do get around this?
var QuestionSchema = Schema ({
title :String,
admin :{type: String, ref: 'User'},
answers :[{type: Schema.Types.Mixed, ref: 'Answer'}]
});
var AnswerSchema = Schema ({
_question :{type: ObjectId, ref: 'Question'},
employee :{type: String, ref: 'User'},
response :String,
isAdmin :{type: Boolean, ref: 'User'}
})
var UserSchema = Schema({
username : String,
isAdmin : {type: Boolean, default: false}
});
module.exports = mongoose.model('Question', QuestionSchema);
module.exports = mongoose.model('Answer', AnswerSchema);
module.exports = mongoose.model('User', UserSchema);
Question.update(
{_id: req.body.id},
{$push: {answers: {_question: req.body.id,
employee: req.body.employee,
response: req.body.response, //this variable changes (yes/no/null)
isAdmin: req.body.isAdmin}}},
{safe: true, upsert: true},
function(err, model) {
}
);
As I see it you seem a little confused and it's reflected in your schema. You don't seem to fully grasp the differences between "embedded" and "referenced" since your schema is actually an invalid "mash" of the two techniques.
Probably best to walk you through both of them.
Embedded Model
So instead of the schema you have defined, you should in fact have something more like this:
var QuestionSchema = Schema ({
title :String,
admin :{type: String, ref: 'User'},
answers :[AnswerSchema]
});
var AnswerSchema = Schema ({
employee :{type: String, ref: 'User'},
response :String,
isAdmin :{type: Boolean, ref: 'User'}
})
mongoose.model('Question', questionSchema);
NOTE: Question is the only actual model here. The AnswerSchema is completely "embedded".
Note the clear definition of the "schema" where the "answers" property in Question is defined as an "array" of AnswerSchema. This is how you do embedding and keep control of the types within the object inside the array.
As for the update, there is a clear logic pattern but you are simply not enforcing it. All you need to do is "tell" the update that you do not want to "push" a new item if something for that "unique" "employee" in the array already exists.
Also. This is NOT and "upsert". Upsert implies "creating a new one", which is different to what you want. You want to "push" to the array of an "existing" Question. If you leave "upsert" on there, then something not found creates a new Question. Which is of course wrong here.
Question.update(
{
"_id": req.body.id,
"answers.employee": { "$ne": req.body.employee },
}
},
{ "$push": {
"answers": {
"employee": req.body.employee,
"response": req.body.response,
"isAdmin": req.body.isAdmin
}
}},
function(err, numAffected) {
});
That will look to check that the "unique" "employee" in the array members already and will only $push where it is not already there.
As a bonus, if you intend to allow the user to "change their answer" then we do this incantation with .bulkWrite():
Question.collection.bulkWrite(
[
{ "updateOne": {
"filter": {
"_id": req.body.id,
"answers.employee": req.body.employee,
},
"update": {
"$set": {
"answers.$.response": req.body.response,
}
}
}},
{ "updateOne": {
"filter": {
"_id": req.body.id,
"answers.employee": { "$ne": req.body.employee },
},
"update": {
"$push": {
"answers": {
"employee": req.body.employee,
"response": req.body.response,
"isAdmin": req.body.isAdmin
}
}
}
}}
],
function(err, writeResult) {
}
);
This effectively puts two updates in one. The first to attempt to alter an existing answer and $set the response at the matched position, and the second to attempt to add a new answer where one was not found on the question.
Referenced Model
With a "referenced" model you actually have the real members of the Answer within their own collection. So instead the schema is defined like this:
var QuestionSchema = Schema ({
title :String,
admin :{type: String, ref: 'User'},
answers :[{ type: Schema.Types.ObjectId, ref: 'Answer' }]
});
var AnswerSchema = Schema ({
_question :{type: ObjectId, ref: 'Question'},
employee :{type: String, ref: 'User'},
response :String,
isAdmin :{type: Boolean, ref: 'User'}
})
mongoose.model('Answer', answerSchema);
mongoose.model('Question', questionSchema);
N.B The other ref's here to User such as :
employee :{type: String, ref: 'User'},
isAdmin :{type: Boolean, ref: 'User'}
These are also really incorrect, and also should be of Schema.Type.ObjectId as they will "reference" the actual _id field of User. But this is actually outside of the scope of this question as asked, so if you still don't grasp that after this read, then Ask a New Question so someone can explain. On with the rest of the answer.
That's the "general" shape of the schema though, with the important thing being the "ref" to the 'Anwser' "model", which is by the registered name. You can optionally just use your "_question" field in modern mongoose versions with a "virtual", but I'm skipping over "Adavanced Usage" for now and keeping it simple with an array of "references" still in the Question model.
In this case, since the Answer model is actually in it's own "collection", then the operations actually become "upserts". Where we only want to "create" when there is no "employee" response to the given "_question" id.
Also demonstrating with a Promise chain instead:
Answer.update(
{ "_question": req.body.id, "employee": req.body.employee },
{
"$set": {
"response": req.body.reponse
},
"$setOnInsert": {
"isAdmin": req.body.isAdmin
}
},
{ "upsert": true }
).then(resp => {
if ( resp.hasOwnProperty("upserted") ) {
return Question.update(
{ "_id": req.body.id, "answers": { "$ne": resp.upserted[0]._id },
{ "$push": { "answers": resp.upserted[0]._id } }
).exec()
}
return;
}).then(resp => {
// either undefined where it was not an upsert or
// the update result from Question where it was
}).catch(err => {
// something
})
This is actually a simple statement since "when matched" we want to change the "response" data with the payload of the request, and really only when "upserting" or "creating/inserting" is when we actually change other data such as the "employee" ( which is always implied for create as part of the query expression ) and the "isAdmin" which clearly should not change with each update request we then explicitly use $setOnInsert so it only writes those two fields on an actual "create".
In the "Promise Chain" we actually look to see if the update request to Answer actually resulted in an "upsert", and when it does we want to append to the array of Question where it does not already exist. In much the same way as the "embedded" example, it's best to look to see if the array actually has the item before modifying with the "update". Alternately you could $addToSet here and just let the query match the Question by _id. To me though, that's a wasted write.
Summary
Those are your different approaches to how you handle this. Each has their own use cases for which you can see a general summary some other answers of mine in:
Mongoose populate vs object nesting which is an overview of the different approaches and reasons behind them
How to Model a “likes” voting system with MongoDB which gives a bit more detail on the "unique array upserts" technique for "embedded" models.
Not "required" reading, but it may help expand your insight into which approach is best for your particular case.
Working Example
Copy these and put them in a directory and do an npm install to install local dependencies. The code will run and create the collections in the database making the alterations.
Logging is turned on with mongoose.set(debug,true) so you should look at the console output and see what it does, along with the resulting collections where answers will be recorded to the related questions, and overwritten instead of "duplicating" where that was also the intent.
Change the connection string if you have to. But that is all you should change in this listing for it's purpose. Both approaches described in the answer are demonstrated.
package.json
{
"name": "colex",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"async": "^2.4.1",
"mongodb": "^2.2.29",
"mongoose": "^4.10.7"
}
}
index.js
const async = require('async'),
mongoose = require('mongoose'),
Schema = mongoose.Schema,
ObjectId = require('mongodb').ObjectID
mongoose.Promise = global.Promise;
mongoose.set('debug',true);
mongoose.connect('mongodb://localhost/coltest');
const userSchema = new Schema({
username: String,
isAdmin: { type: Boolean, default: false }
});
const answerSchemaA = new Schema({
employee: { type: Schema.Types.ObjectId, ref: 'User' },
response: String,
});
const answerSchemaB = new Schema({
question: { type: Schema.Types.ObjectId, ref: 'QuestionB' },
employee: { type: Schema.Types.ObjectId, ref: 'User' },
response: String,
});
const questionSchemaA = new Schema({
title: String,
admin: { type: Schema.Types.ObjectId, ref: 'User' },
answers: [answerSchemaA]
});
const questionSchemaB = new Schema({
title: String,
admin: { type: Schema.Types.ObjectId, ref: 'User' },
answers: [{ type: Schema.Types.ObjectId, ref: 'AnswerB' }]
});
const User = mongoose.model('User', userSchema);
const AnswerB = mongoose.model('AnswerB', answerSchemaB);
const QuestionA = mongoose.model('QuestionA', questionSchemaA);
const QuestionB = mongoose.model('QuestionB', questionSchemaB);
async.series(
[
// Clear data
(callback) => async.each(mongoose.models,(model,callback) =>
model.remove({},callback),callback),
// Create some data
(callback) =>
async.each([
{
"model": "User",
"object": {
"_id": "594a322619ddbd437193c759",
"name": "Admin",
"isAdmin": true
}
},
{
"model": "User",
"object": {
"_id": "594a323919ddbd437193c75a",
"name": "Bill"
}
},
{
"model": "User",
"object": {
"_id": "594a327b19ddbd437193c75b",
"name": "Ted"
}
},
{
"model": "QuestionA",
"object": {
"_id": "594a32f719ddbd437193c75c",
"admin": "594a322619ddbd437193c759",
"title": "Question A Model"
}
},
{
"model": "QuestionB",
"object": {
"_id": "594a32f719ddbd437193c75c",
"admin": "594a322619ddbd437193c759",
"title": "Question B Model"
}
}
],(data,callback) => mongoose.model(data.model)
.create(data.object,callback),
callback
),
// Submit Answers for Users - Question A
(callback) =>
async.eachSeries(
[
{
"_id": "594a32f719ddbd437193c75c",
"employee": "594a323919ddbd437193c75a",
"response": "Bills Answer"
},
{
"_id": "594a32f719ddbd437193c75c",
"employee": "594a327b19ddbd437193c75b",
"response": "Teds Answer"
},
{
"_id": "594a32f719ddbd437193c75c",
"employee": "594a323919ddbd437193c75a",
"response": "Bills Changed Answer"
}
].map(d => ([
{ "updateOne": {
"filter": {
"_id": ObjectId(d._id),
"answers.employee": ObjectId(d.employee)
},
"update": {
"$set": { "answers.$.response": d.response }
}
}},
{ "updateOne": {
"filter": {
"_id": ObjectId(d._id),
"answers.employee": { "$ne": ObjectId(d.employee) }
},
"update": {
"$push": {
"answers": {
"employee": ObjectId(d.employee),
"response": d.response
}
}
}
}}
])),
(data,callback) => QuestionA.collection.bulkWrite(data,callback),
callback
),
// Submit Answers for Users - Question A
(callback) =>
async.eachSeries(
[
{
"_id": "594a32f719ddbd437193c75c",
"employee": "594a323919ddbd437193c75a",
"response": "Bills Answer"
},
{
"_id": "594a32f719ddbd437193c75c",
"employee": "594a327b19ddbd437193c75b",
"response": "Teds Anwser"
},
{
"_id": "594a32f719ddbd437193c75c",
"employee": "594a327b19ddbd437193c75b",
"response": "Ted Changed it"
}
],
(data,callback) => {
AnswerB.update(
{ "question": data._id, "employee": data.employee },
{ "$set": { "response": data.response } },
{ "upsert": true }
).then(resp => {
console.log(resp);
if (resp.hasOwnProperty("upserted")) {
return QuestionB.update(
{ "_id": data._id, "employee": { "$ne": data.employee } },
{ "$push": { "answers": resp.upserted[0]._id } }
).exec()
}
return;
}).then(() => callback(null))
.catch(err => callback(err))
},
callback
)
],
(err) => {
if (err) throw err;
mongoose.disconnect();
}
)
Here was my quick work around before Neill updated his answer (I used a $pull & $push). Works just as his but I'll mark his correct as I believe it's more efficient.
Question.update(
{_id: req.body.id},
{$pull: {answers: { employee: req.body.employee}}},
{safe: true, multi:true, upsert: true},
function(err, model) {
}
);
Question.update(
{_id: req.body.id},
{$push: {answers: {_question: req.body.id,
employee: req.body.employee,
response: req.body.response,
isAdmin: req.body.isAdmin}}},
{safe: true, upsert: true},
function(err, model) {
}
);

Resources