Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

get parent object in directive function #289

Closed
vvakame opened this issue Aug 14, 2018 · 10 comments
Closed

get parent object in directive function #289

vvakame opened this issue Aug 14, 2018 · 10 comments

Comments

@vvakame
Copy link
Collaborator

vvakame commented Aug 14, 2018

Expected Behaviour

Parent object can be retrieved from ResolverContext.

Actual Behavior

It seems not to be able to get it.

Minimal graphql.schema and models to reproduce

I made small demo project.
Is there a good way to do it?

https://github.com/vvakame/til/blob/ad9d192d297b4e0be358f3273442502497ff50fb/graphql/gqlgen-directive-parent/schema.graphql#L9
https://github.com/vvakame/til/blob/ad9d192d297b4e0be358f3273442502497ff50fb/graphql/gqlgen-directive-parent/server/server.go#L49-L56

@JulienBreux
Copy link

Good question ;)

@jekaspekas
Copy link
Contributor

Hi!
The short answer is nothing.

// directive implement func
resp, err := next(ctx)

resp always contains the type of the field to which the directive is assigned and you receive a compilation error, or runtime error if you use a directive for fields with different types.
Performing a next(ctx) function is dangerous because if this directive assigned to mutation field, you run change this field anyway.
Also, you can't use correctly CurrentUser from context, because the context is recreated every request.

But if you use a websocket as a transport, you can see that wsConnection ("gqlgen/handler/websocket.go") struct is not change on every request. Exportable WSConnection with new CurrentUser field you can write to context. But this will require changing the package "99designs/gqlgen/handler" (you can copy this to project directory).
handler/websocket.go:

...
type WSConnection struct {
  ctx    context.Context
  conn   *websocket.Conn
  exec   graphql.ExecutableSchema
  active map[string]context.CancelFunc
  mu     sync.Mutex
  cfg    *Config
  // new field
  CurrentUser *gqlgen_directive_parent.User
}
type key string
const WSConnectionKey key = "WSConnectionKey"
...
func (c *WSConnection) subscribe(message *operationMessage) bool {
...
  reqCtx := c.cfg.newRequestContext(doc, reqParams.Query, vars)
  ctx := graphql.WithRequestContext(c.ctx, reqCtx)
  // write WSConnection into context
  ctx = context.WithValue(ctx, WSConnectionKey, c)
...
}

ID comparison must be performed at todoResolver func.

func (r *todoResolver) Text(ctx context.Context, obj *gqlgen_directive_parent.Todo) (string, error) {
  conn, ok := ctx.Value(handler.WSConnectionKey).(*handler.WSConnection)
  if !ok {
    return nil, gqlerror.Errorf("BOOM! Headshot")
  }
  currentUser := conn.CurrentUser
  ...
  if todo.User.ID != currentUser.ID {
    return nil, nil
  }
  ...
}

Perhaps this way is idiomatically incorrect, but for me it works.
Possible changes after Pull request.

@mathewbyrne
Copy link
Contributor

mathewbyrne commented Aug 20, 2018

I'm not sure how necessary this is. To me it feels like a directive is not the correct tool for this requirement, as it would explicitly tie your directive to the context of a Todo, unless you wrote your directive in such a way that it handled every possible parent type.

After a bit of internal discussion, we thought that a solution might instead be for the parent resolver to set something in the context that the directive could read back out. Something like ResolverUser?

I am interested to know what other people think though, as I'm interested in what other use-cases people have for directives.

@pandada8
Copy link

maybe a object level directive can be helpful ?

@vvakame
Copy link
Collaborator Author

vvakame commented Aug 21, 2018

It seems like Resolver has a hierarchy of ParentObject in ResolverContext, just like Path.

My other usecase.

enum Role {
  Public
  LoggedIn
  ResourceOwner
  Staff
}

directive @hasRole(requires: Role) on OBJECT | FIELD_DEFINITION

directive @afterEventElectionClosed on OBJECT | FIELD_DEFINITION

type Event implements Node {
  id: ID!
  name: String!
  electionClosed: Boolean! # this field can modify only staff
}

