Context not being passed to nested Apollo GraphQL resolver - javascript

I am using Apollo Server v2 for my project
I have added auth like given here https://www.apollographql.com/docs/apollo-server/features/authentication/
I wanted to include nested resolver in my Query and Mutation so I did as per https://stackoverflow.com/a/40916089/7584077
The thing is my resolver is a lil bit more complex than the one shown above
// typeDefs/typeDefs.js
import { gql } from "apollo-server-express";
import issueTracker from "./issueTracker";
const base = gql`
scalar Timestamp
type Query {
ping: String!
}
type Mutation {
ping: String!
}
`;
export default [base, issueTracker];
// typeDefs/issuetracker.js
import { gql } from "apollo-server-express";
export default gql`
type Comment {
id: ID
message: String
commentBy: User
createdAt: Timestamp
updatedAt: Timestamp
version: Int
}
type Issue {
id: ID
requestId: ID
title: String
issueNumber: Int
status: Int
tags: [String]
assignees: [User]
createdBy: User
comments: [Comment]
createdAt: Timestamp
updatedAt: Timestamp
version: Int
}
input CreateIssueRequest {
requestId: ID!
title: String!
createdBy: ID!
message: String!
assignees: [ID]!
}
type IssueTrackerQuery {
ping: String!
}
type IssueTrackerMutation {
createIssue(request: CreateIssueRequest!): Issue
}
extend type Query {
IssueTracker: IssueTrackerQuery
}
extend type Mutation {
IssueTracker: IssueTrackerMutation
}
`;
And a lil modified version of the stackoverflow answer above.
Here is my combined resolver.
// resolvers/resolvers.js
import IssueTracker from "./issueTracker";
export default {
Query: {
ping: () => "ping!",
IssueTracker: () => ({
ping: IssueTracker.ping,
}),
},
Mutation: {
ping: () => "ping!",
IssueTracker: () => ({
createIssue: IssueTracker.createIssue,
}),
},
};
This is because I wanted Query & Mutation to be completely separate.
Here is my IssueTracker resolver
// resolvers/issueTracker.js
export default {
ping: () => "ping",
createIssue: async (parent, args, context) => {
console.log(parent);
console.log(args);
console.log(context);
// create issue as per request and return
}
The thing is here is that, parent actually is the args field!
And I need userId from context to make sensible data.

Hmm, the SDL first approach can be a bit tricky. It's not easy to explain what is wrong here but I will do my best. First let me tell you what to need to do to make this work and then I will explain what goes wrong.
Create a IssueTrackerMutation field in the resolver map:
export default {
Query: {
ping: () => "ping!",
IssueTracker: () => ({ // same here but I will just do the mutation for you
ping: IssueTracker.ping,
}),
},
Mutation: {
ping: () => "ping!",
IssueTracker: () => null, // or whatever you want as a root here
},
IssueTrackerMutation: {
createIssue: IssueTracker.createIssue
}
};
Note the difference between creating a "pure" resolver for the IssueTracker and returning an object for IssueTracker with a createIssue method.
Now the function should be called with the expected parameters. The reason why the parent argument seems to be missing is the very special implementation of the default resolver. The resolver is intended to work with an object oriented style where fields can be fields or methods. You can imagine the resolver to work like this:
defaultResolver(fieldName, parent, args, context, info) {
if (typeof parent !== 'object') {
throw "Need object to default resolve";
}
if (typeof parent[fieldName] === 'function') {
return parent[fieldName](args, context, info);
}
return parent[fieldName];
}
This would allow you to write your data access objects in the following manner:
class IssueTracker {
issues = []
async createIssue(args, ctx, info) {
const issue = await createIssue(args.request);
this.issues.push(issue);
return issue;
}
}
const issueTracker = new IssueTracker();
export default {
// ...
Mutation: {
// ...
IssueTracker: () => issueTracker,
},
IssueTrackerMutation: {
createIssue: IssueTracker.createIssue
}
};
This is not talked about that much but is probably relatively close how GraphQL at Facebook works. The community seems to move more logic into the resolvers.

Related

Cannot query across one-to-many for property NestJS and TypeORM

I have two entities one is car and another one is carAvailability
import { Entity, Column, PrimaryGeneratedColumn, OneToMany } from 'typeorm';
import { CarAvailability } from 'src/car-availabilitys/car-availability.entity';
#Entity('cars')
export class Car {
#PrimaryGeneratedColumn()
id: number;
#Column()
name: string;
#OneToMany(() => CarAvailability, (carAvailability) => carAvailability.car, {
eager: true,
cascade: true,
})
availabilities: CarAvailability[];
}
I am trying to add a service that queries and filters cars based on the availabilities. In My Service and tried two ways:
Method 1 with repo functions:
async test () {
const startDateTime = '2012-04-24 02:25:43.511';
return await this.repo.find({
relations: ['availabilities'],
where: {
availabilities: {
start_date_time: startDateTime
}
}
});
}
Method 2 with query builder:
async test () {
const startDateTime = '2012-04-24 02:25:43.511';
return this.repo.createQueryBuilder('cars')
.innerJoin('cars.availabilities', 'car_availabilities')
.where("cars.availabilities.start_date_time = :startDateTime", { startDateTime })
.getMany();
}
Method 1 error:
Error: Cannot query across one-to-many for property availabilities
Method 2 error:
QueryFailedError: missing FROM-clause entry for table "availabilities"
I feel like I am missing something but I am not sure. Have referred both NestJS and TypeORM docs but can't seem to figure out what went wrong.
Using Method 2, because you aliased car.availabilities as car_availabilities, your where clause should use the alias name:
car_availabilities.start_date_time
not:
cars.availabilities.start_date_time
Using Method 1 repo functions with nested query builder:
async test () {
const startDateTime = '2012-04-24 02:25:43.511';
return await this.repo.find({
relations: ['availabilities'],
where: (qb) => {
qb.where('availabilities.start_date_time = :startDateTime', {
startDateTime
});
}
});
}
Remove cascade from oneToMany side and put:
{
cascade: true,
onDelete: 'CASCADE',
onUpdate:'CASCADE'
}
on manyToOne side.
And if you going to use relations: ['availabilities'] you can remove eager: true.

