Background
We are working on a fairly large Apollo project. A very simplified version of our api looks like this:
type Operation {
foo: String
activity: Activity
}
type Activity {
bar: String
# Lots of fields here ...
}
We've realised splitting Operation and Activity does no benefit and adds complexity. We'd like to merge them. But there's a lot of queries that assume this structure in the code base. In order to make the transition gradual we add #deprecated directives:
type Operation {
foo: String
bar: String
activity: Activity #deprecated
}
type Activity {
bar: String #deprecated(reason: "Use Operation.bar instead")
# Lots of fields here ...
}
Actual question
Is there some way to highlight those deprecations going forward? Preferably by printing a warning in the browser console when (in the test environment) running a query that uses a deprecated field?
So coming back to GraphQL two years later I just found out that schema directives can be customized (nowadays?). So here's a solution:
import { SchemaDirectiveVisitor } from "graphql-tools"
import { defaultFieldResolver } from "graphql"
import { ApolloServer } from "apollo-server"
class DeprecatedDirective extends SchemaDirectiveVisitor {
public visitFieldDefinition(field ) {
field.isDeprecated = true
field.deprecationReason = this.args.reason
const { resolve = defaultFieldResolver, } = field
field.resolve = async function (...args) {
const [_,__,___,info,] = args
const { operation, } = info
const queryName = operation.name.value
// eslint-disable-next-line no-console
console.warn(
`Deprecation Warning:
Query [${queryName}] used field [${field.name}]
Deprecation reason: [${field.deprecationReason}]`)
return resolve.apply(this, args)
}
}
public visitEnumValue(value) {
value.isDeprecated = true
value.deprecationReason = this.args.reason
}
}
new ApolloServer({
typeDefs,
resolvers,
schemaDirectives: {
deprecated: DeprecatedDirective,
},
}).listen().then(({ url, }) => {
console.log(`🚀 Server ready at ${url}`)
})
This works on the server instead of the client. It should print all the info needed to track down the faulty query on the client though. And having it in the server logs seem preferable from a maintenance perspective.
Related
I found out that this is known problem with Strapi + Gatsby setup, but I found some articles about how to get rid of that error like the following ones:
https://www.virtualbadge.io/blog-articles/nullable-relational-fields-strapi-gatsbyjs-graphql
https://medium.com/swlh/gatsby-graphql-and-the-missing-but-necessary-explanation-about-type-definitions-87a5ef83e759
And indeed it helped with the bug itself, but caused another. At the moment when I fill the field inside the CMS graphql cannot see it and interprets it as null.
Without gatsby-node.ts:
And With gatsby-node.ts:
import type { GatsbyNode } from 'gatsby'
export const sourceNodes: GatsbyNode['sourceNodes'] = async ({ actions }) => {
const { createTypes } = actions
const typeDefs = `
type STRAPI__COMPONENT_BASE_HERO implements Node {
backgroundVideo: STRAPI__MEDIA
}
`
createTypes(typeDefs)
}
I have two subgraphs hosting their own respective schemas. Each of these schemas have input types that are named the same. For example, both schemas have an entity called Product and therefore both have inputs relevant to this entity called ProductCreateInput. Due to the way Apollo Federation works, because Input types are merged using the intersection strategy, I have to rename the inputs to different names to avoid composition errors when composing a supergraph.
So I rename the ProductCreateInput to something like Product_ProductCreateInput and Review_ProductCreateInput. I do this for every input type by using a regex and wrapSchema from #graphql-tools/wrap to rename the input types to precede with their subgraph name.
The driver code :
#Injectable()
export class GqlConfigService implements GqlOptionsFactory {
constructor(private readonly config: ConfigService, private readonly prisma: PrismaService) {}
createGqlOptions(): ApolloFederationDriverConfig {
const plugins: PluginDefinition[] = [];
if (this.config.graphql.sandbox) plugins.push(ApolloServerPluginLandingPageLocalDefault);
if (!this.config.graphql.trace) plugins.push(ApolloServerPluginInlineTraceDisabled());
return {
typeDefs: print(ALL_TYPE_DEFS),
resolvers: { Upload: GraphQLUpload },
transformSchema: async (schema: GraphQLSchema) => {
return renamedInputTypesSchema(schema);
},
debug: !this.config.production,
playground: false,
plugins,
introspection: this.config.graphql.introspection,
cors: this.config.cors,
csrfPrevention: this.config.graphql.csrfPrevention,
cache: 'bounded',
installSubscriptionHandlers: this.config.graphql.subscriptions,
subscriptions: this.config.graphql.subscriptions
? {
'graphql-ws': {
onConnect: (context: Context<any>) => {
const { connectionParams, extra } = context;
extra.token = connectionParams.token;
},
},
}
: undefined,
context: async (ctx): Promise<IContext> => {
// Subscriptions pass through JWT token for authentication
if (ctx.extra) return { req: ctx.extra, prisma: this.prisma };
// Queries, Mutations
else return { ...ctx, prisma: this.prisma };
},
};
}
}
The schemaWrapper code :
import { RenameTypes, wrapSchema } from '#graphql-tools/wrap';
import { GraphQLSchema } from 'graphql';
export const modelNames = ['User', 'Product'];
//This subgraph's name is set to "Product".
//Input type args is modified to be preceded by "Product_" because inputs are merged using the intersection strategy in the current version of Apollo Federation and directives are not supported with input types.
export const renamedInputTypesSchema = async (schema: GraphQLSchema) => {
const typeMap = schema.getTypeMap();
const models: string = modelNames.join('|');
const inputTypes = Object.keys(typeMap).filter(type => {
const inputTypesRegex = new RegExp(
`(${models})(WhereInput|OrderByWithRelationInput|WhereUniqueInput|OrderByWithAggregationInput|ScalarWhereWithAggregatesInput|CreateInput|UncheckedCreateInput|UpdateInput|UncheckedUpdateInput|CreateManyInput|UpdateManyMutationInput|UncheckedUpdateManyInput|CountOrderByAggregateInput|AvgOrderByAggregateInput|MaxOrderByAggregateInput|MinOrderByAggregateInput|SumOrderByAggregateInput|Create.*?Input|Update.*?Input)`
);
return type.match(inputTypesRegex)?.input;
});
return wrapSchema({
schema: schema,
transforms: [new RenameTypes(name => (inputTypes.includes(name) ? `Product_${name}` : name))],
});
};
This works. When I go into Apollo Sandbox and look at the schema, all the inputs are successfully preceded with Product_ like I expect :
However, when I use rover subgraph introspect in order to pipe the output to publish to my managed federation, I get the unwrapped schema (the relevant rover subgraph introspect output):
input ProductCountOrderByAggregateInput {
id: SortOrder
sku: SortOrder
description: SortOrder
}
input ProductAvgOrderByAggregateInput {
id: SortOrder
}
input ProductMaxOrderByAggregateInput {
id: SortOrder
sku: SortOrder
description: SortOrder
}
input ProductMinOrderByAggregateInput {
id: SortOrder
sku: SortOrder
description: SortOrder
}
What is going on here? Apollo sandbox shows the correct wrapped schema, yet rover introspect doesn't.
I have the following directive in y GraphQL schema to check if arguments are real slugs
directive #slug on ARGUMENT_DEFINITION
Query {
subject(id: ID! #slug): Subject
}
We had a working solution before using SchemaDirectiveVisitor from apollo-server-express, which to my understanding was just an re export from graphql-tools. As this was removed with the latest major release we have to refactor it. As far as I understand this, also the graphql-tools API has changed, so to react on directives we have to follow the examples here.
So this is what I have so far but it doesn't go into the fieldConfig.resolve function at all:
export default function (schema) {
return mapSchema(schema, {
[MapperKind.ARGUMENT]: fieldConfig => {
const slugDirective = getDirective(schema, fieldConfig, 'slug')?.[0]
if (slugDirective) {
const { resolve = defaultFieldResolver } = fieldConfig
fieldConfig.resolve = async function (
source,
args,
context,
info
) {
const result = await resolve(source, args, context, info)
console.log('result', result)
}
return fieldConfig
}
},
})
}
Currently trying to switch from graphql-js to literal GraphQL types/schemas, I'd like to know if anyone has had any experience with this.
Let's take this really simple one :
const Person = new GraphQLObjectType({
name: 'Person',
fields: () => ({
name: {
type: GraphQLString,
description: 'Person name',
},
}),
});
I'd like to switch to the native GraphQL schema syntax i.e
type Person {
# Person name
name: String
}
However this would have to be incremental, and given the use of graphql-js, the best solution for now would be to parse GraphQL template literals to GraphQLObjectType (or any other type for that matter). Does anyone have experience doing this, I cannot seem to find any library for it unfortunately.
import { printType } from 'graphql';
printType(Person)
output:
type Person {
"""Person name"""
name: String
}
Here is the demo:
import { expect } from 'chai';
import { printType, printSchema, buildSchema, GraphQLSchema } from 'graphql';
import { logger } from '../util';
import { Person } from './';
describe('test suites', () => {
it('convert constructor types to string types', () => {
const stringTypeDefs = printType(Person).replace(/\s/g, '');
logger.info(printType(Person));
const expectValue = `
type Person {
"""Person name"""
name: String
}
`.replace(/\s/g, '');
expect(stringTypeDefs).to.be.equal(expectValue);
});
it('buildSchema', () => {
const stringTypeDefs = printType(Person);
const schema = buildSchema(stringTypeDefs);
expect(schema).to.be.an.instanceof(GraphQLSchema);
});
it('printSchema', () => {
const stringTypeDefs = printType(Person);
const schema = printSchema(buildSchema(stringTypeDefs));
logger.info(schema);
const expectValue = `
type Person {
"""Person name"""
name: String
}
`.replace(/\s/g, '');
expect(schema.replace(/\s/g, '')).to.be.eql(expectValue);
});
});
source code:
https://github.com/mrdulin/nodejs-graphql/blob/master/src/convert-constructor-types-to-string-types/index.spec.ts
You can use graphql-cli to extract a native graphql schema from a graphql server. All you need to do is..
Download the tool | npm i -g graphql-cli
Run graphql init in the directory of your project to
create .graphqlconfig file
Start your graphql server
Run graphql get-schema and this will generate a your schema in native graphql
SAMPLE .graphqlconfig
{
"projects": {
"my_sample_project": {
"schemaPath": "schema.graphql",
"extensions": {
"endpoints": {
"local": "http://localhost:8080/graphql"
}
}
}
}
}
We leverage the auto-generation of graphql schema/queries/mutations for our CI workflows.
I was going through the relay docs and came to following code in RANGE_ADD.
class IntroduceShipMutation extends Relay.Mutation {
// This mutation declares a dependency on the faction
// into which this ship is to be introduced.
static fragments = {
faction: () => Relay.QL`fragment on Faction { id }`,
};
// Introducing a ship will add it to a faction's fleet, so we
// specify the faction's ships connection as part of the fat query.
getFatQuery() {
return Relay.QL`
fragment on IntroduceShipPayload {
faction { ships },
newShipEdge,
}
`;
}
getConfigs() {
return [{
type: 'RANGE_ADD',
parentName: 'faction',
parentID: this.props.faction.id,
connectionName: 'ships',
edgeName: 'newShipEdge',
rangeBehaviors: {
// When the ships connection is not under the influence
// of any call, append the ship to the end of the connection
'': 'append',
// Prepend the ship, wherever the connection is sorted by age
'orderby(newest)': 'prepend',
},
}];
}
/* ... */
}
Now over here it is mentioned that edgeName is required for adding new node to the connection. Looks well and fine.
Now, I move further down the documentation and reached the GraphQL implementation of this mutation.
mutation AddBWingQuery($input: IntroduceShipInput!) {
introduceShip(input: $input) {
ship {
id
name
}
faction {
name
}
clientMutationId
}
}
Now according to docs this mutation gives me output as
{
"introduceShip": {
"ship": {
"id": "U2hpcDo5",
"name": "B-Wing"
},
"faction": {
"name": "Alliance to Restore the Republic"
},
"clientMutationId": "abcde"
}
}
I cannot see edgeName being present here.
I was using graphene for my project. Over there also I saw something similar only
class IntroduceShip(relay.ClientIDMutation):
class Input:
ship_name = graphene.String(required=True)
faction_id = graphene.String(required=True)
ship = graphene.Field(Ship)
faction = graphene.Field(Faction)
#classmethod
def mutate_and_get_payload(cls, input, context, info):
ship_name = input.get('ship_name')
faction_id = input.get('faction_id')
ship = create_ship(ship_name, faction_id)
faction = get_faction(faction_id)
return IntroduceShip(ship=ship, faction=faction)
Over here also I cannot see edgeName anywhere.
Any help please? I am working on mutations for the first so wanted to confirm a m I missing something or is something wrong here?
This example might be either simplified or a bit obsoloete, because in practice there is need to return edge and that's exactly what is fetched by relay (other fields in RANGE_ADD are more a kind of declaration and are not necessarily fetched).
Here is how you can do it in graphene:
# Import valid as of graphene==0.10.2 and graphql-relay=0.4.4
from graphql_relay.connection.arrayconnection import offset_to_cursor
class IntroduceShip(relay.ClientIDMutation):
class Input:
ship_name = graphene.String(required=True)
faction_id = graphene.String(required=True)
ship = graphene.Field(Ship)
faction = graphene.Field(Faction)
new_ship_edge = graphene.Field(Ship.get_edge_type().for_node(Ship))
#classmethod
def mutate_and_get_payload(cls, input, context, info):
ship_name = input.get('ship_name')
faction_id = input.get('faction_id')
ship = create_ship(ship_name, faction_id)
faction = get_faction(faction_id)
ship_edge_type = Ship.get_edge_type().for_node(Ship)
new_ship_edge = edge_type(
# Assuming get_ships_number supplied
cursor=offset_to_cursor(get_ships_number())
node=ship
)
return cls(ship=ship, faction=faction, new_ship_edge=new_ship_edge)