Git Product home page Git Product logo

elk's Introduction

elk


Important

elk has been superseded by the extensions entoas and ogent and this package has been discontinued as resources are now directed at the two mentioned extensions.

This package provides an extension to the awesome entgo.io code generator.

elk can do two things for you:

  1. Generate a fully compliant, extendable OpenAPI specification file to enable you to make use of the Swagger Tooling to generate RESTful server stubs and clients.
  2. Generate a ready-to-use and extendable server implementation of the OpenAPI specification. The code generated by elk uses the Ent ORM while maintaining complete type-safety and leaving reflection out of sight.

⚠️ This is work in progress: The API may change without further notice!

This package depends on Ent, an ORM project for Go. To learn more about Ent, how to connect to different types of databases, run migrations or work with entities head over to their documentation.

Getting Started

The first step is to add the elk package to your project:

go get github.com/masseelch/elk

elk uses the Ent Extension API to integrate with Ent’s code-generation. This requires that we use the entc (ent codegen) package as described here. Follow the next four steps to enable it and to configure Ent to work with the elk extension:

  1. Create a new Go file named ent/entc.go and paste the following content:
// +build ignore

package main

import (
	"log"

	"entgo.io/ent/entc"
	"entgo.io/ent/entc/gen"
	"github.com/masseelch/elk"
)

func main() {
	ex, err := elk.NewExtension(
		elk.GenerateSpec("openapi.json"),
		elk.GenerateHandlers(),
	)
	if err != nil {
		log.Fatalf("creating elk extension: %v", err)
	}
	err = entc.Generate("./schema", &gen.Config{}, entc.Extensions(ex))
	if err != nil {
		log.Fatalf("running ent codegen: %v", err)
	}
}
  1. Edit the ent/generate.go file to execute the ent/entc.go file:
package ent

//go:generate go run -mod=mod entc.go
  1. (Only required if server generation is enabled) elk uses some external packages in its generated code. Currently, you have to get those packages manually once when setting up elk:
go get github.com/mailru/easyjson github.com/go-chi/chi/v5 go.uber.org/zap
  1. Run the code generator:
go generate ./...

In addition to the files Ent would normally generate, another directory named ent/http and a file named openapi.json was created. The ent/http directory contains the code for the elk-generated HTTP CRUD handlers while openapi.json contains the OpenAPI Specification. Feel free to have a look at this example spec file and the implementing server code.

If you want to generate a client matching the spec as well, you can user the following function and call it after generating the spec in entc.go

package main

import (
	"io/ioutil"
	"log"
	"os"
	"path/filepath"

	"github.com/deepmap/oapi-codegen/pkg/codegen"
	"github.com/deepmap/oapi-codegen/pkg/util"
)

func generateClient() {
	swagger, err := util.LoadSwagger("./openapi.json")
	if err != nil {
		log.Fatalf("Failed to load swagger %v", err)
	}

	generated, err := codegen.Generate(swagger, "stub", codegen.Options{
		GenerateClient: true,
		GenerateTypes:  true,
		AliasTypes:     true,
	})
	if err != nil {
		log.Fatalf("generaring client failed %s", err);
	}

	dir := filepath.Join(".", "stub")
	stub := filepath.Join(".", "stub", "http.go")
	perm := os.FileMode(0777)
	if err := os.MkdirAll(dir, perm); err != nil {
		log.Fatalf("error creating dir: %s", err)
	}

	if err := ioutil.WriteFile(stub, []byte(generated), perm); err != nil {
		log.Fatalf("error writing generated code to file: %s", err)
	}
}

Setting up a server

This section guides you to a very simple setup for an elk-powered Ent. The following two files define the two schemas Pet and User with a Many-To-One relation: A Pet belongs to a User, and a User can have multiple Pets.

ent/schema/pet.go

package schema

import (
	"entgo.io/ent"
	"entgo.io/ent/schema/edge"
	"entgo.io/ent/schema/field"
)

// Pet holds the schema definition for the Pet entity.
type Pet struct {
	ent.Schema
}

// Fields of the Pet.
func (Pet) Fields() []ent.Field {
	return []ent.Field{
		field.String("name"),
	}
}

// Edges of the Pet.
func (Pet) Edges() []ent.Edge {
	return []ent.Edge{
		edge.From("owner", User.Type).
			Ref("pets").
			Unique(),
	}
}