Cannot fetch data in Graphql Relay

I'm learning Relay to use in a React-Relay project. After my research and learning on the internet, I've run into problems with my graphql schema and resolvers. I can't seem to figure out what resolvers do I need and how go about it correctly. If someone can point me in the right direction, that would be great. I have attached my code below. All I'm trying to do is fetch elements of a list using the relay graphql server specification.
Graphql Schema
interface Node {
id: ID!
}
type Link implements Node {
id: ID!
title: String!
description: String!
}
type LinkConnection {
edges: [LinkEdge]
pageInfo: PageInfo!
}
type LinkEdge {
cursor: String!
node: Link
}
type Query {
links(after: String, before: String, first: Int, last: Int): LinkConnection
node(id: ID!): Node
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
Resolvers
const resolvers = {
Query,
Node: {
__resolveType(node, context, info){
if(node.title){
return 'Link'
}
return null
}
},
}
Query Resolver
const node = async (parent, args, {prisma}) => {
try{
console.log('hit')
const data = await prisma.link.findUnique({
where: {
id: Number(args.id)
}
})
console.log(data)
return data
} catch(err){
return err
}
}
export default {
node,
}
P.S. Im using Apollo Server and Prisma under the hood
Edit:
I solved this issue by realizing that the resolves are invoked in the same order the schema is nested. So by writing separate resolvers for each type and passing the information in parent argument, things worked.

Destructuring function parameters in Typescript

So I have this little function that takes care of updating a todo document in the database, it looks like so
async function update({id, ...todoInfo }: ITodo) { // this line here
const db = await makeDb()
const foundTodo = await db.collection('todos').updateOne({ _id: transformId(id) }, { $set: { ...todoInfo } })
return foundTodo.modifiedCount > 0 ? { _id: id, ...todoInfo } : null
}
The id property is meant to come from the req.params object, and the ...todoInfo comes from the req.body object. But typescript throws an error that property id does not exist on interface ITodo. How do I overcome this issue? The interface ITodo looks like this currently,
export interface ITodo {
todo_name: string
start_time: Date
end_time: Date
description: string
priority: Priority
}
I tried this method, but it led to object nesting like so.. {todoInfo: {todo_name...}}
async function update({id, ...todoInfo }: {id: string, todoInfo: ITodo}) {
const db = await makeDb()
const foundTodo = await db.collection('todos').updateOne({ _id: transformId(id) }, { $set: { ...todoInfo } })
return foundTodo.modifiedCount > 0 ? { _id: id, ...todoInfo } : null
}
I don't want to add the property id to the interface because it's either I keep using it everywhere or I could make it optional which will mess up other function calls because the id property cannot be undefined. Thank you very much.
You can use an intersection type:
async function update({ id, ...todoInfo }: { id: string } & ITodo) {
//...
}
Playground Link

Typescript and Mongoose: Property 'x' does not exist on type 'Document'

This is my Mongoose model that I use together with TypeScript:
import mongoose, { Schema } from "mongoose";
const userSchema: Schema = new Schema(
{
email: {
type: String,
required: true,
unique: true,
lowercase: true,
},
name: {
type: String,
maxlength: 50,
},
...
...
}
);
userSchema.method({
transform() {
const transformed = {};
const fields = ["id", "name", "email", "createdAt", "role"];
fields.forEach((field) => {
transformed[field] = this[field];
});
return transformed;
},
});
userSchema.statics = {
roles,
checkDuplicateEmailError(err: any) {
if (err.code === 11000) {
var error = new Error("Email already taken");
return error;
}
return err;
},
};
export default mongoose.model("User", userSchema);
I use this model in my controller:
import { Request, Response, NextFunction } from "express";
import User from "../models/user.model";
import httpStatus from "http-status";
export const register = async (
req: Request,
res: Response,
next: NextFunction
) => {
try {
const user = new User(req.body);
const savedUser = await user.save();
res.status(httpStatus.CREATED);
res.send(savedUser.transform());
} catch (error) {
return next(User.checkDuplicateEmailError(error));
}
};
I get the following errors:
Property 'transform' does not exist on type 'Document'.
Property 'checkDuplicateEmailError' does not exist on type 'Model<Document, {}>'.
I tried export default mongoose.model<any>("User", userSchema); and I do not get the transform error but still the error for checkDuplicateEmailError.
You know that mongoose.model("User", userSchema); creates a Model, but the question is: a model of what?
Without any type annotations, the model User gets the type Model<Document, {}> and the user object created from new User() gets the type Document. So of course you are going to get errors like "Property 'transform' does not exist on type 'Document'."
When you added your <any> variable, the type for user became any. Which actually gives us less information than knowing that user is a Document.
What we want to do is create a model for specific type of Document describing our user. Instances of the user should have a method transform() while the model itself should have the method checkDuplicateEmailError(). We do this by passing generics to the mongoose.model() function:
export default mongoose.model<UserDocument, UserModel>("User", userSchema);
The hard part is figuring out those two types. Frustratingly, mongoose doesn't automatically apply the fields from your schema as properties of the type, though there are packages that do this. So we have to write them out as typescript types.
interface UserDocument extends Document {
id: number;
name: string;
email: string;
createdAt: number;
role: string;
transform(): Transformed;
}
Our transform function returns a object with 5 specific properties from the UserDocument. In order to access the names of those properties without having to type them again, I moved the fields from inside your transform method to be a top-level property. I used as const to keep their types as string literals rather than just string. (typeof transformFields)[number] gives us the union of those strings.
const transformFields = ["id", "name", "email", "createdAt", "role"] as const;
type Transformed = Pick<UserDocument, (typeof transformFields)[number]>
Our UserModel is a Model of UserDocument and it also includes our checkDuplicateEmailError function.
interface UserModel extends Model<UserDocument> {
checkDuplicateEmailError(err: any): any;
}
We should also add the UserDocument generic when we create our Schema, so that this will have the type UserDocument when we access it inside a schema method.
const userSchema = new Schema<UserDocument>({
I got all sorts of typescript errors trying to implement the transform() method including missing index signatures. We can avoid reinventing the wheel here by using the pick method from lodash. I still had problems with the mongoose methods() helper function, but it works fine using the direct assignment approach.
userSchema.methods.transform = function (): Transformed {
return pick(this, transformFields);
};
You could also use destructuring to avoid the index signature issues.
userSchema.methods.transform = function (): Transformed {
const {id, name, email, createdAt, role} = this;
return {id, name, email, createdAt, role};
}
Within your email check function, I added a typeof check to avoid runtime errors from trying to access the property err.code if err is undefined.
if ( typeof err === "object" && err.code === 11000) {
That should fix all your errors.
Playground Link

GraphQL nested query definition

I'm trying to create tree-like structure for my queries, to get rid off queries like
peopleList, peopleSingle, peopleEdit, peopleAdd, peopleDelete companyList, companySingle, companyEdit, companyAdd, companyDelete etc.
In the end I would like to send query like this:
query test {
people {
list {
id
name
}
single(id: 123) {
id
name
}
}
company {
list {
id
name
}
single(id: 456) {
id
name
}
}
}
mutation test2 {
people {
create(data: $var) {
id
name
}
}
people {
edit(id: 123, data: $var) {
id
name
}
}
}
This is part of my query object on people module:
people: {
type: //What type this should be?
name: 'Root of People queries',
fields: () => ({
list: {
type: peopleType,
description: 'Returns all people in DB.',
resolve: () => {
// resolve method implementation
}
},
single: {
type: peopleType,
description: 'Single row from people table. Requires ID argument.',
args: {
id: { type: new GraphQLNonNull(GraphQLID) }
},
resolve: () => {
// resolve method implementation
}
}
})
}
I have tried to put this snippet into GraphQLObjectType and then combine them together in RootQuery (using GraphQLObjectType again) - didn't work.
Alternative method could be to create new Type - like peopleQueriesType, inside this type specify all my queries as fields and then create single query for this object. But this seems odd to me - polluting my code with unnecessary objects just to merge my queries in tree-like shape.
I have tried to look at Apollo server implementation, if it can do this kind of query structure, but couldn't find any help in documentation.
I'm using node.js + express + graphql-js on my server.
Short answer:
type should be a GraphQLObjectType containing all the fields like this:
type: new GraphQLObjectType({ name: 'patientQuery', fields: { find, findOne } })
Details: I ended up with this query using the code below:
{
patient {
find {
id
active
}
findOne(id: "pat3") {
id
active
}
}
}
in patient/queries/index.js I have this
import findOne from './find-one.js';
import find from './find.js';
import { GraphQLObjectType } from 'graphql';
export default {
patient: {
type: new GraphQLObjectType({ name: 'patientQuery', fields: { find, findOne } }),
resolve(root, params, context, ast) {
return true;
}
}
};
then in queries.js
import patient from './patient/queries/index.js';
export default {
...patient
};
and finally my schema schema.js that is passed to graphql express server
import {
GraphQLObjectType,
GraphQLSchema
} from 'graphql';
import queries from './queries';
import mutations from './mutations';
export default new GraphQLSchema({
query: new GraphQLObjectType({
name: 'Query',
fields: queries
}),
mutation: new GraphQLObjectType({
name: 'Mutation',
fields: mutations
})
});

Categories