I have been writing an API that uses GraphQL. I am still pretty new to it, and have been running into some problems regarding mutations. A simplistic form of my API has two record types. There is a contact record and a tag record. A contact record can have multiple tag records associated with it.
The schema I wrote for each of these record types are below:
const Tag = new graphQL.GraphQLObjectType({
name: 'Tag',
description: 'Categorizes records into meaningful groups',
fields: () => ({
_id: {
type: graphQL.GraphQLID
},
name: {
type: graphQL.GraphQLString
}
})
});
const Contact = new graphQL.GraphQLObjectType({
name: 'Contact',
description: 'Contact record',
fields: () => ({
_id: {
type: graphQL.GraphQLID
},
name: {
type: graphQL.GraphQLString
},
tags: {
type: new graphQL.GraphQLList(Tag),
resolve: function(src, args, context) {
return TagModel.findByContactId(src._id)
.then(tags => {
return Promise.map(tags, (tag) => {
return TagModel.findById(tag.tag_id);
});
});
}
}
})
});
I can make a mutation easy enough on records such as tags since they don't contain nested records of their own, but I'm not sure how to make a mutation on a record like contacts since it can contain tags as well. The mutation code I put in place looks like this:
const Mutation = new graphQL.GraphQLObjectType({
name: 'Mutation',
fields: {
createContact: {
type: Contact,
description: "Create Contact",
args: {
name: {type: new graphQL.GraphQLNonNull(graphQL.GraphQLString)},
tags: {type: new graphQL.GraphQLList(Tag)}
},
resolve: function(source, args) {
return ContactModel.save(args.name);
}
}
}
});
I'm not sure how to complete the resolver in the mutation in order to be able to save a contact and tag records at the same time. For instance, if I made a mutation query to save a new contact record with a new tag like this:
{"query": "mutation createNewContact {
contact: createContact (name: "John Smith", tags { name: "family" } )
{_id, text, tags { name } } }" }
Is there something special that I need to do in my mutation schema in order to allow for this type of mutation to happen?
You can't use Tag as an input object type, you would have to create a type like TagInput
const TagInput = new GraphQLInputObjectType({
name: 'TagInput',
fields: {
_id: { type: GraphQLID },
name: { type: GraphQLString }
}
});
It is recommended to always create Input version of your normal type. You could do the same with Contact by creating ContactInput. Then you could create a mutation in very similar way you did it
const Mutation = new GraphQLObjectType({
name: 'Mutation',
fields: {
createContact: {
type: Contact,
args: {
contact: { type: new GraphQLNonNull(ContactInput) },
tags: { type: new GraphQLList(TagInput) }
},
resolve: (root, args, context) => {
console.log(args);
// this would console something like
// { contact: { name: 'contact name' },
// tags: [ { name: 'tag#1' }, { name: 'tag#2' } ] }
// here create contact with tags
}
}
});
The query you would run would look like that
{
"operationName": "createContact",
"query": "mutation createContact($contact: ContactInput!, $tags: [TagInput])
{
createContact(contact: $contact, tags: $tags) {
_id
text
tags {
name
}
}
}",
"variables": {
contact: { name: "contact name" },
tags: [ { name: "tag#1" }, { name: "tag#2" } ]
}
}
Related
Maybe this is a simple fix, but I can't seem to figure out what I'm doing wrong here.
I have a table that lists all the states
Model:
static get jsonSchema() {
return {
type: 'object',
properties: {
id: { type: 'integer' },
name: { type: 'string', minLength: 1, maxLength: 100 },
},
}
}
static get relationMappings() {
return {
users: {
relation: Model.HasManyRelation,
modelClass: User,
join: {
from: `${tableNames.state}.id`,
to: `${tableNames.user}.state_id`,
},
},
}
Migration:
await knex.schema.createTable(tableNames.state, (table) => {
table.increments().primary().notNullable()
table.string('name', 100).notNullable()
User table model:
static get jsonSchema() {
return {
type: 'object',
properties: {
id: { type: 'integer' },
first_name: { type: 'string', minLength: 1, maxLength: 100 },
last_name: { type: 'string', minLength: 1, maxLength: 100 },
state_id: { type: 'integer', default: null },
},
}
}
static get relationMappings() {
return {
state: {
relation: Model.BelongsToOneRelation,
modelClass: State,
join: {
from: `${tableNames.user}.state_id`,
to: `${tableNames.state}.id`,
},
}
}
}
User table migration:
await knex.schema
.createTable(tableNames.user, (table) => {
table.increments().primary().notNullable()
table.string('first_name', 100).notNullable()
table.string('last_name', 100).notNullable()
table.integer('state_id').unsigned()
table
.foreign('state_id')
.references('id')
.inTable(tableNames.state)
.onDelete('SET NULL')
})
Now the issue: I want the state_id column to be nullable, as in not every user will have a state assigned to them. But when I try inserting a user with no state_id, I get this: insert or update on table \"user\" violates foreign key constraint \"user_state_id_foreign\".
two things you are doing wrong
in your json schema define your column as state_id: {type: ['integer', 'null']}
in your user migrations make table.integer('state_id').unsigned().nullable()
I've been trying to get it so I can put multiple "trainings" within the training type if since they would merge into each other once the user has both. However I can't seem to get it working and I'm stumped as to how to go about it.
Here is my trainingmodel:
const mongoose = require('mongoose')
const Schema = mongoose.Schema
let trainingSchema = new Schema({
name: {
type: String,
required: true,
unique: true
},
shortHand: {
type: String,
required: true,
unqiue: true
},
desc: { type: String },
office: {
type: Schema.Types.ObjectId,
ref: "Office"
},
isMerge: { type: Boolean, default: false},
mergeInto: [{
type: Schema.Types.ObjectId,
ref: "Training"
}]
})
module.exports = mongoose.model('Training', trainingSchema)
and here is my training object
/**
* Defines Training Type
*/
const TrainingType = new GraphQLObjectType({
name: "Training",
fields: {
id: { type: GraphQLID },
name: { type: GraphQLString },
shortHand: { type: GraphQLString },
desc: { type: GraphQLString },
office: {
type: require('./office').OfficeType,
resolve: async (office) => {
return await Office.findById(office.office)
}
},
isMerge: { type: GraphQLBoolean },
mergeInto: {
type: new GraphQLList(TrainingType), // This is the error line
resolve: async (training) => {
return await Training.find({id: training.id})
}
}
}
})
module.exports.TrainingType = TrainingType
Now obviously the error I get back is TrainingType is not defined, since I'm trying use something that hasn't fully been defined yet. But I've tried other ways like making a different objectType called MergesInto and then use it in the other. But that doesn't work either as one requires the other and one has to be defined before the other which will give me the error Is Not Defined. I can't seem to figure out how to get it working. Is this even possible?
fields can be either an object or a function that returns one. Making it a function will delay the execution of the code inside the function, so you can reference the TrainingType variable without an error.
const TrainingType = new GraphQLObjectType({
name: "Training",
fields: () => ({
...
mergeInto: {
type: new GraphQLList(TrainingType),
...
},
})
})
I'm combining a GraphQL app with my existing Firebase project and am having a lot of problems getting the queries to correctly get data from the firestore().
So far I have the mutations working correctly, but when I go to query the data I can't get the firestore().get() snapshot into a form that graphQL will recognize.
so far it looks like this:
const {GraphQLObjectType,
GraphQLString,
GraphQLBoolean,
GraphQLFloat,
GraphQLSchema,
GraphQLID,
GraphQLList,
GraphQLNonNull} = require("graphql");
const admin = require('firebase-admin');
const functions = require('firebase-functions');
admin.initializeApp(functions.config().firebase);
//Models
const Room = admin.firestore().collection('room');
const Position = admin.firestore().collection('position');
const Plant = admin.firestore().collection('plant');
const PlantInfo = admin.firestore().collection('plantInfo');
const RoomType = new GraphQLObjectType({
name: "Room",
fields: () => ({
id: { type: GraphQLID },
name: { type: GraphQLString },
description: { type: GraphQLString },
floor: { type: GraphQLString },
building: { type: GraphQLString },
positions: {
type: new GraphQLList(PositionType),
resolve(parent, arg) {
//return _.filter(positions, {inRoomId:parent.id})
return Position.orderByChild('inRoomId').equalTo(parent.id);
}
}
})
});
const PositionType = new GraphQLObjectType({
name: "Position",
fields: () => ({
id: { type: GraphQLID },
name: { type: GraphQLString },
description: { type: GraphQLString },
exposure: { type: GraphQLString },
size: { type: GraphQLString },
inRoom: {
type: RoomType,
resolve(parent, args) {
//return _.find(rooms, {id:parent.inRoomId})
return Room.child(parent.inRoomId);
}
}
})
});
const RootQuery = new GraphQLObjectType({
name: "RootQueryType",
fields: {
room: {
type: RoomType,
args: { id: { type: GraphQLID } },
resolve(parent, args) {
//code to get data from db/othersourse
//return _.find(rooms, {id: args.id});
return Room.child(args.id);
}
},
position: {
type: PositionType,
args: { id: { type: GraphQLID } },
resolve(parent, args) {
//code to get data from db/othersourse
//return _.find(positions, {id: args.id})
return Position.child(args.id);
}
},
rooms: {
type: new GraphQLList(RoomType),
resolve(parent, args) {
//return rooms
return Room.get().then(snapshot => {snapshot.forEach(doc => {return doc})})
}
},
positions: {
type: new GraphQLList(PositionType),
resolve(parent, args) {
//return positions
return Position.get().then(doc => console.log(doc)).catch(err => console.log('Error getting document', err));
}
}
}
});
const Mutation = new GraphQLObjectType({
name: "Mutation",
fields: {
addRoom: {
type: RoomType,
args: {
name: { type: new GraphQLNonNull(GraphQLString) },
floor: { type: new GraphQLNonNull(GraphQLString) },
building: { type: new GraphQLNonNull(GraphQLString) }
},
resolve(parent, args) {
let room = {
name: args.name,
floor: args.floor,
building: args.building
};
return Room.add(room);
}
},
addPosition: {
type: PositionType,
args: {
name: { type: new GraphQLNonNull(GraphQLString) },
exposure: { type: new GraphQLNonNull(GraphQLString) },
size: { type: new GraphQLNonNull(GraphQLString) },
inRoomId: { type: new GraphQLNonNull(GraphQLString) }
},
resolve(parent, args) {
let position = {
name: args.name,
exposure: args.exposure,
size: args.size,
inRoomId: args.inRoomId
};
return Position.add(position);
}
}
}
});
module.exports = new GraphQLSchema({
query: RootQuery,
mutation: Mutation
});
Under the RootQuery -> Rooms I'm trying to get a graphQL query to return all the rooms in my 'room' collection. I have been able to get it to console.log() a list of documents using:
return Room.get()
.then(snapshot => {
snapshot.forEach(doc => {
console.log(doc.id, " => ", doc.data());
But getting this into an array has so far eluded me. Any help is really appreciated.
Seeing as no one was able to answer this, I ended up figuring it out for myself :p
So resolve functions relating to getting a collection of related data for example positions. the following works:
first you need a function to convert the snapshots into an array as this is what graphQL is expecting. This also allows your to seperate the id and add it in with the array item:
const snapshotToArray = (snapshot) => {
var returnArr = [];
snapshot.forEach((childSnapshot)=> {
var item = childSnapshot.data();
item.id = childSnapshot.id;
returnArr.push(item);
});
return returnArr;
};
Next when getting the data you use .get() which returns a promise (and error) which can be passed into the snapshotToArray().
return Position.get().then((snapshot) => {
return snapshotToArray(snapshot);
})
For resolve functions that only call on one dataset for example inRoom. Its similar to the first one except using .where() and seperating the id and data() in the snapshot functions:
return Room.doc(parent.inRoomId).get().then((snapshot) => {
var item = snapshot.data();
item.id = snapshot.id;
return item;
})
Just incase someone else runs into the same problem :)
I think I am missing something in the docs but I am not sure how to handle objects as the type withing a new GraphQLObjectType. I am looking to set up the queries for weather data from this sample data, and I am not sure how to handle the nested objects. I currently have:
// Creating a Type for the Weather Object
const WeatherType = new GraphQLObjectType({
name: 'Weather',
fields: () => ({
weather: { type: GraphQLObject? },
})
});
I am looking to get specific with the queries and set up the structure to specify more select data like:
// Creating a Type for the Weather Object
const WeatherType = new GraphQLObjectType({
name: 'Weather',
fields: () => ({
weather: {
main: { type: GraphQLString },
// And so on
},
})
});
Are there any references to examples of this?
When constructing a schema with nested custom types, you just set the type of the field to a reference of your other created type:
const WeatherType = new GraphQLObjectType({
name: 'Weather',
fields: {
id: {
type: GraphQLInt,
}
main: {
type: GraphQLString,
}
description: {
type: GraphQLString,
}
icon: {
type: GraphQLString,
}
}
})
const MainType = new GraphQLObjectType({
name: 'Main',
fields: {
temp: {
type: GraphQLFloat,
}
pressure: {
type: GraphQLFloat,
}
humidity: {
type: GraphQLFloat,
}
tempMin: {
type: GraphQLFloat,
resolve: (obj) => obj.temp_min
}
tempMax: {
type: GraphQLFloat,
resolve: (obj) => obj.temp_max
}
}
})
const WeatherSummaryType = new GraphQLObjectType({
name: 'WeatherSummary',
fields: {
weather: {
type: new GraphQLList(WeatherType),
}
main: {
type: MainType,
}
}
})
Be careful when molding existing JSON responses into GraphQL schemas -- it's easy to get burned by differences in structure. For example, the main field in your sample response is an object, but the weather field is actually an array, so we have to wrap it in GraphQLList when specifying the type for the field.
I want to use mutation in Relay to change an array (not connection). The array is typed GraphQLList in the GraphQL side. The graphql side worked perfectly, but relay side needs dataID for each item in an array. And when I am inserting new item or modifying existing item in the array, there are no dataID provided? What is the right way to do this? By the way, I am using redux to maintain the list, and submit changes via relay at the end.
The schema:
let widgetType = new GraphQLInputObjectType({
name: 'Widget',
fields: () => ({
label: {
type: GraphQLString
},
type: {
type: GraphQLString
},
list: {
type: new GraphQLList(GraphQLString)
},
description: {
type: GraphQLString
},
required: {
type: GraphQLBoolean
}
})
});
let modifyFormMutation = mutationWithClientMutationId({
name: 'ModifyForm',
inputFields: {
id: {
type: new GraphQLNonNull(GraphQLString)
},
name: {
type: new GraphQLNonNull(GraphQLString)
},
userId: {
type: new GraphQLNonNull(GraphQLString)
},
widgets: {
type: new GraphQLList(widgetType)
}
},
outputFields: {
formEdge: {
type: formConnection.edgeType,
resolve: (obj) => {
return {
node: {
id: obj.id,
name: obj.name,
userId: obj.userId,
widgets: obj.widgets
},
cursor: obj.id
};
}
},
app: {
type: appType,
resolve: () => app
}
},
mutateAndGetPayload: ({
id, name, userId, widgets
}) => {
db.collection('forms').findOneAndUpdate({
_id: new ObjectID(id)
}, {
name, userId, widgets, createAt: Date.now()
});
return {
id, name, userId, widgets
};
}
})
Relay mutation:
export default class ModifyFormMutation extends Mutation {
getMutation () {
return Relay.QL`mutation{modifyForm}`;
}
getFatQuery() {
return Relay.QL`
fragment on ModifyFormPayload {
formEdge
app { forms }
}
`;
}
getCollisionKey() {
return `check_${this.props.app.id}`;
}
getConfigs() {
return [{
type: 'FIELDS_CHANGE',
fieldIDs: {
formEdge: {node: this.props.node},
app: this.props.app.id
}
}];
}
getVariables() {
return {
name: this.props.node.name,
id: this.props.node.id,
userId: this.props.node.userId,
widgets: this.props.node.widgets
};
}
getOptimisticResponse() {
return {
formEdge: {
name: this.props.node.name,
id: this.props.node.id,
userId: this.props.node.userId,
widgets: this.props.node.widgets
}
};
}
}
And error message from browser:
"Variable "$input_0" got invalid value
{"name":"asdfasdfsa","id":"57e790cec252f32aa805e38d","userId":"57e10a02da7e1116c0906e40","widgets":[{"dataID":"client:618507132","label":"sdfas","type":"text","list":[],"description":"","required":true},{"label":"sfasdfasaaa","list":[],"type":"number","description":"","required":"false"}],"clientMutationId":"0"}.↵In
field "widgets": In element #0: In field "dataID": Unknown field."