ent/schema/user.go

package schema

import (
	"entgo.io/ent"
	"entgo.io/ent/schema/edge"
	"entgo.io/ent/schema/field"
)

// User holds the schema definition for the User entity.
type User struct {
	ent.Schema
}

// Fields of the User.
func (User) Fields() []ent.Field {
	return []ent.Field{
		field.String("name"),
	}
}

// Edges of the User.
func (User) Edges() []ent.Edge {
	return []ent.Edge{
		edge.To("pets", Pet.Type),
	}
}

After regenerating the code you can spin up a runnable server with the below main function:

package main

import (
	"context"
	"log"
	"net/http"

	"<your-project>/ent"
	elk "<your-project>/ent/http"

	"github.com/go-chi/chi/v5"
	_ "github.com/mattn/go-sqlite3"
	"go.uber.org/zap"
)

func main() {
	// Create the ent client. This opens up a sqlite file named elk.db.
	c, err := ent.Open("sqlite3", "./elk.db?_fk=1")
	if err != nil {
		log.Fatalf("failed opening connection to sqlite: %v", err)
	}
	defer c.Close()
	// Run the auto migration tool.
	if err := c.Schema.Create(context.Background()); err != nil {
		log.Fatalf("failed creating schema resources: %v", err)
	}
	// Start listen to incoming requests.
	if err := http.ListenAndServe(":8080", elk.NewHandler(c, zap.NewExample())); err != nil {
		log.Fatal(err)
	}
}

Start the server:

go run -mod=mod main.go

Congratulations! You now have a running server serving the Pets API. The database is still empty though. the following two curl requests create a new user and adds a pet, that belongs to that user.

curl -X 'POST' -H 'Content-Type: application/json' -d '{"name":"Elk"}' 'localhost:8080/users'
{
  "id": 1,
  "name": "Elk"
}
curl -X 'POST' -H 'Content-Type: application/json' -d '{"name":"Kuro","owner":1}' 'localhost:8080/pets'
{
  "id": 1,
  "name": "Kuro"
}

The response data on the creation operation does not include the User the new Pet belongs to. elk does not include edges in its output by default. You can configure elk to render edges using a feature called serialization groups.

Serialization Groups

elk by default includes every field of a schema in an endpoints output and excludes fields. This behaviour can be changed by using serialization groups. You can configure elk what serialization groups to request on what endpoint using a elk.SchemaAnnotation. With a elk.Annotation you configure what fields and edges to include. elk follows the following rules to determine if a field or edge is included or not:

  • If no groups are requested all fields are included and all edges are excluded
  • If a group x is requested all fields with no groups and fields with group x are included. Edges with x are eager loaded and rendered.

Change the previously mentioned schemas and add serialization groups:

ent/schema/pet.go

package schema

import (
	"entgo.io/ent"
	"entgo.io/ent/schema"
	"entgo.io/ent/schema/edge"
	"entgo.io/ent/schema/field"
	"github.com/masseelch/elk"
)

// Pet holds the schema definition for the Pet entity.
type Pet struct {
	ent.Schema
}

// Fields of the Pet.
func (Pet) Fields() []ent.Field {
	return []ent.Field{
		field.String("name"),
	}
}

// Edges of the Pet.
func (Pet) Edges() []ent.Edge {
	return []ent.Edge{
		edge.From("owner", User.Type).
			Ref("pets").
			Unique().
			// render this edge if one of 'pet:read' or 'pet:list' is requested.
			Annotations(elk.Groups("pet:read", "pet:list")),
	}
}

// Annotations of the Pet.
func (Pet) Annotations() []schema.Annotation {
	return []schema.Annotation{
		// Request the 'pet:read' group when rendering the entity after creation.
		elk.CreateGroups("pet:read"),
		// You can request several groups per endpoint.
		elk.ReadGroups("pet:list", "pet:read"),
	}
}

ent/schema/user.go

package schema

import (
	"entgo.io/ent"
	"entgo.io/ent/schema/edge"
	"entgo.io/ent/schema/field"
	"github.com/masseelch/elk"
)

// User holds the schema definition for the User entity.
type User struct {
	ent.Schema
}

