Apollo Rover CLI returns non-wrapped schema when using NestJS driver - graphql

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.

Related

GraphQL combining two Resolvers

I currently have two resolvers, Authors and Books, that return data from two separate API's. In most scenarios I only need to call one or the other, however, in this scenario, I need to attach the books to the author.
It's not clear to me how I should do this.
Option 1 - Simple to do
I call the book API in the Author resolver and combine them here. This means I'd potentially make unnecessary calls to the Book API. It also means if the book API changes, I'd have to make updates to both the author and book resolvers instead of just updating the Book resolver.
Option 2 - Resolver
Is there a way to call the Book resolver from within the Author resolver?
Options 3 - Client
Is there a way to stitch the author and book together from within the client query?
I’m new to graphql and type-graphql so apologies if this is obvious.
Author
const author = {
name: 'James',
bookIds: [1, 2]
};
Book
const book = {
id: 1,
title: 'Book 1'
};
Desired outcome
const author = {
name: 'James',
books: [{
id: 1,
title: 'Book 1'
},
{
id: 2,
title: 'Book 2'
}]
}
Resolvers
#Service()
#Resolver(() => Author)
export class AuthorResolver {
constructor(private readonly authorService: authorService) { }
#Query(() => Author)
async author(
#Arg('authorId', () => ID, { nullable: false }) authorId: string,
#Ctx() { dataSources }: ResolverContext
): Promise<Author | undefined> {
const { authorService } = dataSources;
const author = await this.author.getAuthor(authorService, authorId);
return {
id: author.id,
name: author.name,
bookIds: author.bookIds
};
}
}
#Service()
#Resolver(() => Book)
export class BookResolver {
constructor(private readonly bookService: bookService) { }
#Query(() => Book)
async book(
#Arg('bookId', () => ID, { nullable: false }) bookId: string,
#Ctx() { dataSources }: ResolverContext
): Promise<Book | undefined> {
const { bookService } = dataSources;
const book = await this.book.getBook(bookService, bookId);
return {
id: book.id,
title: book.title
};
}
}
Client Side Query
query BookQuery($bookId: ID!) {
book(bookId: $bookId) {
id
title
}
}
query authorQuery($authorId: ID!) {
book(authorId: $authorId) {
id
name
books
}
}
You must implement a FieldResolver, called books, in the Author resolver.
If an API changes in the future, it will not affect the resolver since you use a service that talks to the API and acts as a middleware layer.
The service must be well implemented (abstraction) and the returned entity must be matched/mapped correctly to a GraphQL object type. i.e. there is no need to return {id: author.id, ...} inside a resolver since it's done automatically by the service and class mappings. 
Moreover, you inject a service instance inside the resolver, so there is no need to use #Ctx and obtain the same service instance: simply use this.[SERVICE_NAME].[METHOD].
Keep you context as simple as possible (e.g. authenticated user id obtained by a JWT).
The final Author resolver is much cleaner and more portable:
#Resolver(() => Author)
#Service()
export class AuthorResolver {
#Inject()
private readonly authorService!: AuthorService;
#Inject()
private readonly bookService!: BookService;
// 'nullable: false' is default behaviour
// No need for '#Ctx' here
// Returned value is inferred from service -> 'Promise<Author | undefined>'
#Query(() => Author)
async author(#Arg('id', () => ID) id: string) {
return this.authorService.findOne(id);
}
#FieldResolver(() => [Book])
async books(#Root() author: Author) {
return this.bookService.findManyByAuthorId(author.id);
// OR 'return this.bookService.findMany(author.bookIds);'
}
}
If you want an example project, see this.

graphql conditional subquery based on parent field

What is the approach when your subquery needs some field from parent to resolve?
In my case owner_id field from parent is needed for owner resolver, but what should i do if user will not request it?
Should i somehow force fetch owner_id field or skip fetching owner when owner_id is not in request?
Problematic query:
project(projectId: $projectId) {
name
owner_id <--- what if user didn't fetch this
owner {
nickname
}
}
Graphql type:
type Project {
id: ID!
name: String!
owner_id: String!
owner: User!
}
And resolvers:
export const project = async (_, args, ___, info) => {
return await getProjectById(args.projectId, getAstFields(info))
}
export default {
async owner(parent, __, ___, info) {
return await getUserById(parent.owner_id, getAstFields(info))
},
}
Helper functions that doesn't matter:
export const getUserById = async (userId: string, fields: string[]) => {
const [owner] = await query<any>(`SELECT ${fields.join()} FROM users WHERE id=$1;`, [userId])
return owner
}
export const getAstFields = (info: GraphQLResolveInfo): string[] => {
const { fields } = simplify(parse(info) as any, info.returnType) as { fields: { [key: string]: { name: string } } }
return Object.entries(fields).map(([, v]) => v.name)
}
You can always read owner_id in project type resolver.
In this case ... modify getAstFields function to accept additional parameter, f.e. an array of always required properties. This way you can attach owner_id to fields requested in query (defined in info).
I was thinking that resolver will drop additional props internally.
It will be removed later, from 'final' response [object/tree].

Can't custom value of graphql enum

I have looked this question: How to use or resolve enum types with graphql-tools?
And, this doc: https://www.apollographql.com/docs/graphql-tools/scalars/#internal-values
Now, I want to custom the value of graphql enum.
typeDefs.ts:
import { gql } from 'apollo-server';
export const typeDefs = gql`
enum Device {
UNKNOWN
DESKTOP
HIGH_END_MOBILE
TABLET
CONNECTED_TV
}
type CampaignPerformanceReport {
campaignNme: String!
campaignId: ID!
device: Device
}
type Query {
campaignPerformanceReports: [CampaignPerformanceReport]!
}
`;
resolvers.ts:
import { IResolvers } from 'graphql-tools';
import { IAppContext } from './appContext';
export const resolvers: IResolvers = {
Device: {
UNKNOWN: 'Other',
DESKTOP: 'Computers',
HIGH_END_MOBILE: 'Mobile devices with full browsers',
TABLET: 'Tablets with full browsers',
CONNECTED_TV: 'Devices streaming video content to TV screens',
},
Query: {
async campaignPerformanceReports(_, __, { db }: IAppContext) {
return db.campaignPerformanceReports;
},
},
};
As you can see, I custom the value of Device enum in the resolver.
db.ts: a fake db with datas
enum Device {
UNKNOWN = 'Other',
DESKTOP = 'Computers',
HIGH_END_MOBILE = 'Mobile devices with full browsers',
TABLET = 'Tablets with full browsers',
CONNECTED_TV = 'Devices streaming video content to TV screens',
}
export const db = {
campaignPerformanceReports: [
{
campaignId: 1,
campaignNme: 'test',
device: Device.DESKTOP,
},
],
};
I also made an integration test for this:
test.only('should query campaign performance reports correctly with executable graphql schema', async () => {
const schema = makeExecutableSchema({ typeDefs, resolvers });
console.log(printSchema(schema));
const server: ApolloServerBase = new ApolloServer({ schema, context: { db } });
const { query }: ApolloServerTestClient = createTestClient(server);
const res: GraphQLResponse = await query({ query: Q.campaignPerformanceReports });
expect(res).toMatchInlineSnapshot(`
Object {
"data": Object {
"campaignPerformanceReports": Array [
Object {
"campaignId": "1",
"campaignNme": "test",
"device": "DESKTOP",
},
],
},
"errors": undefined,
"extensions": undefined,
"http": Object {
"headers": Headers {
Symbol(map): Object {},
},
},
}
`);
});
As you can see, the result of snapshot testing. The value of device field is still "DESKTOP", I expected the value should be "Computers"
Dependencies version:
"apollo-server": "^2.9.3",
"apollo-server-express": "^2.9.3",
"graphql": "^14.5.4",
The minimal repo: https://github.com/mrdulin/apollo-graphql-tutorial/tree/master/src/custom-scalar-and-enum
The internal values you specify for a GraphQL enum are just that -- internal. This is stated in the documentation:
These don't change the public API at all, but they do allow you to use that value instead of the schema value in your resolvers
If you map the enum value DESKTOP to the internal value Computers, only the behavior of your resolvers will be affected. Specifically:
If a field takes an argument of the type Device and the argument is passed the value DESKTOP, the value actually passed to the resolver function will be Computers.
If a field itself has the type device and we want to return DESKTOP, inside our resolver, we will need to return Computers instead.
Take for example a schema that looks like this:
type Query {
someQuery(device: Device!): Device!
}
If you don't specify internal values, our resolver works like this:
function (parent, args) {
console.log(args.device) // "DESKTOP"
return 'DESKTOP'
}
If you do specify internal values, the resolver looks like this:
function (parent, args) {
console.log(args.device) // "Computers"
return 'Computers'
}
The resolver is the only thing impacted by providing internal values for each enum value. What doesn't change:
How the enum value is serialized in the response. Enum values are always serialized as strings of the enum value name.
How the enum value is written as a literal inside a document. For example, if querying the above same field, we would always write: { someQuery(device: DESKTOP) }
How the enum value is provided as a variable. A variable of the type Device would always be written as "DESKTOP".
NOTE: While the question pertains specifically to Apollo Server, the above applies to vanilla GraphQL.js as well. For example, this enum
const DeviceEnum = new GraphQLEnumType({
name: 'Device',
values: {
UNKNOWN: { value: 'Other' },
DESKTOP: { value: 'Computers' },
HIGH_END_MOBILE: { value: 'Mobile devices with full browsers' },
TABLET: { value: 'Tablets with full browsers' },
CONNECTED_TV: { value: 'Devices streaming video content to TV screens' },
}
})
will still behave as described above.

