elk

package module
v0.0.0-...-2e07358 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 2, 2023 License: MIT Imports: 20 Imported by: 0

README

elk

feature

add list api support filter by column using query string

just replace entc.go

github.com/masseelch/elk => github.com/chestarss/elk

list.api add such code in template

   // dynamic filter by query string
            queryStringMap := r.URL.Query()
            for qs := range queryStringMap {
                for _, col := range {{ $n.Name | lower }}.Columns {
                    if col == qs {
                        q = q.Where(sql.FieldEQ(qs, queryStringMap.Get(qs)))
                        break
                    }
                }
            }

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.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (

	// Funcs contains the extra template functions used by elk.
	Funcs = template.FuncMap{
		"contains":        contains,
		"edges":           edges,
		"filterEdges":     filterEdges,
		"filterNodes":     filterNodes,
		"imports":         imports,
		"kebab":           strcase.KebabCase,
		"needsValidation": needsValidation,
		"nodeOperations":  nodeOperations,
		"pluralize":       rules.Pluralize,
		"view":            newView,
		"views":           newViews,
		"stringSlice":     stringSlice,
		"xextend":         xextend,
		"zapField":        zapField,
	}
	// HTTPTemplate holds all templates for generating http handlers.
	HTTPTemplate = gen.MustParse(gen.NewTemplate("elk").Funcs(Funcs).ParseFS(templateDir, "template/http/*.tmpl"))
)

Functions

func EasyJSONGenerator

func EasyJSONGenerator(c EasyJsonConfig) gen.Hook

Types

type Annotation

type Annotation struct {
	// Groups holds the serialization groups to use on this field / edge.
	Groups serialization.Groups
	// MaxDepth tells the generator the maximum depth of this field when there is a cycle possible.
	MaxDepth uint
	// Expose defines if a read/list for this edge should be generated.
	Expose Policy
	// OpenAPI spec example value on schema fields.
	Example interface{}
	// OpenAPI security object for the read/list operation on this edge.
	Security spec.Security
}

Annotation annotates fields and edges with metadata for templates.

func Example

func Example(v interface{}) Annotation

Example returns an example annotation.

func ExcludeEdge

func ExcludeEdge() Annotation

ExcludeEdge returns a Exclude annotation.

func ExposeEdge

func ExposeEdge() Annotation

ExposeEdge returns a Expose annotation.

func Groups

func Groups(gs ...string) Annotation

Groups returns a groups annotation.

func MaxDepth

func MaxDepth(d uint) Annotation

MaxDepth returns a max depth annotation.

func (*Annotation) Decode

func (a *Annotation) Decode(o interface{}) error

Decode from ent.

func (*Annotation) EnsureDefaults

func (a *Annotation) EnsureDefaults()

EnsureDefaults ensures defaults are set.

func (Annotation) Merge

Merge implements ent.Merger interface.

func (Annotation) Name

func (Annotation) Name() string

Name implements ent.Annotation interface.

type Config

type Config struct {
	// HandlerPolicy defines the default policy for handler generation.
	// It is used if no policy is set on a (sub-)resource.
	// Defaults to policy.Expose.
	HandlerPolicy Policy
}

func (*Config) Decode

func (c *Config) Decode(o interface{}) error

Decode from ent.

func (Config) Name

func (c Config) Name() string

Name implements entc.Annotation interface.

type EasyJsonConfig

type EasyJsonConfig struct {
	NoStdMarshalers          bool
	SnakeCase                bool
	LowerCamelCase           bool
	OmitEmpty                bool
	DisallowUnknownFields    bool
	SkipMemberNameUnescaping bool
}

type Edge

type Edge struct {
	*gen.Edge
	Edges Edges
}

Edge specifies and edge to load for a type.

func (Edge) EntQuery

func (e Edge) EntQuery() string

EntQuery constructs the code to eager load all the defined edges for the given edge.

type Edges

type Edges []Edge

Edges is a list of multiple EdgeToLoad.

func (Edges) EntQuery

func (es Edges) EntQuery() string

EntQuery simply runs EntQuery on every item in the list.

type Extension

type Extension struct {
	entc.DefaultExtension
	// contains filtered or unexported fields
}

Extension implements entc.Extension interface for providing http handler code generation.

func NewExtension

func NewExtension(opts ...ExtensionOption) (*Extension, error)

NewExtension returns a new elk extension with default values.

func (*Extension) Annotations

func (e *Extension) Annotations() []entc.Annotation

Annotations of the Extension.

func (*Extension) Hooks

func (e *Extension) Hooks() []gen.Hook

Hooks of the Extension.

func (*Extension) SpecGenerator

func (e *Extension) SpecGenerator(out string) gen.Hook

SpecGenerator TODO

func (*Extension) Templates

func (e *Extension) Templates() []*gen.Template

Templates of the Extension.

type ExtensionOption

type ExtensionOption func(*Extension) error

ExtensionOption allows managing Extension configuration using functional arguments.

func DefaultHandlerPolicy

func DefaultHandlerPolicy(p Policy) ExtensionOption

DefaultHandlerPolicy sets the policy.Policy to use of none is given on a (sub-)schema.

func GenerateHandlers

func GenerateHandlers(opts ...HandlerOption) ExtensionOption

GenerateHandlers enables generation of http crud handlers.

func GenerateSpec

func GenerateSpec(out string, hooks ...Hook) ExtensionOption

GenerateSpec enables the OpenAPI-Spec generator. Data will be written to given filename.

type GenerateFunc

type GenerateFunc func(*spec.Spec) error

The GenerateFunc type is an adapter to allow the use of ordinary function as Generator. If f is a function with the appropriate signature, GenerateFunc(f) is a Generator that calls f.