// Fields of the User.
func (User) Fields() []ent.Field {
	return []ent.Field{
		field.String("name").
			// render this field only if no groups or the 'owner:read' groups is requested.
			Annotations(elk.Groups("owner:read")),
	}
}

// Edges of the User.
func (User) Edges() []ent.Edge {
	return []ent.Edge{
		edge.To("pets", Pet.Type),
	}
}

After regenerating the code and restarting the server elk renders the owner if you create a new pet.

curl -X 'POST' -H 'Content-Type: application/json' -d '{"name":"Martha","owner":1}' 'localhost:8080/pets'
{
  "id": 2,
  "name": "Martha",
  "owner": {
    "id": 1,
    "name": "Elk"
  }
}

Validation

elk supports the validation feature of Ent. For demonstration extend the above Pet schema:

package schema

import (
	"errors"
	"strings"

	"entgo.io/ent"
	"entgo.io/ent/schema"
	"entgo.io/ent/schema/edge"
	"entgo.io/ent/schema/field"
	"github.com/masseelch/elk"
)

// Pet holds the schema definition for the Pet entity.
type Pet struct {
	ent.Schema
}

// Fields of the Pet.
func (Pet) Fields() []ent.Field {
	return []ent.Field{
		field.Int("age").
			// Validator will only be called if the request body has a 
			// non nil value for the field 'age'.
			Optional().
			// Works for built-in validators.
			Positive(),
		field.String("name").
			// Works for built-in validators.
			MinLen(3).
			// Works for custom validators.
			Validate(func(s string) error {
				if strings.ToLower(s) == s {
					return errors.New("name must begin with uppercase")
				}
				return nil
			}),
		// Enums are validated against the allowed values.
		field.Enum("color").
			Values("red", "blue", "green", "yellow"),
	}
}

// Edges of the Pet.
func (Pet) Edges() []ent.Edge {
	return []ent.Edge{
		edge.From("owner", User.Type).
			Ref("pets").
			Unique().
			// Works with edge validation.
			Required().
			// render this edge if one of 'pet:read' or 'pet:list' is requested.
			Annotations(elk.Groups("pet:read", "pet:list")),
	}
}

// Annotations of the Pet.
func (Pet) Annotations() []schema.Annotation {
	return []schema.Annotation{
		// Request the 'pet:read' group when rendering the entity after creation.
		elk.CreateGroups("pet:read"),
		// You can request several groups per endpoint.
		elk.ReadGroups("pet:list", "pet:read"),
	}
}

Sub Resources

elk provides first level sub resource handlers for all your entities. With previously set up server, run the following:

curl 'localhost:8080/pets/1/owner'

You'll get information about the Owner of the Pet with the id 1. elk uses elk.SchemaAnnotation.ReadGroups for a unique edge and elk.SchemaAnnotation.ListGroups for a non-unique edge.

Pagination

elk paginates all list endpoints. This is valid for both resource and sub-resources routes.

curl 'localhost:8080/pets?page=2&itemsPerPage=1'
[
  {
    "id": 2,
    "name": "Martha"
  }
]

Configuration

elk lets you decide what endpoints you want it to generate by the use of generation policies. You can either expose all routes by default and hide some you are not interested in or exclude all routes by default and only expose those you want generated:

ent/entc.go

package main

import (
	"log"

	"entgo.io/ent/entc"
	"entgo.io/ent/entc/gen"
	"github.com/masseelch/elk"
	"github.com/masseelch/elk/policy"
	"github.com/masseelch/elk/spec"
)

func main() {
	ex, err := elk.NewExtension(
		elk.GenerateSpec("openapi.json"),
		elk.GenerateHandlers(),
		// Exclude all routes by default.
		elk.DefaultHandlerPolicy(elk.Exclude),
	)
	if err != nil {
		log.Fatalf("creating elk extension: %v", err)
	}
	err = entc.Generate("./schema", &gen.Config{}, entc.Extensions(ex))
	if err != nil {
		log.Fatalf("running ent codegen: %v", err)
	}
}

ent/schema/user.go

package schema

import (
	"entgo.io/ent"
	"entgo.io/ent/schema"
	"entgo.io/ent/schema/edge"
	"entgo.io/ent/schema/field"
	"github.com/masseelch/elk"
)

// User holds the schema definition for the User entity.
type User struct {
	ent.Schema
}

// Annotations of the User.
func (User) Annotations() []schema.Annotation {
	return []schema.Annotation{
		// Generate creation and read endpoints.
		elk.Expose(elk.Create, elk.Read),
	}
}

For more information about how to configure elk and what it can do have a look at the docs integration test setup .

Known Issues and Outlook

  • elk does currently only work with JSON. It is relatively easy to support XML as well and there are plans to provide conditional XML / JSON parsing and rendering based on the Content-Type and Accept headers.

  • The generated code does not have very good automated tests yet.

Contribution

elk has not reach its first release yet but the API can be considered somewhat stable. I welcome any suggestion or feedback and if you are willing to help I'd be very glad. The issues tab is a wonderful place for you to reach out for help, feedback, suggestions and contribution.

elk's People

Contributors

awinterman avatar dependabot[bot] avatar masseelch avatar stevecastle avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

elk's Issues

How do you deal with sanitization?

Let's take some Post that has a content text field.

The client can pass html or markdown
You can't trust this input and it needs to be sanitized, with https://github.com/microcosm-cc/bluemonday for instance.

What options are available, short of manually modifying the generated http dir?

Is this in scope of elk or should this be handled by ent?

Generated code did not import github.com/masseelch/render

Hi, I just did the same as your guide here: https://entgo.io/blog/2021/07/29/generate-a-fully-working-go-crud-http-api-with-ent/.
But when I did the step: Next, start the server: go run -mod=mod main.go

I found out that some generated go files in elk-example/ent/http: create.go, delete.go, read.go, update.go, list.go did not import github.com/masseelch/render so after typing above command, it returned errors:

ent/http/create.go:28:3: undefined: render
ent/http/create.go:42:3: undefined: render
ent/http/create.go:53:4: undefined: render
ent/http/create.go:56:4: undefined: render
ent/http/create.go:60:12: undefined: sheriff
ent/http/create.go:66:3: undefined: render
ent/http/create.go:70:2: undefined: render
ent/http/create.go:84:3: undefined: render
ent/http/create.go:92:3: undefined: render
ent/http/create.go:103:4: undefined: render
ent/http/create.go:103:4: too many errors

Can you fix this one? or did I miss something?

Gui

Gio instead of flutter is my own preference.

used to use flutter and switched to gio which is golang based and is architecturally the same as flutter is how it fundamentally works.

would be easier to support also.

checkout the gio examples !

component is a good starter: https://github.com/gioui/gio-example/tree/main/x/component

easyjson complains when generating code to a different package.

Getting easyjson-bootstrap.go:14:3: package db/http is not in GOROOT (/usr/local/go/src/db/http) when generating the code to a different package.

Of course I'm using gomod.

ex, _ := elk.NewExtension(
	elk.GenerateSpec("openapi.json"),
	elk.GenerateHandlers(),
)

err = entc.Generate("./schema", &gen.Config{
+	Target:  "db",
+	Package: "db",
	IDType:  &field.TypeInfo{Type: field.TypeString},
	Features: []gen.Feature{
		gen.FeaturePrivacy,
		gen.FeatureEntQL,
	},
}, entc.Extensions(ex))

It might be related to mailru/easyjson#293

If an entity type starts with the letter "i" $.Receiver is "i" breaking Code Gen

# templates/response.tmpl
func NewIndividualListResponse(i []*ent.Individual) IndividualListResponse {
	r := make(IndividualListResponse, len(i))
	for i := range i {
		r[i] = struct {
			ID      int    `json:"id,omitempty"`
			IRI     string `json:"IRI,omitempty"`
			Hash    string `json:"hash,omitempty"`
			Comment string `json:"comment,omitempty"`
		}{
			ID:      i[i].ID,
			IRI:     i[i].IRI,
			Hash:    i[i].Hash,
			Comment: i[i].Comment,
		}
	}
	return r
}

Entity struct argument name is shadowed by iterator in loop if $.Receiver is "i" This happens if the Entity Type starts with that letter.

no OAS-type exists for field.JSON fields

Following along with the blog on an existing schema got me these results:

go generate ./ent/
2021/09/13 09:16:58 running ent codegen: no OAS-type exists for "*[]schema.Source"
exit status 1
ent/generate.go:5: running "go": exit status 1