type CircleExhibitInfo implements Node {
  id: ID!
  event: Event!
  name: String!
  spaces: [String!]! @afterEventElectionClosed   # this field can see when this.event.electionClosed === true
  note: String @hasRole(requires: ResourceOwner) # resource owner can write note to staff
  staffNote: String @hasRole(requires: Staff)    # staff can write note, but it isn't visible from resource owner
}

@vvakame vvakame changed the title Question: How can we get parent object in directive function get parent object in directive function Aug 21, 2018
@vektah
Copy link
Collaborator

vektah commented Aug 21, 2018

Another interesting use case is in mutations as @jekaspekas suggests:

type Mutation {
   updateThing(thingId: ID, newValue: String): Stuff @hasRole(requires: OWNER)
}

you never want to run the update, you kind of want to do the select, then run the directive, and then update.

It makes me really wish mutations were in the graph, like:

{ thing(id: 123) { mutateValue(newValue: "asdf") } }

And then you would have the parent object to check again.

@vvakame
Copy link
Collaborator Author

vvakame commented Aug 21, 2018

IMHO is here ... #301

so, it is the implementer's responsibility to which evaluate first about next func or directive.
Certainly, there is a common pitfall, so I have to be careful. 😺

@vektah vektah mentioned this issue Aug 24, 2018
2 tasks
@ibantoo
Copy link

ibantoo commented Jul 26, 2020

hey @vvakame @vektah trying to implement this on my directive, but the obj always nil, here is schema

directive @Object on OBJECT

type DocumentResult @Object {
  id: ObjectID
  folderName: String!
  versions: [Map!]!
  createdBy: String!
  createdAt: ObjectDateTime!
  updatedAt: ObjectDateTime
}

and here the config.Directive

config.Directives.Object = func(ctx context.Context, obj interface{}, next graphql.Resolver) (res interface{}, err error) {
		getContext := graphql.GetFieldContext(ctx)
		getFieldError := graphql.GetFieldErrors(ctx, getContext)
		getError := graphql.GetErrors(ctx)

		log.Printf("\n this is parent: %v, ini args: %v, this is result: %v, this is field name: %v \n", getContext.Parent, getContext.Args, getContext.Field.Name, getContext.Object)

		log.Printf("\n getttt errrro .......>> %v \n", getFieldError.Error())

		log.Printf("\n get errro again ====> %v \n", getError.Error())

		log.Printf("this is obj %v", obj)

		return next(ctx)
}

I was look and tried the type-system-extension example and run that project, it was logged the Object, but after I follow to implement it on my app, it doesn't log the Obj
something wrong with schema ??

@yohanyflores
Copy link

hey @vvakame @vektah trying to implement this on my directive, but the obj always nil, here is schema

directive @Object on OBJECT

type DocumentResult @Object {
  id: ObjectID
  folderName: String!
  versions: [Map!]!
  createdBy: String!
  createdAt: ObjectDateTime!
  updatedAt: ObjectDateTime
}

and here the config.Directive

config.Directives.Object = func(ctx context.Context, obj interface{}, next graphql.Resolver) (res interface{}, err error) {
		getContext := graphql.GetFieldContext(ctx)
		getFieldError := graphql.GetFieldErrors(ctx, getContext)
		getError := graphql.GetErrors(ctx)

		log.Printf("\n this is parent: %v, ini args: %v, this is result: %v, this is field name: %v \n", getContext.Parent, getContext.Args, getContext.Field.Name, getContext.Object)

		log.Printf("\n getttt errrro .......>> %v \n", getFieldError.Error())

		log.Printf("\n get errro again ====> %v \n", getError.Error())

		log.Printf("this is obj %v", obj)

		return next(ctx)
}

I was look and tried the type-system-extension example and run that project, it was logged the Object, but after I follow to implement it on my app, it doesn't log the Obj something wrong with schema ??

The same situation appears to me. The directive is never called.

But if it works on type-system-extesion

Does anyone know how to solve it?

@yohanyflores
Copy link

I just saw it, it only works on master branch

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants