My GraphQL query looks like this:
{
p1: property(someArgs: "some_value") {
id
nestedField {
id
moreNestedField {
id
}
}
}
}
On the server side, I'm using Apollo Server.
I have a resolver for the property and other resolvers for nestedField and moreNestedField.
I need to retrieve the value of someArgs on my nested resolvers.
I tried to do this using the context available on the resolver:
property: (_, {someArgs}, ctx) => {
ctx.someArgs = someArgs;
// Do something
}
But this won't work as the context is shared among all resolvers, thus if I have multiple propertyon my query, the context value won't be good.
I also tried to use the path available on info on my nested resolvers. I'm able to go up to the property field but I don't have the arguments here...
I also tried to add some data on info but it's not shared on nested resolvers.
Adding arguments on all resolvers is not an option as it would make query very bloated and cumbersome to write, I don't want that.
Any thoughts?
Thanks!
Params can be passed down to child resolvers using the currently returned value. Additional data will be removed from the response later.
I'll 'borrow' Daniel's code, but without specific params - pass args down as reference (suitable/cleaner/more readable for more args):
function propertyResolver (parent, args) {
const property = await getProperty()
property.propertyArgs = args
return property
}
// if this level args required in deeper resolvers
function nestedPropertyResolver (parent, args) {
const nestedProperty = await getNestedProperty()
nestedProperty.propertyArgs = parent.propertyArgs
nestedProperty.nestedPropertyArgs = args
return nestedProperty
}
function moreNestedPropertyResolver (parent) {
// do something with parent.propertyArgs.someArgs
}
As Daniels stated this method has limited functionality. You can chain results and make something conditionally in child resolver. You'll have parent and filtered children ... not filtered parent using child condition (like in SQL ... WHERE ... AND ... AND ... on joined tables), this can be done in parent resolver.
Do not pass your argument through root, except IDs or parent object, anything from client, use field level argument.
Please check this answer here on how to pass the arguments:
https://stackoverflow.com/a/63300135/11497165
To simplify it, you can put args in your field:
Example Type Definition
Server defination:
type Query{
getCar(color: String): Car
... other queries
}
type Car{
door(color: String): Door // <-- added args
id: ID
previousOwner(offset: Int, limit: Int): Owner // <-- added args
...
}
client query:
query getCar(carId:'123'){
door(color:'grey') // <-- add variable
id
previousOwner(offset: 3) // <-- added variable
... other queries
}
You should be able to access color in your child resolver arguments:
In your resolver:
Car{
door(root,args,context){
const color = args.color // <-- access your arguments here
}
previousOwner(root,args,context){
const offset = args.offset // <-- access your arguments here
const limit = args.limit // <-- access your arguments here
}
...others
}
For your example:
it will be like this
{
p1: property(someArgs: "some_value") { // <-- added variable
id
nestedField(someArgs: "some_value") { // <-- added variable
id
moreNestedField(offset: 5) {
id
}
}
}
}
You can pass the value through the parent field like this:
function propertyResolver (parent, { someArgs }) {
const property = await getProperty()
property.someArgs = someArgs
return property
}
function nestedPropertyResolver ({ someArgs }) {
const nestedProperty = await getNestedProperty()
nestedProperty.someArgs = someArgs
return nestedProperty
}
function moreNestedPropertyResolver ({ someArgs }) {
// do something with someArgs
}
Note that while this works, it may also point to an underlying issue with your schema design in the first place. Depending on how you're resolving these fields (getting them from a database, making requests to another API, etc.), it may be preferable to take a different approach altogether -- for example, by eager loading everything inside the root resolver. Without more context, though, it's hard to make any additional recommendations.
Related
Suppose the following scenario:
We have a class that handles a Mongoose connection as below:
export interface IInstance {
name: string;
instance: Mongoose.Mongoose;
}
export default class MongoHandler {
public static instances: IInstance[] = [{
name: 'default',
instance: null,
}];
// Connect to mongo instance and add it to the instances array
public static async connect(name: string, uri: string, options?: object): Promise<void> {
const instance: Mongoose.Mongoose = await Mongoose.connect(uri, options);
const newInstance: IInstance = {
name,
instance,
};
MongoHandler.instances.push(newInstance);
}
// Returns the instance based on the name of instance
public static getInstance(name: string = 'default'): Mongoose.Mongoose {
return this.instances.find(instance => instance.name === name).instance;
}
}
The other module called CarModel is using getInstance() method for creating a model:
export interface ICar {
name: string;
}
const carSchema = new Mongoose.Schema<ICar>(
{
name: {
type: String,
required: true,
},
}
);
const carModel = MongoHandler.getInstance('default').model<ICar>('Car', carSchema, 'Cars');
export default carModel;
We are using carModel in a module called CarController.
In index.ts we are calling these two modules as below:
import
const app = new App(
[
MongoHandler.connect('default', process.env.MONGO_URI),
],
[
new CarController(),
]
);
App is a class for handling express bootstrapping (can be ignored).
While running this code MongoHandler.getInstance('default') is undefined because of the order of dependency resolution (I think)! And resolving MongoHandler.getInstance('default') is followed by MongoHandler.connect() which should be reversed.
How can I solve this?
Best regards
I think there are 2 issues at play here, neither of them having to do with module resolution (which will work just fine as you don't have any circular dependencies).
array.prototype.find returns the FIRST match found, you instantiate instances array with an object that matches the name default, but has no instance. When you connect, you add another object with the name default, but this will be second in the list, thus find will return the original object, which has its instance object set as null.
I would advice removing this default empty instance object for error and code clarity.
your connect function makes use of async-await. But you never await your connect function, thus not guaranteeing that your new connection instance has been made before you are calling getInstance inside of your carController. You should catch the Promise returned by the connect function and await it. If you do not want to delay your CarController instantiation, you can use save this Promise in the MongoHandler and return it with some init function that you call inside of the CarController to make sure the connect has been resolved.
Let me explain:
type SecondType {
id: String
}
type FirstType {
id: String
secondTypes: [SecondType]
}
type Query {
firstTypes: [FirstType]
secondTypes: [SecondType]
}
//
const resolver = {
Query: {
firstTypes: fetchFirstTypes,
secondTypes: fetchSecondTypes
}
};
The idea is that if there's a parent-child relationship between firstType and secondType, for example secondType contains it's parent (firstType) id, through the result of the previous resolver, secondTypes inside query firstTypes can fetch secondTypes related to it's parent firstType. What I've experienced is that the resolver of firstType is looking for a key named secondTypes from the result of fetchFirstTypes. But I want to let GraphQL know that it needs to resolve secondTypes from the resolver of Query.secondTypes. How can I do that? Maybe is there a way to write "fallback reducers" of a field? So if it doesn't find a key from the result, it can look for a resolver?
Based on my understanding of your question, you need a resolver for the key secondTypes which is present in FirstType. For this,
you can make the resolver as follows:
const resolver = {
Query: {
firstTypes: fetchFirstTypes,
secondTypes: fetchSecondTypes
},
FirstType: {
secondTypes: (parent, args, context, info) => {
// Here you will get the parent id(FirstType) parent.id
// From Here resolve the secondtypes data
}
}
};
I'm using adonisjs, and am trying to sanitize a post request on the server. The data I get back has extra properties that are not mapped to the table/model so it is erroring when I try to save. Here is the update method
async update ({ params, request, response }) {
const contract = await Contract.find(params.id);
contract.merge(request.post());
return await contract.save();
}
The problem is that when I returned the contract earlier on a get request, I added some computed properties. I could do something along the lines of
const { prop1, prop2 } = request.post();
but that doesn't feel like a future proof or clean solution. Ideally I want the object to only have the properties defined on the table/model. I have a validator setup as described in the validator docs, but it still lets other properties bypass it.
I resolved this by adding a beforeSave hook in the model class that filter's properties on the object, which allows us to keep a thin controller.
const FIELDS = ['id', 'description'];
class Contract extends Model {
static boot() {
super.boot();
this.addHook('beforeSave', async contractInstance => {
Object.keys(contractInstance.$attributes).forEach(key => {
if (!CONTRACT_FIELDS.includes(key)) {
delete contractInstance.$attribute[key];
}
});
});
}
}
What about an helper class?
class RequestContractExtractor{
static desiredProps = [
"prop1",
"prop2",
]; // you could replace this with a list you get from your model class
constructor(requestData){
desiredProps.forEach(prop => this[prop] = requestData[prop]);
}
static from(...args){ return new this(...args); }
}
Then you'd be able to do:
async update ({ params, request, response }) {
const contract = await Contract.find(params.id);
contract.merge(RequestContractExtractor.from(request.post()));
return await contract.save();
}
I resolved this by adding a beforeSave hook that filter's properties on the object.
Create a file at /app/Model/Hooks/ContractHook.js
'use strict'
const ContractHook = module.exports = {}
const CONTRACT_FIELDS = ["id", "description"];
ContractHook.removeDynamicFields = async (contractInstance) => {
if (contractInstance) {
Object.keys(contractInstance.$attributes).forEach((key) => {
if (!CONTRACT_FIELDS.includes(key) && contractInstance.$attributes[key]) {
delete contractInstance.$attributes[key];
}
});
}
};
Use it in your model like so...
class Contract extends Model {
static boot() {
super.boot();
this.addHook("beforeCreate", [
"ContractHook.removeDynamicFields",
]);
}
I am looking for a way to modify the response object of a graphql query or mutation before it gets sent out.
Basically in addition the the data object, I want to have extra fields like code and message.
At the moment I am solving this by adding the fields directly into my GQL schemas take this type definition for example:
type Query {
myItems: myItemResponse
}
type myItemResponse {
myItem: Item
code: String!
success: Boolean!
message: String!
}
The response itself would be look like that:
{
data: {
myItems: {
myItem: [ ... fancy Items ... ],
message: 'successfully retrieved fancy Items',
code: <CODE_FOR_SUCCESSFUL_QUERY>
}
}
}
I find that solution not nice because it overcomplicates things in my FrontEnd.
I would prefer a solution where message code and other Metadata are seperated from the actual data, so something like this:
{
data: {
myItems: [ ... fancy Items ... ],
},
message: 'successfully retrieved fancy Items',
code: <CODE_FOR_SUCCESSFUL_QUERY>
}
With apollo-server I already tried the formatResponse object in the constructor:
const server = new ApolloServer({
...
formatResponse({ data }) {
return {
data,
test: 'Property to test if shown in the FrontEnd',
}
}
...
}
unfortunately that doesn't have the desired effect. Before I use express middlewares I want to ask if there is a possibility to do this via apollo-server out of the box or if I am maybe just missing something in the formatResponse function.
from graphql.org:
A response to a GraphQL operation must be a map.
If the operation encountered any errors, the response map must contain an entry with key errors. The value of this entry is described in the “Errors” section. If the operation completed without encountering any errors, this entry must not be present.
If the operation included execution, the response map must contain an entry with key data. The value of this entry is described in the “Data” section. If the operation failed before execution, due to a syntax error, missing information, or validation error, this entry must not be present.
The response map may also contain an entry with key extensions. This entry, if set, must have a map as its value. This entry is reserved for implementors to extend the protocol however they see fit, and hence there are no additional restrictions on its contents.
To ensure future changes to the protocol do not break existing servers and clients, the top level response map must not contain any entries other than the three described above.
After doing a lot of research I found out that the only allowed top level properties in a graphql responses are data, errors, extensions. Here you can find the regarding Issue in GitHub
GitHub Issue
for my purpose I will probably use the extensions field.
Example data modifier
This function will concat ":OK" suffix on each string in the output object
// Data/output modifier - concat ":OK" after each string
function outputModifier(input: any): any {
const inputType = typeof input;
if (inputType === 'string') {
return input + ':OK';
} else if (Array.isArray(input)) {
const inputLength = input.length;
for (let i = 0; i < inputLength; i += 1) {
input[i] = outputModifier(input[i]);
}
} else if (inputType === 'object') {
for (const key in input) {
if (input.hasOwnProperty(key)) {
input[key] = outputModifier(input[key]);
}
}
}
return input;
}
Solution 1 - Override GraphQL Resolvers
Long story short: you have 3 main types (Query, Mutation, and Subscription).
Each main type has fields with resolvers.
The resolvers are returning the output data.
So if you override the resolvers you will be able to modify the outputs.
Example transformer
import { GraphQLSchema } from 'graphql';
export const exampleTransformer = (schema: GraphQLSchema): GraphQLSchema => {
// Collect all main types & override the resolvers
[
schema?.getQueryType()?.getFields(),
schema?.getMutationType()?.getFields(),
schema?.getSubscriptionType()?.getFields()
].forEach(fields => {
// Resolvers override
Object.values(fields ?? {}).forEach(field => {
// Check is there any resolver at all
if (typeof field.resolve !== 'function') {
return;
}
// Save the original resolver
const originalResolve = field.resolve;
// Override the current resolver
field.resolve = async (source, inputData, context, info) => {
// Get the original output
const outputData: any = await originalResolve.apply(originalResolve.prototype, [source, inputData, context, info]);
// Modify and return the output
return outputModifier(outputData);
};
});
});
return schema;
};
How to use it:
// Attach it to the GraphQLSchema > https://graphql.org/graphql-js/type/
let schema = makeExecutableSchema({...});
schema = exampleTransformer(schema);
const server = new ApolloServer({schema});
server.listen(serverConfig.port);
This solution will work on any GraphQL-JS service (apollo, express-graphql, graphql-tools, etc.).
Keep in min with this solution you will be able to manipulate the inputData too.
Solution 2 - Modify the response
This solution is more elegant, but is implemented after the implementation of the directives and scalar types and can not manipulate the input data.
The specific for the output object is that the data is null-prototype object (no instance methods like .hasOwnProperty(), .toString(), ...) and the errors are locked objects (readonly).
In the example I'm unlocking the error object... be careful with this and do not change the structure of the objects.
Example transformer
import { Translator } from '#helpers/translations';
import type { GraphQLResponse, GraphQLRequestContext } from 'apollo-server-types';
import type { GraphQLFormattedError } from 'graphql';
export const exampleResponseFormatter = () => (response: GraphQLResponse, requestContext: GraphQLRequestContext) => {
// Parse locked error fields
response?.errors?.forEach(error => {
(error['message'] as GraphQLFormattedError['message']) = exampleTransformer(error['message']);
(error['extensions'] as GraphQLFormattedError['extensions']) = exampleTransformer(error['extensions']);
});
// Parse response data
response.data = exampleTransformer(response.data);
// Response
return response;
};
How to use it:
// Provide the schema to the ApolloServer constructor
const server = new ApolloServer({
schema,
formatResponse: exampleResponseFormatter()
});
Conclusion
I'm using both solutions in my projects. With the first you can control the input and the output based on specific access directives in the code or to validate the whole data flow (on any graphql type) .
And second to translate all the strings based on the context headers provided by the user without messing resolvers and the code with language variables.
Those examples are tested on TS 4+ and GraphQL 15 and 16
I'm trying to figure out how to set a property type to be a specific object.
Or, to be more clear about my specific situation --- I'm injecting all of my models into GraphQL's context object. Each of the models are simple objects that look like:
const getUser = (id: string): UserType => db.collections('User').find({ id })
const User = {
getUser,
// ...etc
}
A resolver would look like:
const user = (_: Object, args: GetUserArgs, ctx: GraphQLContext) => {
ctx.models.User.getUser(args.input.id)
}
I could type GraphQLContext like this:
type UserModel = {
getUser: (id: string): UserType
// ... etc, for each function. But I've already typed `getUser` above.
}
type Models = {
user: UserModel
}
type GraphQLContext = {
models: Models
}
But, this seems tedious and error-prone as functions I'm adding to my models grow and change during development. Is there a way that I can type the models property that would get all of the type definitions that are already applied to each function? In other words, is there a way I can do this without having to type getUser in both places?
Here's an example
You could use the typeof operator to get the type of your model objects.
const User = {
getUser
}
const Models = {
User
}
type GraphQLContext = {
models: typeof Models
}
I implemented the idea in your example