In my schema Source is defined as

field.JSON("sources", &[]Source{}).Optional(),

Are JSON fields not supported by this plugin? If not what are the chances that they will/could be ?

Latest version of everything (Go, Ent, Elk)

Open api async ?

There are now golang reps that provide open api that works with standard http request / response but also asynchronous situations too.

Async can be anything you want . Emit an emit when some record collection changes in your db. Useful for clients to stay up to date.

It makes for an alternative to graphql subscriptions .

If you want some URLs of golang repos that implement open api async let me know .

If would be cool to be able to use ent this way

missing required field "name"

{"level":"error","msg":"could not create item","handler":"ItemHandler","method":"Create","error":"ent: missing required field "name""}

	var d ItemCreateRequest
	if err := easyjson.UnmarshalFromReader(r.Body, &d); err != nil {
		l.Error("error decoding json", zap.Error(err))
		BadRequest(w, "invalid json string")
		return
	}

easyjson.UnmarshalFromReader can't unmarshal from body properly

Problem testing the ent-elk crud example

Hi,

I was following https://entgo.io/blog/2021/07/29/generate-a-fully-working-go-crud-http-api-with-ent

When i try to run the server i get this error

go run -mod=mod main.go

# command-line-arguments
.\main.go:32:26: "elk-example/ent/http".NewPetHandler(c, l).Mount undefined (type *"elk-example/ent/http".PetHandler has no field or method Mount)
.\main.go:32:36: undefined: "elk-example/ent/http".PetRoutes

Here is the github repo https://github.com/Exnoic/ent-elk-example

Please tell me if you need any other information!

Best regards,
Henrik

Curl add user

following instructions on how to add user to db and I'm getting this error. Not sure what I'm doing wrong.

{"level":"error","msg":"could not create profile","handler":"ProfileHandler","method":"Create","error":"ent: missing required field 
\"username\""}

Using

curl -X 'POST' -H 'Content-Type: application/json' -d '{"username":"Elk","email":"[email protected]","password":"Elk123"}' 'localhost:3000/profiles

Tried postman, same error. Profile looks like this:

func (Profile) Fields() []ent.Field {
	return []ent.Field{
		field.String("username").
			Immutable(),
		field.String("email").
			Unique(),
		field.String("password").
			Sensitive(),
	}
}
func (Profile) Edges() []ent.Edge {
	return []ent.Edge{
		edge.To("customers", Customer.Type),
		edge.To("suppliers", Supplier.Type),
		edge.To("admins", Admin.Type),
	}
}

Does not generate when using other types for IDs

I use UUIDs for the id field. But his results in the following error when running the main.go.

running ent codegen: execute template "http/relations": template: relations.tmpl:30:67: executing "http/relations" at <zapField $n.ID>: error calling zapField: elk: invalid ID-Type "uuid.UUID"

Go module indirect dependency

Go Mod alway complain about non-usage module github.com/masseelch/elk. But very times, I ran go generate ./... it always download the module.

Untitled.mov

Missing zap import?

I'm trying to use elk according to the ent blog post.
I'm getting an error that zap is unknown. I'm guessing you need to add "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap" to the imports?

Update Error Message / HTTP Folder

Think there might be another issue or something, as I've added Positive(), method to a field and is showing an error in http/update.go file. Removed it and it goes away. Issue also with Username which I do have but saying its undefined in the update file. I've pushed the repository to github so you can do a pull request on it and let us know.

responseView function not getting passed correct annotations from template for Serialization Groups

The issue I am having is when generating response types (in list, read, create etc), the responseView function is not getting passed any groups from the template and hashing to the wrong type name, so none of my API endpoints with group annotations return the right fields.

I haven't quite figured out how to fix this, but maybe you would know right away.

On the following line, I believe that the annotation name should be ElkSchema instead of Elk, but for some reason when I make that change it doesn't seem to work either. I may be missing or misunderstanding something about how this works.

easyjson.MarshalToHTTPResponseWriter(New{{ (responseView $n $n.Annotations.Elk.ReadGroups).ViewName }}(e), w)

Provide elk command

Instead of having to create a file locally, it would be awesome to have a main.go file for the elk generator checked in: ./cmd/elk. It could be improved slightly by accepting some command line argument (for the schema output path, etc.)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.