I am developing an application that has a quite sizeable amount of Queries and Mutation. Structures for data are often not complex, but there is plenty of them, so I have made myself a snippet, that generates the most common things repeating throughout them. This snippet also generates an input for mutations so it can be used for both simple and complex data structures. In quite a bit of instances, the input is just for adding a name. The API is supposed to be used mainly by my fronted, but after the app gets mature enough should be publicly available. Is doing this a problem in terms on conventions?
Sample of what I mean
/*=============================================
Types
=============================================*/
interface AddSampleSchemaInput {
input: AddSampleSchema
}
interface AddSampleSchema {
name: string
}
/*=============================================
Main
=============================================*/
export const SampleSchemaModule = {
typeDefs: gql`
type Mutation {
addSampleSchema(input: AddSampleSchemaInput): SampleSchema!
}
type SampleSchema {
_id: ID!
name: String!
}
input AddSampleSchemaInput {
name: String!
}
`
,
resolvers: {
Mutation: {
addSampleSchema: async (parents: any, args: AddSampleSchemaInput, context: GraphqlContext) => {
}
}
}
}
Sample of what I assume it should be.
/*=============================================
Main
=============================================*/
export const SampleSchemaModule = {
typeDefs: gql`
type Mutation {
addSampleSchema(name: String): SampleSchema!
}
type SampleSchema {
_id: ID!
name: String!
}
`
,
resolvers: {
Mutation: {
addSampleSchema: async (parents: any, args: { name: string }, context: GraphqlContext) => {
}
}
}
}
export default SampleSchemaModule
Would usage of the first code example be a problem. This means using input (input AddSampleSchemaInput), even if it were to contain just a single value (in this case name).
Or in other words is using input for every mutation a problem no matter the complexity.
Or the impact on frontent:
addDogBreed({
variables: {
input: {
name: "Retriever",
avergeHeight: 0.65
}
}
})
addDog({
variables: {
input: {
name: "Charlie"
}
}
})
// ======= VS =======
addDogBreed({
variables: {
input: {
name: "Retriever",
avergeHeight: 0.65
}
}
})
addDog({
variables: {
name: "Charlie"
}
})
In this case, is having the first one instead of the second one a problem?
Is having an input that only contains one key is something problematic?
No, on the contrary, it is something desirable in GraphQL. While nesting may sometimes seem superfluous, it is key in forward compatibility and extensibility of your schema. You should not have different conventions of how to design your mutation arguments depending on the number of inputs. If you always use an input object, you can easily deprecate existing fields or add new optional fields and stay compatible with all existing clients. If you were to completely change the shape of the mutation arguments just because you have an object with a single key, it would break compatibility.
I'm not seeing a problem that would drive you to
"only use GraphQL when dealing with Fetching / Get Data, and normal
REST API Request for mutating data (create, update, delete)."
Like #Bergi said. Plus you can provide your entity with multiple mutators some which can work like a PATCH or a PUT request.
Related
How can I group my queries into namespaces in GraphQL? I have something like this right now:
const queryType = new g.GraphQLObjectType({
name: "Query",
fields: fields,
});
and in fields I have field -> object mappings and it works fine, but I'd like to group these mappings into two groups (live and historical). If I modify the above code to this however:
const queryType = new g.GraphQLObjectType({
name: "Query",
fields: {
historical: {
type: new g.GraphQLObjectType({
name: "historical",
fields: fields,
})
}
},
});
everything resolves to null. How can I write a resolver for this grouping? Is it possible at all?
so often people want namespaces for the sake of splitting up code, not sure if this is your end goal but you could achieve that this way aswell:
# in one file
type Mutation {
login(username: String, password: String): User
}
# in other file
extend type Mutation {
postX(title: String, message: String): X
}
I have a scheduled function that resets an integer value back to zero in my firestore. The problem that I'm running into is that, while the merge-set succeeds (for the specified properties), it somehow resets my Organization document reference to null.
So far I've tried the following
Not using a converter along with the Update() function (instead of Set()). While this works, it is untyped, and I have to get rid of the converter which encapsulates the moment() to Date conversion.
Using Set() and simply pass the entire object.
user.reference?.withConverter(userConverter).set(user)
This is also working but it overrides the entire user object and can lead to concurrency issues in case a user also updates his object while the timed function is running.
I'm looking for a solution that allows me to use the converter class along with a merge Set().
The User interface looks like this
export interface User extends Document {
email?: string
name?: string
organization?: Organization | null
numberOfForwards?: number
lastForwardReset?: moment.Moment
}
with its converter like so
export class UserConverter implements firestore.FirestoreDataConverter<User> {
toFirestore(user: User): firestore.DocumentData {
return {
email: user.email,
name: user.name,
organization: user.organization ? user.organization.reference : null,
number_of_forwards: user.numberOfForwards,
last_forward_reset: user.lastForwardReset?.toDate()
}
}
fromFirestore(snapshot: firestore.QueryDocumentSnapshot): User {
const data = snapshot.data()!
return {
reference: snapshot.ref,
email: data.email,
name: data.name,
organization: data.organization ? { reference: data.organization } : null,
numberOfForwards: data.number_of_forwards,
lastForwardReset: moment(data.last_forward_reset.toDate())
}
}
}
export const resetNumberOfForwards = functions.pubsub
.schedule('every 15 minutes')
.onRun(async () => {
const reset = (user: User) => {
console.log(`Resetting ${user.email} from [${user.numberOfForwards}] to [0]`)
// Claim user reference
user.reference
?.withConverter(userConverter)
.set({ numberOfForwards: 0, lastForwardReset: Moment() }, { merge: true })
}
for the partial set to work, I've included the following snippet on top of my file
firebase.firestore().settings({
ignoreUndefinedProperties: true
})
I think there are two issues going on here. For a partial set() you should use the merge option or else it will overwrite the document.
ref.set(data, {merge: true})
In addition, in your toFirestore method, either set the organization field as undefined and let the ignoreUndefinedProperties: true setting remove it, or don't include it at all if organization was not given. Something like this
toFirestore((numberOfForwards, lastForwardReset, ...user): User): firestore.DocumentData {
if (user.organization) {
user.organization = user.organization.reference;
}
return {
...user,
number_of_forwards: numberOfForwards,
last_forward_reset: lastForwardReset?.toDate()
}
}
I took out the numberOfForwards and lastForwardReset fields from the user object here and use the spread operator to copy over the remaining fields to the return value, but you could also save a temporary object, modify it, and return that.
PS: I know this is old, but it came up in my search so thought I might add an answer still.
The title may be miss leading but I'm not really sure how do I ask this question correctly. Here is the problem: I'd like to query my own API(not created yet so I made placeholder data) for global settings which might change in the future and I will only need to rebuild the website instead of editing it manually, I want to create source node called CmsSettings and pass it to GraphQL (structure similar to site.siteMetadata) but I don't know how can I achieve that. What I achieved so far is to create a source node called allCmsSettings which has my data as an object in nodes array.
exports.sourceNodes = ({ actions, createNodeId, createContentDigest }) => {
const { createNode } = actions;
const myData = {
key: 123,
app_title: `The foo field of my node`,
...
}
const nodeContent = JSON.stringify(myData);
const nodeMeta = {
id: createNodeId(`my-data${ myData.key }`),
parent: null,
children: [],
internal: {
type: `CmsSettings`,
mediaType: `text/html`,
content: nodeContent,
contentDigest: createContentDigest(myData)
}
}
const node = Object.assign({}, myData, nodeMeta);
createNode(node);
}
Here is the query used to get the data of the source node
allCmsSettings {
edges {
node {
id
app_title
...
}
}
}
Creating a query results in an array of results(which I know is the result of creating source nodes) but I'd like to create that source so that I could query it like this and:
CmsSettings {
app_title
app_keywords
app_descriptions
app_logo_path
brand_name
...
}
You get the point. I was browsing the gatsby node API but I can't find how to achieve this.
Thank you for your help
Nevermind, the answer is pretty simple, if you are new to gatsby just like me the sourceNodes export creates 2 graphql fields for you with all prefix and camel case source node. The thing that I wanted to make is already there and is queryable with
cmsSettings {
app_title
app_keywords
app_descriptions
app_logo_path
brand_name
...
}
Notice the lowercase letter even though it was declared as CmsSettings. It seems that gatsby really does some magic under the hood.
I am attempting to create Gatsby pages programmatically using the Gatsby API createPages and data from Firebase. I've set up everything successfully up to the point where Firebase data is accessible via GraphQL and now I want to query specifict data for each of the new pages that were created using id (which are in string format). However, when I create the template component and try to query the data i get this error:
Variable "$clientId" of type "String!" used in position expecting type "StringQueryOperatorInput".
I have looked everywhere for a reference of this StringQueryOperatorInput and can't find any info on it. Google and graphql docs don't seem to mention the term and this is my first time seeing it. After troubleshooting for an hour I got a different error:
If you're e.g. filtering for specific nodes make sure that you choose the correct field (that has the same type "String!") or adjust the context variable to the type "StringQueryOperatorInput".
File: src/templates/Homeowner/Homeowner.js:24:9
However, I still don't know what a StringQueryOperatorInput is or how to fix this.
Below is my code for this component and my gatsby-node.js, and my gatsby-config.js where i use a plugin to source the Firebase data.
I could really use some help on this, I can't seem to find any reference of this StringQueryOperatorInput.
Everything else works fine, I just can't get this query on the Homeowner.js template to work.
gatsby-node.js
exports.createPages = async ({ graphql, actions }) => {
const { createPage } = actions;
const result = await graphql(`
query {
allClients {
nodes {
firstName
lastName
id
}
}
}
`);
console.log(JSON.stringify(result, null, 4));
result.data.allClients.nodes.forEach(node => {
const slug = `/client/${node.id}`;
createPage({
path: slug,
component: require.resolve(`./src/templates/Homeowner/Homeowner.js`),
context: { clientId: node.id },
});
});
};
src/templates/Homeowner/Homeowner.js
import React from 'react';
import { graphql } from 'gatsby';
import { withFirebase } from '../../components/Firebase';
import { withStyles } from '#material-ui/core/styles';
import Layout from '../../components/layout';
const Homeowner = ({ data }) => {
console.log(data.clients, 'data');
return (
<>
<Layout>
<h1>Home Owner Component</h1>
{/* <h3>{client.firstName}</h3>
<h3>{client.lastName}</h3>
<h3>{client.email}</h3> */}
</Layout>
</>
);
};
export default Homeowner;
export const query = graphql`
query($clientId: String!) {
clients(id: $clientId) {
firstName
lastName
email
}
}
`;
gatsby-config.js
require('dotenv').config({
path: `.env.${process.env.NODE_ENV}`,
});
module.exports = {
siteMetadata: {
title: `SiteTitle`,
siteUrl: `https://www.mysitwe.com`,
description: `YourSite`,
},
plugins: [
`gatsby-plugin-react-helmet`,
`gatsby-plugin-sitemap`,
`gatsby-plugin-styled-components`,
`gatsby-plugin-sharp`,
`gatsby-transformer-sharp`,
{
resolve: `gatsby-source-firebase`,
options: {
credential: require('./firebase-key.json'),
databaseURL: 'https://firebaseurl/',
types: [
{
type: 'Clients',
path: 'clients',
},
{
type: 'Users',
path: 'users',
},
],
},
},
{
resolve: `gatsby-plugin-prefetch-google-fonts`,
options: {
fonts: [
{
family: `Nunito Sans`,
variants: [`400`, `600`, `800`],
},
{
family: `Montserrat`,
variants: [`300`, `400`, `400i`, `500`, `600`],
},
{
family: `Spectral`,
variants: [`400`, `600`, `800`],
},
{
family: `Karla`,
variants: [`400`, `700`],
},
],
},
},
{
resolve: `gatsby-source-filesystem`,
options: {
name: `images`,
path: `${__dirname}/src/images`,
},
},
`gatsby-plugin-offline`,
],
};
THank you in advance if anyone can help me out.
Actually literally right after I posted this question I found the solution. I needed to set up my query like so:
export const query = graphql`
query($clientId: String!) {
clients(id: { eq: $clientId }) {
firstName
lastName
email
}
}
`;
I assume that leaving out the {eq: $clientId} throws that StringQuery error on the GraphQL side. I still do not know what a StringQueryOperatorInput is, however, I have successfully generated the pages with the data from firebase.
StringQueryOperatorInput is the type of the id argument of the clients field. GraphQL includes scalar types like String, ID or Int as well as types that describe more complex data structures like arrays or objects. In this case, StringQueryOperatorInput is an input object type -- it describes objects that can be used as inputs like argument values or variables.
When filtering fields, Gatsby uses an input object like this to enable using a variety of comparison operators to filter the exposed data -- in addition to eq (equals), you can use other operators like ne, regex, in, gt, etc. You can see the full list here. Because not all inputs apply to all scalars (regex makes sense for a String field but lte does not), there's a different input type for each scalar (IntQueryOperatorInput, BooleanQueryOperatorInput, etc.)
Gatsby exposes a GraphiQL endpoint in development. Writing queries using GraphiQL allows you to utilize autocomplete and syntax highlighting so that you can avoid unexpected syntax errors like this. You can also use the "Docs" button to search and browse the entire schema.
I got the following "problem". I am used to having an API like that.
/users
/users/{id}
The first one returns a list of users. The second just a single object. I would like the same with GraphQL but seem to fail. I got the following Schema
var schema = new GraphQLSchema({
query: new GraphQLObjectType({
name: 'Query',
fields: {
users: {
type: new GraphQLList(userType),
args: {
id: {type: GraphQLString}
},
resolve: function (_, args) {
if (args.id) {
return UserService.findOne(args.id).then(user => [user]);
} else {
return UserService.find()
}
}
}
}
})
});
How can I modify the type of users to either return a List OR a single object?
You shouldn't use one field for different purposes. Instead of that, make two fields. One for single object and another for list of objects. It's better practice and better for testing
fields: {
user: {
type: userType,
description: 'Returns a single user',
args: {
id: {type: GraphQLString}
},
resolve: function (_, args) {
return UserService.findOne(args.id);
}
},
users: {
type: new GraphQLList(userType),
description: 'Returns a list of users',
resolve: function () {
return UserService.find()
}
}
}
The above answer is correct, the usual approach is to add singular and plural form of queries. However, in large schema, this can duplicate a lot of logic and can be abstracted a little bit for example with Node interface and node, nodes queries. But the nodes query is usually applied with ids as argument (in Relay viz node Fields), but you can build your own abstracted way for fetching so that you have just nodes with some argument for type and based on that you can say what type of list to fetch. However, the simpler approach is to just duplicate the logic for every type and use singular and plural form of query and do the same type of queries as above or in this code snippet for every type. For more detail explanation on implementing GraphQL list modifiers in queries or even as an input for mutations. I just published the article on that.