Wednesday, May 19, 2021

Journey Of A Request At 99

This is a summary of how 99d’s infrastructure works holistically for a single component that fires a request when a button is pressed. The UI I’m using in this example is <DismissJobButton /> that is defined within the pages/browse-projects directory. Let’s walk through the steps of what happens:

  1. When the component renders we invoke Apollo Clients useMutation hook which returns a function that will fire onSubmit when the dismiss button is pressed

  2. Note here that we don't directly invoke useMutation, instead we use a library called GraphQL Code Generator to generate custom hooks, the command used to generate this code is:

nx run spa:graphql

When exactly can you run this generator?

The generator looks for the schema files in backend, so the type generation can be run after updating the .graphql files! See here.

  1. The GraphQL mutation for dismissing a job sits alongside the component, this code strongly types the input that is passed to the mutation and specifies what values are returned from the mutation

  2. The mutation input is sent to a resolver in our BFF (Backend For Frontend), this is the backend directory that is contained within the frontend repo, the code written within the backend defines a GraphQL schema and defines resolvers for mutations and queries that are then written in backend/graph, for example DismissJob is a function that we define in job.go and fires off a request to the DismissJob RPC, this is an example of that call

func (m *mutationResolver) DismissJob(ctx context.Context, input models.DismissJobInput) (*createpb.JobForDesigner, error) {
	res, err := m.Clients.Job.DismissJob(ctx, &createpb.DismissJobRequest{
		Actor:   actorFromContext(ctx),
		JobId:   input.ID,
		Reasons: input.Reasons,
	})

	if err != nil {
		return nil, err
	}

	return res.Job, nil
}
  1. If the resolvers are defined you can use gqlgen to turn your GraphQL types, mutations and queries into go structs, functions and interfaces, this ends up in generated.go, the command used to generate this code is
go generate ./...

Where are models defined?

gqlgen will generate models for the types defined in the graphql schema files. For example, the input that is passed to the mutation resolver is defined in the backend/models directory in models_gen.go. See code here. There is also some config around using the @goModel directive but will come back to this another time.

What is the code in generated.go is used for?

This file contains the entire GraphQL server code that can used to attach to an endpoint. You can see it being configured here.

  1. Before I mentioned the RPC function being invoked in the BFF however we also need to define this, the RPC framework we use is called Twirp, in this instance we're creating new jobs for designers (or dismissing jobs), therefore this RPC interface lives within the create service

  2. Within the rpc/proto directory we have a job.proto file, this file defines a DismissJob RPC

rpc DismissJob(DismissJobRequest) returns (DismissJobResponse);

and also maps out what parameters a request can receive and a response can send.

  1. We then have some Go server code in the create service that needs to align to this RPC definition, this is defined in rpc/job and there is a job.go file with instance methods for the Service struct, one of which is DismissJob
func (s *Service) DismissJob(ctx context.Context, req *createpb.DismissJobRequest) (*createpb.DismissJobResponse, error) {
	job, err := s.addJobInterest(ctx, model.NewJobInterestParams{
		EffectiveUserID: int(req.Actor.EffectiveUserId),
		RealUserID:      int(req.Actor.RealUserId),
		JobID:           req.JobId,
		Type:            model.JobInterestTypeDismiss,
		Data:            castSdkDismissJobDataToDismissJobData(req.Reasons),
	}, req.Actor)

	if err != nil {
		return nil, err
	}

	return &createpb.DismissJobResponse{Job: castJobForDesignerToSdk(job)}, nil
}

this function takes in parameters from the request and calls a function which executes some SQL and updates the DB, we then send back a response that contains the values we defined as a message in the RPC.

How does the mapper and SQL calls actually work?

It might be good to familiarize yourself with how to perform DB queries in Go.

  1. If there are no errors the resolver returns a job, that job is currently not being used in the <DismissJob /> component