func (GenerateFunc) Generate

func (f GenerateFunc) Generate(s *spec.Spec) error

Generate calls f(s).

type Generator

type Generator interface {
	// Generate edits the given OpenAPI spec.
	Generate(*spec.Spec) error
}

Generator is the interface that wraps the Generate method.

type HandlerOption

type HandlerOption ExtensionOption

HandlerOption allows managing RESTGenerator configuration using function arguments.

func HandlerEasyJsonConfig

func HandlerEasyJsonConfig(c EasyJsonConfig) HandlerOption

HandlerEasyJsonConfig sets a custom EasyJsonConfig.

type Hook

type Hook func(Generator) Generator

Hook defines the "spec generate middleware".

func SpecDescription

func SpecDescription(v string) Hook

SpecDescription sets the title of the Info block.

func SpecDump

func SpecDump(out io.Writer) Hook

SpecDump dumps the current specs content to the given io.Writer.

func SpecSecurity

func SpecSecurity(sec spec.Security) Hook

SpecSecurity sets the global security Spec.

func SpecSecuritySchemes

func SpecSecuritySchemes(schemes map[string]spec.SecurityScheme) Hook

SpecSecuritySchemes sets the security schemes of the Components block.

func SpecTitle

func SpecTitle(v string) Hook

SpecTitle sets the title of the Info block.

func SpecVersion

func SpecVersion(v string) Hook

SpecVersion sets the version of the Info block.

type Policy

type Policy uint
const (
	None Policy = iota
	Exclude
	Expose
)

func (Policy) Validate

func (p Policy) Validate() error

type SchemaAnnotation

type SchemaAnnotation struct {
	// CreatePolicy defines if a creation handler should be generated.
	CreatePolicy Policy
	// ReadPolicy defines if a read handler should be generated.
	ReadPolicy Policy
	// UpdatePolicy defines if an update handler should be generated.
	UpdatePolicy Policy
	// DeletePolicy defines if a delete handler should be generated.
	DeletePolicy Policy
	// ListPolicy defines if a list handler should be generated.
	ListPolicy Policy
	// CreateGroups holds the serializations groups to use on the creation handler.
	CreateGroups serialization.Groups
	// ReadGroups holds the serializations groups to use on the read handler.
	ReadGroups serialization.Groups
	// UpdateGroups holds the serializations groups to use on the update handler.
	UpdateGroups serialization.Groups
	// ListGroups holds the serializations groups to use on the list handler.
	ListGroups serialization.Groups
	// CreateSecurity sets the security property of the operation in the generated OpenAPI Spec.
	CreateSecurity spec.Security
	// ReadSecurity sets the security property of the operation in the generated OpenAPI Spec.
	ReadSecurity spec.Security
	// UpdateSecurity sets the security property of the operation in the generated OpenAPI Spec.
	UpdateSecurity spec.Security
	// DeleteSecurity sets the security property of the operation in the generated OpenAPI Spec.
	DeleteSecurity spec.Security
	// ListSecurity sets the security property of the operation in the generated OpenAPI Spec.
	ListSecurity spec.Security
}

SchemaAnnotation annotates an entity with metadata for templates.

func CreateGroups

func CreateGroups(gs ...string) SchemaAnnotation

CreateGroups returns a creation groups schema-annotation.

func CreatePolicy

func CreatePolicy(p Policy) SchemaAnnotation

CreatePolicy returns a creation policy schema-annotation.

func CreateSecurity

func CreateSecurity(s spec.Security) SchemaAnnotation

CreateSecurity returns a create-security schema-annotation.

func DeletePolicy

func DeletePolicy(p Policy) SchemaAnnotation

DeletePolicy returns a delete policy schema-annotation.

func DeleteSecurity

func DeleteSecurity(s spec.Security) SchemaAnnotation

DeleteSecurity returns a delete-security schema-annotation.

func ListGroups

func ListGroups(gs ...string) SchemaAnnotation

ListGroups returns a list groups schema-annotation.

func ListPolicy

func ListPolicy(p Policy) SchemaAnnotation

ListPolicy returns a list policy schema-annotation.

func ListSecurity

func ListSecurity(s spec.Security) SchemaAnnotation

ListSecurity returns a list-security schema-annotation.

func ReadGroups

func ReadGroups(gs ...string) SchemaAnnotation

ReadGroups returns a read groups schema-annotation.

func ReadPolicy

func ReadPolicy(p Policy) SchemaAnnotation

ReadPolicy returns a read policy schema-annotation.

func ReadSecurity

func ReadSecurity(s spec.Security) SchemaAnnotation

ReadSecurity returns a read-security schema-annotation.

func SchemaPolicy

func SchemaPolicy(p Policy) SchemaAnnotation

SchemaPolicy returns a schema-annotation with all operation-policies set to the given one.

func SchemaSecurity

func SchemaSecurity(s spec.Security) SchemaAnnotation

SchemaSecurity sets the given security on all schema operations.

func UpdateGroups

func UpdateGroups(gs ...string) SchemaAnnotation

UpdateGroups returns an update groups schema-annotation.

func UpdatePolicy

func UpdatePolicy(p Policy) SchemaAnnotation

UpdatePolicy returns an update policy schema-annotation.

func UpdateSecurity

func UpdateSecurity(s spec.Security) SchemaAnnotation

UpdateSecurity returns an update-security schema-annotation.

func (*SchemaAnnotation) Decode

func (a *SchemaAnnotation) Decode(o interface{}) error

Decode from ent.

func (SchemaAnnotation) Merge

Merge implements ent.Merger interface.

func (SchemaAnnotation) Name

func (SchemaAnnotation) Name() string

Name implements ent.Annotation interface.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL