Lambda Server in Go

Hi!

After playing around with DGraph, I really came to like its concepts of doing things. Especially in conjunction with the Lambda-Server it provides a lot of benefits.

Unfortunately the provided Lambda-Server did not fulfill all my needs. Due to the fact that you have to use webpack to use external libraries and some of these libraries not supporting webpack (e.g. firebase-admin), using the lambda server became cumbersome, as it would need another external service to serve the additional functionality.

That is why I sat down and wrote an alternative in Go dgraph-lambda-go.
It’s my first official Go project, so I hope I did not miss on any best practices in the development process. As of now I would say it’s in a Testing-Phase. I’m using it in my own project and it fulfilled all my needs for now, but you might have some more complex needs.

Features

  • Resolvers on Fields, Queries and Mutations
  • Global Middleware
  • Middleware on specific resolvers
  • Webhooks

Future plan:

  • Middleware on resolver groups
  • Generate resolvers and structs from the DGraph graphql schema.

How does it work (Basically Readme)

  • To install dgraph-lambda-go run the command go get github.com/schartey/dgraph-lambda-go in your project directory.
  • Then initialize the project by running go run github.com/schartey/dgraph-lambda-go init.

Implement resolver functions and middleware

On startup this library provides a resolver.

err := api.RunServer(func(r *resolver.Resolver, gql *graphql.Client, dql *dgo.Dgraph) {

})

Within this startup function you can provide resolver functions and middleware. It’s best to first define the input and output structs for the resolver. For example CreateUserInput and UserData struct


type CreateUserInput struct {
	Username string `json:"username"`
}

type UserData struct {
	Id              string `json:"id"`
	Username        string `json:"username"`
	ComplexProperty string `json:"complexProperty"`
}

Then you can provide a resolver for fields, queries and mutations like this

// Field Resolver
r.ResolveFunc("UserData.complexProperty", func(ctx context.Context, input []byte, parents []byte, ah resolver.AuthHeader) (interface{}, error) {
    var userParents []UserData
    json.Unmarshal(parents, &userParents)

    var complexProperties []string
    for _, userParent := range userParents {
        complexProperties = append(complexProperties, fmt.Sprintf("VeryComplex - %s", userParent.Id))
    }

    return complexProperties, nil
})

// Query/Mutation Resolver
r.ResolveFunc("Mutation.createUser", func(ctx context.Context, input []byte, parents []byte, ah resolver.AuthHeader) (interface{}, error) {
    var createUserInput CreateUserInput
    json.Unmarshal(input, &createUserInput)

    // Do Something
    user := UserData{
        Id:       "0x1",
        Username: createUserInput.Username,
    }
    return user, nil
})

You can also provide global middleware, as well as middleware on specific resolvers

r.Use(func(hf resolver.HandlerFunc) resolver.HandlerFunc {
    return func(c context.Context, b []byte, parents []byte, ah resolver.AuthHeader) (interface{}, error) {
        // Authorization.
        // Add user to context
        return hf(c, b, parents, ah)
    }
})

r.UseOnResolver("Mutation.createUser", func(hf resolver.HandlerFunc) resolver.HandlerFunc {
    return func(c context.Context, b []byte, parents []byte, ah resolver.AuthHeader) (interface{}, error) {
        // Validation .
        return b, nil
    }
})

Finally a webhook resolver is also provided on Types

r.WebHookFunc("UserData", func(ctx context.Context, event resolver.Event) error {
    // Send E-Mail
    return nil
})

Additionally a graphql and dql client connected to the dgraph server are provided, so you can query and make changes to the databases.

If you could provide some feedback I would appreciate it. As said, dgraph-lambda-go is my first open source project and I hope you like it.

Best regards

6 Likes

I like it! Thank you for your work! I also started my own golang implementation of lambda (and use it in production), but I think I’ll abandon that and will just contribute to your project instead when I have time.

That being said, I think that resolver/type generation from dgraph schema would be very useful for a next step and spare us a decent amount of boilerplate code.

Also it would be great to add custom types to func(r *resolver.Resolver, gql *graphql.Client, dql *dgo.Dgraph) { ... }. For instance, I’m using an auto-generated graphql client for easy dgraph access that I’d like to use (instead graphql.Client).

2 Likes
5 Likes

Thank you!
I am already working on resolver/type generation. Type generation seems to be working (on gen branch), but I need to do some extensive testing. I will do resolver generation next.

About passing custom graphql and dql clients I was actually thinking about getting rid of the clients overall, so people can provide their own. I only added those, because the original lambda server did so too. You can use your own client anyways.

Thanks for your feedback!

1 Like

Are you aware of gqlgen? You could integrate their type generation easily. It’s battle-tested.

1 Like

Thanks for your suggestion. I was a little busy the last few days, but I think you are right with using gqlgen for this and started implementation for it on gqlgen branch. It’s a way nicer and easier solution, but there are some issues left that have to be resolved. I think the new solution using gqlgen will be ready by next week.

1 Like

Hi guys!
I have been really occupied in the last months and wasn’t able to make a lot of changes. But now I’m glad to announce I was able to build a lambda server generator (using gqlgen as inspiration).
Unfortunately I was not able to create a simple plugin for gqlgen due to some issues with validation.

Following are obviously breaking changes.
Unfortunately I can’t edit the original post but here is a short run-through of what to expect.

dgraph-lambda-go is now able to generate resolvers just based on your schema that you use for your dgraph server.

  • To install dgraph-lambda-go run the command go get github.com/schartey/dgraph-lambda-go in your project directory.
  • Then initialize the project by running go run github.com/schartey/dgraph-lambda-go init.
  • Edit the lambda.yaml file. Important: Change the path to your graphql schema. Everything else can stay as is.
  • Generate types and resolvers go run github.com/schartey/dgraph-lambda-go generate
  • Implement your lambda resolvers
  • Run your server go run server.go

The generator will generate resolvers for

  • Fields
  • Queries
  • Mutations
  • Webhooks
  • Middleware

Also you can now use a graphql and dql client of your choice by adding it to the Resolver struct.

Example:

Schema:

type User @lambdaOnMutate(add: true, update: false, delete: true) {
    id: ID!
    username: String!
    """
    @middleware(["auth"])
    """
    secret: String @lambda
}

input CreateUserInput {
    username: String!
    secret: String
}

type Query {
    """
    @middleware(["auth"])
    """
    randomUser(seed: String): User @lambda
}

type Mutation {
    """
    @middleware(["auth"])
    """
    createUser(input: CreateUserInput!): User @lambda
}

Generated Resolvers with example response:
FieldResolver

func (f *FieldResolver) User_secret(ctx context.Context, parents []string, authHeader api.AuthHeader) ([]string, error) { 
	var secrets []string
    for _, userParent := range userParents {
        secrets = append(secrets, fmt.Sprintf("Secret - %s", userParent.Id))
    }
    return secrets, nil
}

Query Resolver

func (q *QueryResolver) Query_randomUser(ctx context.Context, seed string, authHeader api.AuthHeader) (*model.User, error) { 
	nameGenerator := namegenerator.NewNameGenerator(seed)
    name := nameGenerator.Generate()

    user := &model.User{
        Id:       "0x1",
        Username: name,
    }
    return user, nil
}

Mutation Resolver

func (q *MutationResolver) Mutation_createUser(ctx context.Context, input *model.CreateUserInput, authHeader api.AuthHeader) (*model.User, error) {
    user := &User{
        Id:       "0x1",
        Username: createUserInput.Username,
    }
	return user, nil
}

Webhook Resolver

func (w *WebhookResolver) Webhook_User(ctx context.Context, event api.Event) error {
    // Send Email
	return nil
}

Middleware

func (m *MiddlewareResolver) Middleware_auth(md *api.MiddlewareData) error {
    // Check Token
    valid := true //false
    if valid {
    	md.Ctx = context.WithValue(md.Ctx, "logged_in", "true")
        return nil
    } else {
        return errors.New("Token invalid!")
    }
}

Inject custom dependencies

Typically you want to at least inject a graphql/dql client into your resolvers. To do so just add your client to the Resolver struct

// Add objects to your desire
type Resolver struct {
    Dql *dgo.Dgraph
}

and pass the client to the executor in your generated server.go file

dql := NewDqlClient()
resolver := &resolvers.Resolver{ Dql: dql}
executer := generated.NewExecuter(resolver)

Then you can access the client in your resolvers like this

func (q *QueryResolver) Query_randomUser(ctx context.Context, seed string, authHeader api.AuthHeader) (*model.User, error) {
    // Oversimplified
    user, err := s.dql.NewTxn().Do(ctx, req)
	if err != nil {
		return nil, &api.LambdaError{ Underlying: errors.New("User not found"), Status: api.NOT_FOUND}
	}
    return user, nil
}

Be aware that I did some basic testing and will also be using this solution for my own projects, but there might be some issues that I’m still missing. I will try to add automated tests and do some more manual testing.

Let me know about features you would like to see.
If you have a try, please leave some feedback, so I can improve whichever issues you may have.

2 Likes