Unable to use Fragments on GraphQL-yoga with Primsa

I am using graphql-yoga with Prisma and Prisma-Bindings
I'm trying to add a fragment to my resolver so that a specific field (id in this situation) is always fetched when the user asks for a custom field, costsToDate.
This is so i can make some additional queries needed to build the result for that field, and i need the ID of the object for that.
Unfortunatley i can't seem to get it to work, and the documentations seems a little lacking on the specifics with graphql-yoga and Prisma.
Here is the definition of the type:
type Job {
id: ID!
projectNumber: String!
client: Client!
name: String!
description: String
quoteNumber: String
workshopDaysQuoted: String!
quoted: String!
targetSpend: String!
costs: [JobCost!]!
estimatedCompletion: DateTime
completed: Boolean!
costTotal: String
workshopDaysUsed: String
costsToDate: String
}
And here is the resolver for the query:
const jobs = {
fragment: `fragment description on Jobs { id }`,
resolve: jobsResolver
}
async function jobsResolver(root, args, context, info) {
await validatePermission(args,context,info,['admin','user','appAuth'])
const {showCompleted} = args
const completedFilter = (typeof showCompleted === 'boolean') ? { completed: showCompleted } : {}
const jobIDS = await context.db.query.jobs({ where: completedFilter }, `{ id }`)
//console.log(jobIDS);
let jobs = await context.db.query.jobs({
where: completedFilter
}, info)
return await getAllJobCostsToDateList(jobs)
}
I am applying the the fragmentReplacements as per below.
const fragmentReplacements = extractFragmentReplacements(resolvers)
console.log(fragmentReplacements)
const port = process.env.PORT || 3010
const graphQLServer = new GraphQLServer({
typeDefs: './src/schema.graphql',
resolvers,
resolverValidationOptions: {
requireResolversForResolveType: false
},
context: req => ({
...req,
db: new Prisma({
typeDefs: `src/generated/prisma.graphql`,
fragmentReplacements,
endpoint: PRISMA_ENDPOINT,
secret: PRISMA_KEY,
debug: false
})
})
})
If i console.log the fragmentReplacements object i get the following, so it does seem to be picking up the fragments.
[ { field: 'job', fragment: 'fragment costsToDate on Job { id }' },
{ field: 'jobs',
fragment: 'fragment costsToDate on Jobs { id }' } ]
So my expectation here is that if i make a query against jobs or job that asks for the costsToDate field that it will also fetch the id for the job/each job.
However if i make the following query.
query{
jobs{
description
costsToDate
}
}
But i see no id fetched, and nothing in the root parameter on the resolver function.
Apologies as i am probably barking up completely the wrong tree here, seems like a somewhat simple requirement, but i can't quite work it out. Sure i'm missing something fundamental.
Thanks!
Gareth
A fragment is used to always retrieve given fields on a given type.
It follows the following format:
fragment NameOfYourFragment on YourType { ... }
You currently can't apply a given fragment conditionally as it is always applied.
Moreover, you specified a fragment on Jobs, but the type name used by Prisma is Job (even if you have the job and jobs resolvers)
You probably only need the following resolver:
const job = {
fragment: `fragment JobId on Job { id }`,
resolve: jobsResolver
}

Switching from graphql-js to native graphql schemas?

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.

Resources