├── .gitignore ├── .travis.yml ├── Godeps └── Godeps.json ├── Makefile ├── README.md ├── auth ├── authable.go └── static │ ├── static.go │ └── static_test.go ├── beat └── main.go ├── config └── config.go ├── db ├── db.go ├── filter.go ├── filter_test.go ├── mongo │ ├── mongo.go │ └── mongo_test.go └── redis │ ├── redis.go │ └── redis_test.go ├── docs ├── configuration.md ├── getting-started.md └── index.md ├── errors ├── errors.go └── errors_test.go ├── examples └── config.yml ├── go.mk ├── mkdocs.yml ├── mocks └── mock_db │ └── mock_db.go ├── requirements_docs.txt ├── schemas ├── collection_schema.go ├── collection_schema_test.go ├── item_schema.go ├── item_schema_test.go └── link.go ├── server ├── resource_routes.go ├── resource_routes_test.go ├── schema_routes.go ├── server.go └── server_test.go └── transaction ├── transaction.go └── transaction_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | *.swp 26 | 27 | coverage.out 28 | 29 | Godeps/_workspace 30 | Godeps/Readme 31 | site/ 32 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | sudo: false 4 | 5 | go: 6 | - 1.5 7 | - tip 8 | 9 | env: 10 | - GOARCH=amd64 11 | 12 | install: 13 | - export PATH="$HOME/gopath/bin:$PATH" 14 | - go get github.com/tools/godep 15 | - go get -t -d ./... 16 | - godep restore ./... 17 | 18 | script: 19 | - cd $HOME/gopath/src/github.com/backstage/beat 20 | - go test -v ./... 21 | 22 | notifications: 23 | email: 24 | recipients: 25 | - backstage1@corp.globo.com 26 | 27 | services: 28 | - mongodb 29 | - redis-server 30 | -------------------------------------------------------------------------------- /Godeps/Godeps.json: -------------------------------------------------------------------------------- 1 | { 2 | "ImportPath": "github.com/backstage/beat", 3 | "GoVersion": "go1.5.2", 4 | "GodepVersion": "v74", 5 | "Packages": [ 6 | "./..." 7 | ], 8 | "Deps": [ 9 | { 10 | "ImportPath": "github.com/BurntSushi/toml", 11 | "Comment": "v0.2.0-9-gf0aeabc", 12 | "Rev": "f0aeabca5a127c4078abb8c8d64298b147264b55" 13 | }, 14 | { 15 | "ImportPath": "github.com/Sirupsen/logrus", 16 | "Comment": "v0.10.0-19-gf3cfb45", 17 | "Rev": "f3cfb454f4c209e6668c95216c4744b8fddb2356" 18 | }, 19 | { 20 | "ImportPath": "github.com/bitly/go-simplejson", 21 | "Comment": "v0.5.0", 22 | "Rev": "aabad6e819789e569bd6aabf444c935aa9ba1e44" 23 | }, 24 | { 25 | "ImportPath": "github.com/dimfeld/httppath", 26 | "Rev": "c8e499c3ef3c3e272ed8bdcc1ccf39f73c88debc" 27 | }, 28 | { 29 | "ImportPath": "github.com/dimfeld/httptreemux", 30 | "Comment": "3.0.0-1-g612c3d8", 31 | "Rev": "612c3d8e47f5a166d9346b694cf2aeb68eff5369" 32 | }, 33 | { 34 | "ImportPath": "github.com/garyburd/redigo/internal", 35 | "Comment": "v1.0.0-1-gb8dc900", 36 | "Rev": "b8dc90050f24c1a73a52f107f3f575be67b21b7c" 37 | }, 38 | { 39 | "ImportPath": "github.com/garyburd/redigo/redis", 40 | "Comment": "v1.0.0-1-gb8dc900", 41 | "Rev": "b8dc90050f24c1a73a52f107f3f575be67b21b7c" 42 | }, 43 | { 44 | "ImportPath": "github.com/golang/mock/gomock", 45 | "Rev": "bd3c8e81be01eef76d4b503f5e687d2d1354d2d9" 46 | }, 47 | { 48 | "ImportPath": "github.com/hashicorp/hcl", 49 | "Rev": "d7400db7143f8e869812e50a53acd6c8d92af3b8" 50 | }, 51 | { 52 | "ImportPath": "github.com/magiconair/properties", 53 | "Comment": "v1.7.0", 54 | "Rev": "c265cfa48dda6474e208715ca93e987829f572f8" 55 | }, 56 | { 57 | "ImportPath": "github.com/mitchellh/mapstructure", 58 | "Rev": "d2dd0262208475919e1a362f675cfc0e7c10e905" 59 | }, 60 | { 61 | "ImportPath": "github.com/satori/go.uuid", 62 | "Comment": "v1.1.0", 63 | "Rev": "879c5887cd475cd7864858769793b2ceb0d44feb" 64 | }, 65 | { 66 | "ImportPath": "github.com/spf13/cast", 67 | "Rev": "27b586b42e29bec072fe7379259cc719e1289da6" 68 | }, 69 | { 70 | "ImportPath": "github.com/spf13/jwalterweatherman", 71 | "Rev": "33c24e77fb80341fe7130ee7c594256ff08ccc46" 72 | }, 73 | { 74 | "ImportPath": "github.com/spf13/pflag", 75 | "Rev": "367864438f1b1a3c7db4da06a2f55b144e6784e0" 76 | }, 77 | { 78 | "ImportPath": "github.com/spf13/viper", 79 | "Rev": "c1ccc378a054ea8d4e38d8c67f6938d4760b53dd" 80 | }, 81 | { 82 | "ImportPath": "golang.org/x/sys/unix", 83 | "Rev": "b44883b474ffefa37335017174e397412b633a4f" 84 | }, 85 | { 86 | "ImportPath": "gopkg.in/check.v1", 87 | "Rev": "4f90aeace3a26ad7021961c297b22c42160c7b25" 88 | }, 89 | { 90 | "ImportPath": "gopkg.in/mgo.v2", 91 | "Comment": "r2016.02.04-3-g29cc868", 92 | "Rev": "29cc868a5ca65f401ff318143f9408d02f4799cc" 93 | }, 94 | { 95 | "ImportPath": "gopkg.in/yaml.v2", 96 | "Rev": "a83829b6f1293c91addabc89d0571c246397bbf4" 97 | }, 98 | { 99 | "ImportPath": "github.com/hashicorp/hcl/hcl/ast", 100 | "Rev": "d7400db7143f8e869812e50a53acd6c8d92af3b8" 101 | }, 102 | { 103 | "ImportPath": "github.com/hashicorp/hcl/hcl/parser", 104 | "Rev": "d7400db7143f8e869812e50a53acd6c8d92af3b8" 105 | }, 106 | { 107 | "ImportPath": "github.com/hashicorp/hcl/hcl/token", 108 | "Rev": "d7400db7143f8e869812e50a53acd6c8d92af3b8" 109 | }, 110 | { 111 | "ImportPath": "github.com/hashicorp/hcl/json/parser", 112 | "Rev": "d7400db7143f8e869812e50a53acd6c8d92af3b8" 113 | }, 114 | { 115 | "ImportPath": "github.com/hashicorp/hcl/hcl/scanner", 116 | "Rev": "d7400db7143f8e869812e50a53acd6c8d92af3b8" 117 | }, 118 | { 119 | "ImportPath": "github.com/hashicorp/hcl/hcl/strconv", 120 | "Rev": "d7400db7143f8e869812e50a53acd6c8d92af3b8" 121 | }, 122 | { 123 | "ImportPath": "github.com/hashicorp/hcl/json/scanner", 124 | "Rev": "d7400db7143f8e869812e50a53acd6c8d92af3b8" 125 | }, 126 | { 127 | "ImportPath": "github.com/hashicorp/hcl/json/token", 128 | "Rev": "d7400db7143f8e869812e50a53acd6c8d92af3b8" 129 | }, 130 | { 131 | "ImportPath": "github.com/fsnotify/fsnotify", 132 | "Comment": "v1.3.0", 133 | "Rev": "30411dbcefb7a1da7e84f75530ad3abe4011b4f8" 134 | }, 135 | { 136 | "ImportPath": "gopkg.in/mgo.v2/bson", 137 | "Comment": "r2016.02.04-3-g29cc868", 138 | "Rev": "29cc868a5ca65f401ff318143f9408d02f4799cc" 139 | }, 140 | { 141 | "ImportPath": "gopkg.in/mgo.v2/internal/sasl", 142 | "Comment": "r2016.02.04-3-g29cc868", 143 | "Rev": "29cc868a5ca65f401ff318143f9408d02f4799cc" 144 | }, 145 | { 146 | "ImportPath": "gopkg.in/mgo.v2/internal/scram", 147 | "Comment": "r2016.02.04-3-g29cc868", 148 | "Rev": "29cc868a5ca65f401ff318143f9408d02f4799cc" 149 | } 150 | ] 151 | } 152 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | include go.mk 2 | 3 | .PHONY: build 4 | build: gomkbuild 5 | 6 | .PHONY: xbuild 7 | xbuild: gomkxbuild 8 | 9 | .PHONY: clean 10 | clean: gomkclean 11 | 12 | .PHONY: run 13 | run: 14 | go run beat/main.go 15 | 16 | .PHONY: setup 17 | setup: deps restoregodeps 18 | 19 | .PHONY: doc-server 20 | doc-server: 21 | mkdocs serve 22 | 23 | .PHONY: setup-docs 24 | setup-docs: 25 | pip install -r requirements_docs.txt 26 | 27 | .PHONY: deploy-docs 28 | deploy-docs: 29 | mkdocs gh-deploy --clean 30 | 31 | .PHONY: update-mocks 32 | update-mocks: 33 | mockgen -destination "mocks/mock_db/mock_db.go" github.com/backstage/beat/db Database 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/backstage/beat?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 2 | [![Build Status](https://travis-ci.org/backstage/beat.png?branch=master)](https://travis-ci.org/backstage/beat) 3 | 4 | ## What is Backstage Beat? 5 | 6 | Backstage Beat is a Backend-as-a-Service software that makes mobile and web development really fast using restful APIs. 7 | 8 | **Attention: Beat is in early development and is not ready for production yet.** 9 | 10 | ### Resources 11 | 12 | * [Documentation](http://backstage.github.io/beat). 13 | -------------------------------------------------------------------------------- /auth/authable.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strings" 7 | ) 8 | 9 | var ( 10 | auths = map[string]RegisterFunc{} 11 | ) 12 | 13 | // User is the basic interface to identify the logged user provided by each 14 | // Authable implementation. 15 | type User interface { 16 | Email() string 17 | } 18 | 19 | type RegisterFunc func() (Authable, error) 20 | 21 | // Authable is the interface that provides all capacity to handle autenticated 22 | // and authorized transations. 23 | // 24 | // GetUser returns the current user based on http header for a transaction. 25 | type Authable interface { 26 | GetUser(*http.Header) User 27 | } 28 | 29 | // Register inserts a implementation of `Authable` in the register, is useful 30 | // to auto discover implementations and change it without changing the code. 31 | func Register(name string, fn RegisterFunc) { 32 | auths[name] = fn 33 | } 34 | 35 | // New returns a implementation of `Authable` found in the register, if not found 36 | // return an error. 37 | func New(name string) (Authable, error) { 38 | fn := auths[name] 39 | if fn == nil { 40 | return nil, ErrNotFound{name: name} 41 | } 42 | db, err := fn() 43 | 44 | if err != nil { 45 | return nil, authError{name: name, originalErr: err} 46 | } 47 | 48 | return db, nil 49 | } 50 | 51 | type ErrNotFound struct { 52 | name string 53 | } 54 | 55 | func (a ErrNotFound) Error() string { 56 | availableAuths := make([]string, 0, len(auths)) 57 | for auth := range auths { 58 | availableAuths = append(availableAuths, auth) 59 | } 60 | 61 | return fmt.Sprintf(`Authentication "%s" not found, are available: %s.`, a.name, strings.Join(availableAuths, ", ")) 62 | } 63 | 64 | type authError struct { 65 | name string 66 | originalErr error 67 | } 68 | 69 | func (a authError) Error() string { 70 | return fmt.Sprintf(`[authentication][%s] %s`, a.name, a.originalErr.Error()) 71 | } 72 | -------------------------------------------------------------------------------- /auth/static/static.go: -------------------------------------------------------------------------------- 1 | package static 2 | 3 | import ( 4 | "fmt" 5 | "github.com/backstage/beat/auth" 6 | "github.com/spf13/viper" 7 | "net/http" 8 | ) 9 | 10 | // StaticAuthentication implements the auth.Authable interface. 11 | // 12 | // This Authable is a simple authentication based on a yaml file 13 | // that contains all tokens allowed to perform a write operation. 14 | // 15 | // An example of yaml file: 16 | // auth: 17 | // tokens: 18 | // example1: 19 | // email: admin@example.net 20 | // 21 | // example2: 22 | // email: guest@example.net 23 | // 24 | // 25 | // For each token is allowed to make a request like 26 | // curl -H "Token: example1" http://myserver/api/collection 27 | 28 | type StaticAuthentication struct{} 29 | 30 | // StaticUser implements the auth.User interface. 31 | type StaticUser struct { 32 | TokenEmail string `mapstructure:"email"` 33 | } 34 | 35 | var ( 36 | TokensConfigPath = "auth.tokens" 37 | NilStaticUser = StaticUser{} 38 | ) 39 | 40 | func init() { 41 | auth.Register("static", func() (auth.Authable, error) { 42 | return NewStaticAuthentication(), nil 43 | }) 44 | } 45 | 46 | // NewStaticAuthentication return a new StaticAuthentication 47 | func NewStaticAuthentication() *StaticAuthentication { 48 | return &StaticAuthentication{} 49 | } 50 | 51 | // GetUser implements auth.Authable interface. 52 | func (authenticaton *StaticAuthentication) GetUser(header *http.Header) auth.User { 53 | var user StaticUser 54 | token := header.Get("Token") 55 | 56 | if token == "" { 57 | return nil 58 | } 59 | 60 | viper.UnmarshalKey(fmt.Sprintf("%s.%s", TokensConfigPath, token), &user) 61 | 62 | if user == NilStaticUser { 63 | return nil 64 | } 65 | 66 | return user 67 | } 68 | 69 | // Email implements auth.User interface. 70 | func (user StaticUser) Email() string { 71 | return user.TokenEmail 72 | } 73 | -------------------------------------------------------------------------------- /auth/static/static_test.go: -------------------------------------------------------------------------------- 1 | package static 2 | 3 | import ( 4 | "github.com/backstage/beat/auth" 5 | "github.com/backstage/beat/config" 6 | "gopkg.in/check.v1" 7 | "net/http" 8 | "testing" 9 | ) 10 | 11 | var _ = check.Suite(&S{}) 12 | 13 | type S struct { 14 | authenticaton *StaticAuthentication 15 | } 16 | 17 | func Test(t *testing.T) { 18 | check.TestingT(t) 19 | } 20 | 21 | func (s *S) SetUpSuite(c *check.C) { 22 | err := config.ReadConfigFile("../../examples/config.yml") 23 | c.Assert(err, check.IsNil) 24 | 25 | s.authenticaton = NewStaticAuthentication() 26 | } 27 | 28 | func (s *S) TestGetFromRegister(c *check.C) { 29 | db, err := auth.New("static") 30 | c.Assert(err, check.IsNil) 31 | c.Assert(db, check.FitsTypeOf, &StaticAuthentication{}) 32 | } 33 | 34 | func (s *S) TestStaticAuthenticationWithUserFound(c *check.C) { 35 | header := &http.Header{} 36 | header.Set("Token", "example1") 37 | 38 | user := s.authenticaton.GetUser(header) 39 | c.Assert(user, check.NotNil) 40 | c.Assert(user.Email(), check.Equals, "admin@example.net") 41 | 42 | header = &http.Header{} 43 | header.Set("Token", "example2") 44 | 45 | user = s.authenticaton.GetUser(header) 46 | c.Assert(user, check.Not(check.IsNil)) 47 | c.Assert(user.Email(), check.Equals, "guest@example.net") 48 | } 49 | 50 | func (s *S) TestStaticAuthenticationWithUserNotFound(c *check.C) { 51 | header := &http.Header{} 52 | header.Set("Token", "not-found") 53 | 54 | user := s.authenticaton.GetUser(header) 55 | c.Assert(user, check.IsNil) 56 | } 57 | 58 | func (s *S) TestStaticAuthenticationWithMissingToken(c *check.C) { 59 | header := &http.Header{} 60 | user := s.authenticaton.GetUser(header) 61 | c.Assert(user, check.IsNil) 62 | } 63 | -------------------------------------------------------------------------------- /beat/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | 6 | log "github.com/Sirupsen/logrus" 7 | 8 | "github.com/backstage/beat/config" 9 | "github.com/backstage/beat/server" 10 | 11 | _ "github.com/backstage/beat/auth/static" 12 | _ "github.com/backstage/beat/db/mongo" 13 | _ "github.com/backstage/beat/db/redis" 14 | ) 15 | 16 | func main() { 17 | var configFile string 18 | flag.StringVar(&configFile, "c", "./examples/config.yml", "Config file") 19 | flag.Parse() 20 | 21 | err := config.ReadConfigFile(configFile) 22 | 23 | if err != nil { 24 | log.Fatal(err) 25 | } 26 | 27 | config.LoadLogSettings() 28 | 29 | s, err := server.New() 30 | if err != nil { 31 | log.Fatal(err.Error()) 32 | } 33 | s.Run() 34 | } 35 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/Sirupsen/logrus" 5 | "github.com/spf13/viper" 6 | "log" 7 | "os" 8 | "strings" 9 | ) 10 | 11 | func ReadConfigFile(filePath string) error { 12 | _, err := os.Stat(filePath) 13 | if err != nil { 14 | return err 15 | } 16 | viper.SetConfigFile(filePath) 17 | return viper.ReadInConfig() 18 | } 19 | 20 | func init() { 21 | viper.SetConfigType("yaml") 22 | viper.AutomaticEnv() 23 | envReplacer := strings.NewReplacer(".", "_") 24 | viper.SetEnvKeyReplacer(envReplacer) 25 | 26 | viper.SetDefault("log.level", "info") 27 | } 28 | 29 | func LoadLogSettings() { 30 | logLevel, err := logrus.ParseLevel(viper.GetString("log.level")) 31 | 32 | if err != nil { 33 | log.Fatal(err) 34 | } 35 | 36 | logrus.SetLevel(logLevel) 37 | } 38 | -------------------------------------------------------------------------------- /db/db.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strings" 7 | 8 | "github.com/backstage/beat/errors" 9 | "github.com/backstage/beat/schemas" 10 | ) 11 | 12 | var ( 13 | ErrItemSchemaNotFound = errors.New("item-schema not found", http.StatusNotFound) 14 | ErrCollectionSchemaNotFound = errors.New("collection-schema not found", http.StatusNotFound) 15 | databases = map[string]RegisterFunc{} 16 | ) 17 | 18 | type RegisterFunc func() (Database, error) 19 | 20 | type Database interface { 21 | CreateItemSchema(*schemas.ItemSchema) errors.Error 22 | FindItemSchema(*Filter) (*ItemSchemasReply, errors.Error) 23 | FindOneItemSchema(*Filter) (*schemas.ItemSchema, errors.Error) 24 | FindItemSchemaByCollectionName(string) (*schemas.ItemSchema, errors.Error) 25 | UpdateItemSchema(*schemas.ItemSchema) errors.Error 26 | DeleteItemSchema(string) errors.Error 27 | } 28 | 29 | type ItemSchemasReply struct { 30 | Items []*schemas.ItemSchema `json:"items"` 31 | } 32 | 33 | // Register inserts a implementation of `Database` in the register, is useful 34 | // to auto discover implementations and change it without changing the code. 35 | func Register(name string, fn RegisterFunc) { 36 | databases[name] = fn 37 | } 38 | 39 | // New returns a implementation of `Database` found in the register, if not found 40 | // return an error. 41 | func New(name string) (Database, error) { 42 | fn := databases[name] 43 | if fn == nil { 44 | return nil, ErrNotFound{name: name} 45 | } 46 | db, err := fn() 47 | 48 | if err != nil { 49 | return nil, databaseError{name: name, originalErr: err} 50 | } 51 | 52 | return db, nil 53 | } 54 | 55 | type ErrNotFound struct { 56 | name string 57 | } 58 | 59 | func (d ErrNotFound) Error() string { 60 | availableDatabases := make([]string, 0, len(databases)) 61 | for db := range databases { 62 | availableDatabases = append(availableDatabases, db) 63 | } 64 | 65 | return fmt.Sprintf(`Database "%s" not found, are available: %s.`, d.name, strings.Join(availableDatabases, ", ")) 66 | } 67 | 68 | type databaseError struct { 69 | name string 70 | originalErr error 71 | } 72 | 73 | func (d databaseError) Error() string { 74 | return fmt.Sprintf(`[db][%s] %s`, d.name, d.originalErr.Error()) 75 | } 76 | -------------------------------------------------------------------------------- /db/filter.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "net/url" 5 | "strconv" 6 | "strings" 7 | 8 | simplejson "github.com/bitly/go-simplejson" 9 | ) 10 | 11 | type Filter struct { 12 | Where *simplejson.Json 13 | Page int 14 | PerPage int 15 | } 16 | 17 | func NewFilterFromQueryString(q string) (*Filter, error) { 18 | filter := &Filter{} 19 | filter.Where = simplejson.New() 20 | filter.loadInitialValues() 21 | 22 | urlValues, err := url.ParseQuery(q) 23 | if err != nil { 24 | return nil, err 25 | } 26 | 27 | for key, value := range urlValues { 28 | filter.putURLValue(key, value[0]) 29 | } 30 | 31 | return filter, nil 32 | } 33 | 34 | func (filter *Filter) loadInitialValues() { 35 | filter.Page = 1 36 | filter.PerPage = 10 37 | } 38 | 39 | func (filter *Filter) Skip() int { 40 | return (filter.Page - 1) * filter.PerPage 41 | } 42 | 43 | func (filter *Filter) putURLValue(key, value string) { 44 | path := []string{} 45 | 46 | for _, part := range strings.Split(key, "[") { 47 | if last := part[len(part)-1]; last == ']' { 48 | part = part[:len(part)-1] 49 | } 50 | 51 | path = append(path, part) 52 | 53 | } 54 | 55 | if path[0] == "filter" && len(path) > 1 { 56 | switch path[1] { 57 | case "perPage": 58 | filter.setPerPageFromString(value) 59 | return 60 | case "page": 61 | filter.setPageFromString(value) 62 | return 63 | case "where": 64 | if len(path) > 2 { 65 | filter.putWhere(path[2:], value) 66 | } 67 | } 68 | } 69 | } 70 | 71 | func (filter *Filter) setPerPageFromString(perPage string) { 72 | if s, err := strconv.Atoi(perPage); err == nil { 73 | if s > 1000 { 74 | s = 1000 75 | } 76 | filter.PerPage = s 77 | } 78 | } 79 | 80 | func (filter *Filter) setPageFromString(page string) { 81 | if s, err := strconv.Atoi(page); err == nil { 82 | if s < 1 { 83 | s = 1 84 | } 85 | filter.Page = s 86 | } 87 | } 88 | 89 | func (filter *Filter) putWhere(path []string, value string) { 90 | filter.Where.SetPath(path, value) 91 | } 92 | -------------------------------------------------------------------------------- /db/filter_test.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "gopkg.in/check.v1" 5 | "testing" 6 | ) 7 | 8 | var _ = check.Suite(&S{}) 9 | 10 | type S struct{} 11 | 12 | func Test(t *testing.T) { 13 | check.TestingT(t) 14 | } 15 | 16 | func (s *S) TestNewFilterFromQueryStringEmpty(c *check.C) { 17 | filter, err := NewFilterFromQueryString("") 18 | c.Assert(err, check.IsNil) 19 | c.Assert(filter, check.NotNil) 20 | c.Assert(filter.PerPage, check.Equals, 10) 21 | 22 | whereMap, err := filter.Where.Map() 23 | c.Assert(err, check.IsNil) 24 | c.Assert(len(whereMap), check.Equals, 0) 25 | } 26 | 27 | func (s *S) TestNewFilterFromQueryPerPage(c *check.C) { 28 | filter, err := NewFilterFromQueryString("filter[perPage]=15") 29 | c.Assert(err, check.IsNil) 30 | c.Assert(filter.PerPage, check.Equals, 15) 31 | } 32 | 33 | func (s *S) TestNewFilterFromQueryPerPageOverFlow(c *check.C) { 34 | filter, err := NewFilterFromQueryString("filter[perPage]=10000") 35 | c.Assert(err, check.IsNil) 36 | c.Assert(filter.PerPage, check.Equals, 1000) 37 | } 38 | 39 | func (s *S) TestNewFilterFromQueryWhere(c *check.C) { 40 | filter, err := NewFilterFromQueryString("filter[where][name]=wilson") 41 | c.Assert(err, check.IsNil) 42 | c.Assert(filter.Where.Get("name").MustString(), check.Equals, "wilson") 43 | 44 | filter, err = NewFilterFromQueryString("filter[where][name]=wilson&filter[where][title][like]=juju") 45 | c.Assert(err, check.IsNil) 46 | 47 | c.Assert(filter.Where.Get("name").MustString(), check.Equals, "wilson") 48 | c.Assert(filter.Where.GetPath("title", "like").MustString(), check.Equals, "juju") 49 | } 50 | 51 | func (s *S) TestNewFilterFromQueryPage(c *check.C) { 52 | filter, err := NewFilterFromQueryString("filter[page]=2") 53 | c.Assert(err, check.IsNil) 54 | c.Assert(filter.Page, check.Equals, 2) 55 | 56 | filter, err = NewFilterFromQueryString("filter[page]=0") 57 | c.Assert(err, check.IsNil) 58 | c.Assert(filter.Page, check.Equals, 1) 59 | } 60 | 61 | func (s *S) TestNewFilterSkip(c *check.C) { 62 | filter, err := NewFilterFromQueryString("") 63 | c.Assert(err, check.IsNil) 64 | c.Assert(filter.Skip(), check.Equals, 0) 65 | 66 | filter, err = NewFilterFromQueryString("filter[page]=2&filter[perPage]=100") 67 | c.Assert(err, check.IsNil) 68 | c.Assert(filter.Skip(), check.Equals, 100) 69 | 70 | filter, err = NewFilterFromQueryString("filter[page]=1&filter[perPage]=100") 71 | c.Assert(err, check.IsNil) 72 | c.Assert(filter.Skip(), check.Equals, 0) 73 | } 74 | -------------------------------------------------------------------------------- /db/mongo/mongo.go: -------------------------------------------------------------------------------- 1 | package mongo 2 | 3 | import ( 4 | "net/http" 5 | 6 | _ "github.com/backstage/beat/config" 7 | "github.com/backstage/beat/db" 8 | "github.com/backstage/beat/errors" 9 | "github.com/backstage/beat/schemas" 10 | simplejson "github.com/bitly/go-simplejson" 11 | "github.com/spf13/viper" 12 | "gopkg.in/mgo.v2" 13 | "gopkg.in/mgo.v2/bson" 14 | ) 15 | 16 | func init() { 17 | viper.SetDefault("mongo.uri", "localhost:27017/backstage_beat_local") 18 | viper.SetDefault("mongo.failFast", true) 19 | 20 | db.Register("mongo", func() (db.Database, error) { 21 | return New() 22 | }) 23 | } 24 | 25 | type MongoDB struct { 26 | dialInfo *mgo.DialInfo 27 | session *mgo.Session 28 | } 29 | 30 | func New() (*MongoDB, error) { 31 | d := &MongoDB{} 32 | 33 | var err error 34 | d.dialInfo, err = mgo.ParseURL(viper.GetString("mongo.uri")) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | d.dialInfo.Username = viper.GetString("mongo.user") 40 | d.dialInfo.Password = viper.GetString("mongo.password") 41 | 42 | d.dialInfo.FailFast = viper.GetBool("mongo.failFast") 43 | session, err := mgo.DialWithInfo(d.dialInfo) 44 | 45 | if err != nil { 46 | return nil, err 47 | } 48 | d.session = session 49 | return d, nil 50 | } 51 | 52 | func (m *MongoDB) CreateItemSchema(itemSchema *schemas.ItemSchema) errors.Error { 53 | session := m.session.Clone() 54 | defer session.Close() 55 | err := session.DB("").C(schemas.ItemSchemaCollectionName).Insert(itemSchema) 56 | 57 | if err != nil { 58 | return convertMongoError(err) 59 | } 60 | 61 | return nil 62 | } 63 | 64 | func (m *MongoDB) UpdateItemSchema(itemSchema *schemas.ItemSchema) errors.Error { 65 | session := m.session.Clone() 66 | defer session.Close() 67 | err := session.DB("").C(schemas.ItemSchemaCollectionName).UpdateId(itemSchema.CollectionName, itemSchema) 68 | 69 | if err != nil { 70 | return convertMongoError(err) 71 | } 72 | 73 | return nil 74 | } 75 | 76 | func (m *MongoDB) FindItemSchema(filter *db.Filter) (*db.ItemSchemasReply, errors.Error) { 77 | session := m.session.Clone() 78 | defer session.Close() 79 | where := BuildMongoWhere(filter.Where, schemas.ItemSchemaPrimaryKey) 80 | query := session.DB("").C(schemas.ItemSchemaCollectionName).Find(where) 81 | 82 | reply := &db.ItemSchemasReply{} 83 | reply.Items = []*schemas.ItemSchema{} 84 | err := query.Skip(filter.Skip()).Limit(filter.PerPage).Iter().All(&reply.Items) 85 | 86 | if err != nil { 87 | return nil, errors.Wraps(err, http.StatusInternalServerError) 88 | } 89 | 90 | return reply, nil 91 | } 92 | 93 | func (m *MongoDB) FindOneItemSchema(filter *db.Filter) (*schemas.ItemSchema, errors.Error) { 94 | session := m.session.Clone() 95 | defer session.Close() 96 | where := BuildMongoWhere(filter.Where, schemas.ItemSchemaPrimaryKey) 97 | query := session.DB("").C(schemas.ItemSchemaCollectionName).Find(where) 98 | 99 | itemSchema := &schemas.ItemSchema{} 100 | err := query.One(&itemSchema) 101 | 102 | if err == mgo.ErrNotFound { 103 | return nil, db.ErrItemSchemaNotFound 104 | } else if err != nil { 105 | return nil, errors.Wraps(err, http.StatusInternalServerError) 106 | } 107 | 108 | return itemSchema, nil 109 | } 110 | 111 | func (m *MongoDB) FindItemSchemaByCollectionName(collectionName string) (*schemas.ItemSchema, errors.Error) { 112 | session := m.session.Clone() 113 | defer session.Close() 114 | 115 | itemSchema := &schemas.ItemSchema{} 116 | err := session.DB("").C(schemas.ItemSchemaCollectionName).FindId(collectionName).One(&itemSchema) 117 | 118 | if err == mgo.ErrNotFound { 119 | return nil, db.ErrItemSchemaNotFound 120 | } else if err != nil { 121 | return nil, errors.Wraps(err, http.StatusInternalServerError) 122 | } 123 | 124 | return itemSchema, nil 125 | } 126 | 127 | func (m *MongoDB) DeleteItemSchema(collectionName string) errors.Error { 128 | session := m.session.Clone() 129 | defer session.Close() 130 | 131 | err := session.DB("").C(schemas.ItemSchemaCollectionName).RemoveId(collectionName) 132 | if err == mgo.ErrNotFound { 133 | return db.ErrItemSchemaNotFound 134 | } else if err != nil { 135 | return errors.Wraps(err, http.StatusInternalServerError) 136 | } 137 | return nil 138 | } 139 | 140 | func BuildMongoWhere(where *simplejson.Json, primaryKey string) bson.M { 141 | mongoWhere := bson.M{} 142 | for key, value := range where.MustMap() { 143 | switch key { 144 | case "and", "or", "nor": 145 | mongoWhere["$"+key] = buildMongoWhereByArray( 146 | where.Get(key), 147 | primaryKey, 148 | ) 149 | continue 150 | 151 | case primaryKey: 152 | mongoWhere["_id"] = value 153 | continue 154 | } 155 | mongoWhere[key] = value 156 | } 157 | return mongoWhere 158 | } 159 | 160 | func buildMongoWhereByArray(wheres *simplejson.Json, primaryKey string) []bson.M { 161 | mongoWheres := []bson.M{} 162 | for key := range wheres.MustArray() { 163 | mongoWhere := BuildMongoWhere(wheres.GetIndex(key), primaryKey) 164 | mongoWheres = append(mongoWheres, mongoWhere) 165 | } 166 | return mongoWheres 167 | } 168 | 169 | func convertMongoError(err error) errors.Error { 170 | if mongoErr, ok := err.(*mgo.LastError); ok { 171 | if mongoErr.Code == 11000 { 172 | return buildMongoDuplicatedError() 173 | } 174 | } 175 | return errors.Wraps(err, http.StatusInternalServerError) 176 | } 177 | 178 | func buildMongoDuplicatedError() errors.Error { 179 | validationError := &errors.ValidationError{} 180 | validationError.Put("_all", "Duplicated resource") 181 | return validationError 182 | } 183 | -------------------------------------------------------------------------------- /db/mongo/mongo_test.go: -------------------------------------------------------------------------------- 1 | package mongo 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "os" 7 | "testing" 8 | 9 | "github.com/backstage/beat/db" 10 | "github.com/backstage/beat/schemas" 11 | simplejson "github.com/bitly/go-simplejson" 12 | "gopkg.in/check.v1" 13 | "gopkg.in/mgo.v2/bson" 14 | ) 15 | 16 | var _ = check.Suite(&S{}) 17 | 18 | type S struct { 19 | Db *MongoDB 20 | } 21 | 22 | func Test(t *testing.T) { 23 | check.TestingT(t) 24 | } 25 | 26 | func (s *S) SetUpSuite(c *check.C) { 27 | var err error 28 | 29 | os.Setenv("MONGO_URI", "localhost:27017/backstage_beat_test") 30 | s.Db, err = New() 31 | c.Assert(err, check.IsNil) 32 | 33 | session := s.Db.session.Clone() 34 | defer session.Close() 35 | session.DB("").DropDatabase() 36 | } 37 | 38 | func (s *S) TestNewMongoDBConfigWithEnviromentVariables(c *check.C) { 39 | os.Unsetenv("MONGO_URI") 40 | os.Unsetenv("MONGO_USER") 41 | os.Unsetenv("MONGO_PASSWORD") 42 | 43 | db, err := New() 44 | c.Assert(err, check.IsNil) 45 | c.Assert(db, check.Not(check.IsNil)) 46 | c.Assert(db.dialInfo.Addrs, check.DeepEquals, []string{"localhost:27017"}) 47 | c.Assert(db.dialInfo.Database, check.Equals, "backstage_beat_local") 48 | c.Assert(db.dialInfo.Username, check.Equals, "") 49 | c.Assert(db.dialInfo.Password, check.Equals, "") 50 | } 51 | 52 | func (s *S) TestNewMongoDBConfigWithDefaultVariables(c *check.C) { 53 | c.Assert(s.Db.dialInfo.Addrs, check.DeepEquals, []string{"localhost:27017"}) 54 | c.Assert(s.Db.dialInfo.Database, check.Equals, "backstage_beat_test") 55 | } 56 | 57 | func (s *S) TestGetFromRegister(c *check.C) { 58 | db, err := db.New("mongo") 59 | c.Assert(err, check.IsNil) 60 | c.Assert(db, check.FitsTypeOf, &MongoDB{}) 61 | } 62 | 63 | func (s *S) TestCreateItemSchema(c *check.C) { 64 | itemSchema := &schemas.ItemSchema{CollectionName: "test-schema"} 65 | dbErr := s.Db.CreateItemSchema(itemSchema) 66 | c.Assert(dbErr, check.IsNil) 67 | 68 | itemSchema, dbErr = s.Db.FindItemSchemaByCollectionName("test-schema") 69 | c.Assert(dbErr, check.IsNil) 70 | c.Assert(itemSchema.CollectionName, check.Equals, "test-schema") 71 | } 72 | 73 | func (s *S) TestCreateItemSchemaDuplicated(c *check.C) { 74 | itemSchema := &schemas.ItemSchema{CollectionName: "duplicated-schema"} 75 | dbErr := s.Db.CreateItemSchema(itemSchema) 76 | 77 | c.Assert(dbErr, check.IsNil) 78 | 79 | dbErr = s.Db.CreateItemSchema(itemSchema) 80 | c.Assert(dbErr, check.NotNil) 81 | c.Assert(dbErr.StatusCode(), check.Equals, 422) 82 | c.Assert(dbErr.Error(), check.Equals, "_all: Duplicated resource") 83 | } 84 | 85 | func (s *S) TestFindItemSchema(c *check.C) { 86 | for i := 0; i < 3; i++ { 87 | dbErr := s.Db.CreateItemSchema(&schemas.ItemSchema{CollectionName: fmt.Sprintf("find-%d", i)}) 88 | c.Assert(dbErr, check.IsNil) 89 | i++ 90 | } 91 | filter, err := db.NewFilterFromQueryString("") 92 | c.Assert(err, check.IsNil) 93 | 94 | reply, dbErr := s.Db.FindItemSchema(filter) 95 | c.Assert(dbErr, check.IsNil) 96 | c.Assert(len(reply.Items) > 3, check.Equals, true) 97 | } 98 | 99 | func (s *S) TestFindItemSchemaWithExactPattern(c *check.C) { 100 | for i := 0; i < 3; i++ { 101 | dbErr := s.Db.CreateItemSchema(&schemas.ItemSchema{CollectionName: fmt.Sprintf("find-exact-%d", i)}) 102 | c.Assert(dbErr, check.IsNil) 103 | } 104 | filter, err := db.NewFilterFromQueryString("filter[where][collectionName]=find-exact-1") 105 | c.Assert(err, check.IsNil) 106 | 107 | reply, dbErr := s.Db.FindItemSchema(filter) 108 | c.Assert(dbErr, check.IsNil) 109 | c.Assert(len(reply.Items), check.Equals, 1) 110 | c.Assert(reply.Items[0].CollectionName, check.Equals, "find-exact-1") 111 | } 112 | 113 | func (s *S) TestFindOneItemSchemaWithExactPattern(c *check.C) { 114 | for i := 0; i < 3; i++ { 115 | dbErr := s.Db.CreateItemSchema(&schemas.ItemSchema{CollectionName: fmt.Sprintf("find-one-exact-%d", i)}) 116 | c.Assert(dbErr, check.IsNil) 117 | } 118 | filter, err := db.NewFilterFromQueryString("filter[where][collectionName]=find-one-exact-1") 119 | c.Assert(err, check.IsNil) 120 | 121 | itemSchema, dbErr := s.Db.FindOneItemSchema(filter) 122 | c.Assert(dbErr, check.IsNil) 123 | c.Assert(itemSchema.CollectionName, check.Equals, "find-one-exact-1") 124 | } 125 | 126 | func (s *S) TestFindOneItemSchemaWithNotFound(c *check.C) { 127 | filter, err := db.NewFilterFromQueryString("filter[where][collectionName]=not-found") 128 | c.Assert(err, check.IsNil) 129 | 130 | _, dbErr := s.Db.FindOneItemSchema(filter) 131 | c.Assert(dbErr, check.NotNil) 132 | c.Assert(dbErr.StatusCode(), check.Equals, http.StatusNotFound) 133 | } 134 | 135 | func (s *S) TestFindItemSchemaByCollectionNameWithNotFound(c *check.C) { 136 | _, dbErr := s.Db.FindItemSchemaByCollectionName("not-found") 137 | c.Assert(dbErr, check.NotNil) 138 | c.Assert(dbErr.StatusCode(), check.Equals, http.StatusNotFound) 139 | } 140 | 141 | func (s *S) TestDeleteItemSchemaWithNotFound(c *check.C) { 142 | dbErr := s.Db.DeleteItemSchema("not-found") 143 | c.Assert(dbErr, check.NotNil) 144 | c.Assert(dbErr.StatusCode(), check.Equals, http.StatusNotFound) 145 | } 146 | 147 | func (s *S) TestDeleteItemSchema(c *check.C) { 148 | dbErr := s.Db.CreateItemSchema(&schemas.ItemSchema{CollectionName: "to-be-deleted"}) 149 | c.Assert(dbErr, check.IsNil) 150 | 151 | dbErr = s.Db.DeleteItemSchema("to-be-deleted") 152 | c.Assert(dbErr, check.IsNil) 153 | 154 | _, dbErr = s.Db.FindItemSchemaByCollectionName("to-be-deleted") 155 | c.Assert(dbErr, check.NotNil) 156 | c.Assert(dbErr.StatusCode(), check.Equals, http.StatusNotFound) 157 | } 158 | 159 | func (s *S) TestMongoBuildWhereSimple(c *check.C) { 160 | where, _ := simplejson.NewJson([]byte(`{"name": "r2"}`)) 161 | mongoWhere := BuildMongoWhere(where, "id") 162 | c.Assert(mongoWhere, check.DeepEquals, bson.M{"name": "r2"}) 163 | } 164 | 165 | func (s *S) TestMongoBuildWhereAndQuery(c *check.C) { 166 | where, _ := simplejson.NewJson([]byte(`{"and": [{"name": "wilson"}, {"tenantId": "globocom"}]}`)) 167 | mongoWhere := BuildMongoWhere(where, "id") 168 | c.Assert(mongoWhere, check.DeepEquals, bson.M{ 169 | "$and": []bson.M{ 170 | bson.M{"name": "wilson"}, 171 | bson.M{"tenantId": "globocom"}, 172 | }, 173 | }) 174 | 175 | } 176 | 177 | func (s *S) TestMongoBuildWhereWithPrimaryKey(c *check.C) { 178 | where, _ := simplejson.NewJson([]byte(`{"tenantId": "globocom"}`)) 179 | mongoWhere := BuildMongoWhere(where, "tenantId") 180 | c.Assert(mongoWhere, check.DeepEquals, bson.M{"_id": "globocom"}) 181 | } 182 | -------------------------------------------------------------------------------- /db/redis/redis.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/backstage/beat/db" 9 | "github.com/backstage/beat/errors" 10 | "github.com/backstage/beat/schemas" 11 | "github.com/spf13/viper" 12 | "gopkg.in/redis.v4" 13 | ) 14 | 15 | var ( 16 | DbPrefix = "db" 17 | ErrNotImplemented = errors.New("Not Implemented for Redis", http.StatusNotImplemented) 18 | ) 19 | 20 | type Redis struct { 21 | *redis.Client 22 | } 23 | 24 | func init() { 25 | viper.SetDefault("redis.host", "localhost:6379") 26 | viper.SetDefault("redis.db", 0) 27 | 28 | db.Register("redis", func() (db.Database, error) { 29 | return New() 30 | }) 31 | } 32 | 33 | func New() (*Redis, error) { 34 | client := redis.NewClient(&redis.Options{ 35 | Addr: viper.GetString("redis.host"), 36 | Password: viper.GetString("redis.password"), 37 | DB: viper.GetInt("redis.db"), 38 | }) 39 | return &Redis{client}, nil 40 | } 41 | 42 | func (r *Redis) CreateItemSchema(itemSchema *schemas.ItemSchema) errors.Error { 43 | return r.createResource(schemas.ItemSchemaCollectionName, itemSchema.CollectionName, itemSchema) 44 | } 45 | 46 | func (r *Redis) UpdateItemSchema(*schemas.ItemSchema) errors.Error { 47 | return ErrNotImplemented 48 | } 49 | 50 | func (r *Redis) FindItemSchema(*db.Filter) (*db.ItemSchemasReply, errors.Error) { 51 | return nil, ErrNotImplemented 52 | } 53 | 54 | func (r *Redis) FindOneItemSchema(*db.Filter) (*schemas.ItemSchema, errors.Error) { 55 | return nil, ErrNotImplemented 56 | } 57 | 58 | func (r *Redis) FindItemSchemaByCollectionName(collectionName string) (*schemas.ItemSchema, errors.Error) { 59 | itemSchema := &schemas.ItemSchema{} 60 | err := r.getResource(schemas.ItemSchemaCollectionName, collectionName, itemSchema) 61 | if err == redis.Nil { 62 | return nil, db.ErrItemSchemaNotFound 63 | } else if err != nil { 64 | return nil, errors.Wraps(err, http.StatusInternalServerError) 65 | } 66 | return itemSchema, nil 67 | } 68 | 69 | func (r *Redis) DeleteItemSchema(collectionName string) errors.Error { 70 | err := r.deleteResource(schemas.ItemSchemaCollectionName, collectionName) 71 | if err == redis.Nil { 72 | return db.ErrItemSchemaNotFound 73 | } else if err != nil { 74 | return errors.Wraps(err, http.StatusInternalServerError) 75 | } 76 | 77 | return nil 78 | } 79 | 80 | func (r *Redis) createResource(collectionName string, primaryKey string, result interface{}) errors.Error { 81 | buf, err := json.Marshal(result) 82 | if err != nil { 83 | return errors.Wraps(err, http.StatusBadRequest) 84 | } 85 | created, err := r.SetNX(r.key(collectionName, primaryKey), string(buf), 0).Result() 86 | 87 | if err == redis.Nil || !created { 88 | validationError := &errors.ValidationError{} 89 | validationError.Put("_all", "Duplicated resource") 90 | return validationError 91 | } else if err != nil { 92 | return errors.Wraps(err, http.StatusInternalServerError) 93 | } 94 | 95 | return nil 96 | } 97 | 98 | func (r *Redis) getResource(collectionName string, primaryKey string, result interface{}) error { 99 | reply, err := r.Get(r.key(collectionName, primaryKey)).Bytes() 100 | 101 | if err != nil { 102 | return err 103 | } 104 | 105 | return json.Unmarshal(reply, result) 106 | } 107 | 108 | func (r *Redis) deleteResource(collectionName string, primaryKey string) error { 109 | reply, err := r.Del(r.key(collectionName, primaryKey)).Result() 110 | 111 | if err != nil { 112 | return err 113 | } 114 | 115 | if reply == 0 { 116 | return redis.Nil 117 | } 118 | 119 | return nil 120 | } 121 | 122 | func (r *Redis) key(collectionName string, primaryKey string) string { 123 | return fmt.Sprintf("%s:%s:%s", DbPrefix, collectionName, primaryKey) 124 | } 125 | -------------------------------------------------------------------------------- /db/redis/redis_test.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "net/http" 5 | "os" 6 | "testing" 7 | 8 | "github.com/backstage/beat/db" 9 | "github.com/backstage/beat/schemas" 10 | "gopkg.in/check.v1" 11 | ) 12 | 13 | var _ = check.Suite(&S{}) 14 | 15 | type S struct { 16 | Db *Redis 17 | } 18 | 19 | func Test(t *testing.T) { 20 | check.TestingT(t) 21 | } 22 | 23 | func (s *S) SetUpSuite(c *check.C) { 24 | var err error 25 | 26 | os.Setenv("REDIS_DB", "1") 27 | s.Db, err = New() 28 | s.Db.FlushDb() 29 | c.Assert(err, check.IsNil) 30 | } 31 | 32 | func (s *S) TestImplementInterface(c *check.C) { 33 | var dbType db.Database 34 | c.Assert(s.Db, check.Implements, &dbType) 35 | } 36 | 37 | func (s *S) TestCreateItemSchema(c *check.C) { 38 | itemSchema := &schemas.ItemSchema{CollectionName: "test-schema"} 39 | dbErr := s.Db.CreateItemSchema(itemSchema) 40 | c.Assert(dbErr, check.IsNil) 41 | 42 | itemSchema, dbErr = s.Db.FindItemSchemaByCollectionName("test-schema") 43 | c.Assert(dbErr, check.IsNil) 44 | c.Assert(itemSchema.CollectionName, check.Equals, "test-schema") 45 | } 46 | 47 | func (s *S) TestCreateItemSchemaDuplicated(c *check.C) { 48 | itemSchema := &schemas.ItemSchema{CollectionName: "duplicated-schema"} 49 | dbErr := s.Db.CreateItemSchema(itemSchema) 50 | 51 | c.Assert(dbErr, check.IsNil) 52 | 53 | dbErr = s.Db.CreateItemSchema(itemSchema) 54 | c.Assert(dbErr, check.NotNil) 55 | c.Assert(dbErr.StatusCode(), check.Equals, 422) 56 | c.Assert(dbErr.Error(), check.Equals, "_all: Duplicated resource") 57 | } 58 | 59 | func (s *S) TestFindItemSchemaByCollectionNameWithNotFound(c *check.C) { 60 | _, dbErr := s.Db.FindItemSchemaByCollectionName("not-found") 61 | c.Assert(dbErr, check.NotNil) 62 | c.Assert(dbErr.StatusCode(), check.Equals, http.StatusNotFound) 63 | } 64 | 65 | func (s *S) TestDeleteItemSchemaByCollectionNameWithNotFound(c *check.C) { 66 | dbErr := s.Db.DeleteItemSchema("not-found") 67 | c.Assert(dbErr, check.NotNil) 68 | c.Assert(dbErr.StatusCode(), check.Equals, http.StatusNotFound) 69 | } 70 | 71 | func (s *S) TestDeleteItemSchemaByCollectionName(c *check.C) { 72 | dbErr := s.Db.CreateItemSchema(&schemas.ItemSchema{CollectionName: "to-be-deleted"}) 73 | c.Assert(dbErr, check.IsNil) 74 | 75 | dbErr = s.Db.DeleteItemSchema("to-be-deleted") 76 | c.Assert(dbErr, check.IsNil) 77 | 78 | _, dbErr = s.Db.FindItemSchemaByCollectionName("to-be-deleted") 79 | c.Assert(dbErr, check.NotNil) 80 | c.Assert(dbErr.StatusCode(), check.Equals, http.StatusNotFound) 81 | } 82 | 83 | func (s *S) TestFindItemSchema(c *check.C) { 84 | reply, dbErr := s.Db.FindItemSchema(nil) 85 | 86 | c.Assert(reply, check.IsNil) 87 | c.Assert(dbErr, check.NotNil) 88 | 89 | c.Assert(dbErr.StatusCode(), check.Equals, http.StatusNotImplemented) 90 | c.Assert(dbErr.Error(), check.Equals, "Not Implemented for Redis") 91 | } 92 | 93 | func (s *S) TestFindOneItemSchema(c *check.C) { 94 | reply, dbErr := s.Db.FindOneItemSchema(nil) 95 | 96 | c.Assert(reply, check.IsNil) 97 | c.Assert(dbErr, check.NotNil) 98 | 99 | c.Assert(dbErr.StatusCode(), check.Equals, http.StatusNotImplemented) 100 | c.Assert(dbErr.Error(), check.Equals, "Not Implemented for Redis") 101 | } 102 | -------------------------------------------------------------------------------- /docs/configuration.md: -------------------------------------------------------------------------------- 1 | # Settings 2 | 3 | Backstage Beat is very configurable, all configurations are available below. 4 | 5 | ## Core 6 | 7 | | Yaml path | Enviroment variable | Default value | Description | 8 | | ------------- |---------------------| ---------------|-------------| 9 | | log.level | LOG_LEVEL | info | Application log level, are available: debug, info, warn, error, fatal and panic. | 10 | | host | HOST | 0.0.0.0 | Host to serve API. | 11 | | port | PORT | 3000 | Port to serve API. | 12 | | database | DATABASE | mongo | Database engine to use, are currently available: `mongo` and `redis` . | 13 | | authentication| AUTHENTICATION | static | Authentication engine to use, is currently available: `static` . | 14 | 15 | 16 | ## Database Engines 17 | 18 | ### MongoDB 19 | 20 | | Yaml path | Enviroment variable | Default value | Description | 21 | | -------------- |---------------------| -------------------------------------|-------------| 22 | | mongo.uri | MONGO_URI | localhost:27017/backstage_beat_local | Database URL. | 23 | | mongo.user | MONGO_USER | None | Username. | 24 | | mongo.password | MONGO_PASSWORD | None | Password. | 25 | | mongo.failFast | MONGO_FAILFAST | true | Cause connection and query attempts to fail faster when the server is unavailable. | 26 | 27 | 28 | ### Redis 29 | 30 | | Yaml path | Enviroment variable | Default value | Description | 31 | | ---------------------- |----------------------- | ---------------|-------------| 32 | | redis.host | REDIS_HOST | localhost:6379 | Host and port. | 33 | | redis.db | REDIS_DB | 0 | Database. | 34 | | redis.password | REDIS_PASSWORD | | Password. | 35 | -------------------------------------------------------------------------------- /docs/getting-started.md: -------------------------------------------------------------------------------- 1 | # Installing 2 | 3 | ## Requirements 4 | 5 | - Go 1.5+ 6 | - MongoDB 3+ 7 | 8 | ## Download and install the development version 9 | 10 | > Ensure that you have your `GOPATH` environment variable properly configured. Check the [Go docs](https://golang.org/doc/code.html#GOPATH) to see how to id: 11 | 12 | ```bash 13 | go get "github.com/backstage/beat/beat" 14 | cd $GOPATH/src/github.com/backstage/beat 15 | make setup 16 | ``` 17 | 18 | ## Running locally 19 | 20 | ``` 21 | make run 22 | ``` 23 | 24 | ## Using (with `curl`) 25 | 26 | ### Create a new collection 27 | 28 | To dynamically define a new collection, just create a new instance of the `ItemSchema`. You can do this using the REST interface to `POST` a valid JSON Schema. First define your schema as below: 29 | 30 | ##### `schema.json` 31 | 32 | ```json 33 | { 34 | "collectionName": "people", 35 | "globalCollectionName": true, 36 | "type": "object", 37 | "title": "Person", 38 | "collectionTitle": "People", 39 | "properties": { 40 | "name": { 41 | "type": "string" 42 | }, 43 | "email": { 44 | "type": "string", 45 | "format": "email" 46 | } 47 | } 48 | } 49 | ``` 50 | 51 | Then you can create a `Person` collection by POSTing the JSON Schema above: 52 | 53 | ```bash 54 | curl -i -XPOST -H "Content-Type: application/json" http://beat-service-example.org/api/item-schemas -T schema.json 55 | ``` 56 | 57 | That is it. The RESTful API will then be available at http://beat-service-example.org/api/people. 58 | 59 | #### Default links 60 | 61 | Each Item schema have a default set of links which correspond to the basic CRUD operations supported by Backstage Beat. For example: 62 | 63 | ```bash 64 | $ curl http://beat-service-example.org/api/item-schemas/people 65 | ``` 66 | 67 | returns 68 | ```json 69 | { 70 | "$schema": "http://json-schema.org/draft-04/hyper-schema#", 71 | "collectionName": "people", 72 | ... 73 | "links": [ 74 | { 75 | "rel": "self", 76 | "href": "http://beat-service-example.org/api/people/{id}" 77 | }, 78 | { 79 | "rel": "item", 80 | "href": "http://beat-service-example.org/api/people/{id}" 81 | }, 82 | { 83 | "rel": "create", 84 | "href": "http://beat-service-example.org/api/people", 85 | "method": "POST", 86 | "schema": { 87 | "$ref": "http://beat-service-example.org/api/item-schemas/people" 88 | } 89 | }, 90 | { 91 | "rel": "update", 92 | "href": "http://beat-service-example.org/api/people/{id}", 93 | "method": "PUT" 94 | }, 95 | { 96 | "rel": "delete", 97 | "href": "http://beat-service-example.org/api/people/{id}", 98 | "method": "DELETE" 99 | }, 100 | { 101 | "rel": "parent", 102 | "href": "http://beat-service-example.org/api/people" 103 | } 104 | ] 105 | } 106 | ``` 107 | 108 | #### Including custom links in an Item Schema 109 | 110 | It is possible to include custom links in an Item Schema. To do so, just include them in the links property of your JSON: 111 | 112 | ```json 113 | { 114 | "type": "object", 115 | ... 116 | "properties": { 117 | ... 118 | }, 119 | "links": [ 120 | { 121 | "rel": "my-custom-item-schema-link", 122 | "href": "http://example.org/my/custom/item-schema-link" 123 | } 124 | ] 125 | } 126 | ``` 127 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | ## What is Backstage Beat? 2 | 3 | Backstage Beat is a Backend-as-a-Service software that makes mobile and web development really fast using restful APIs. 4 | 5 | **Attention: Beat is in early development and is not ready for production yet.** 6 | 7 | ### Related projects 8 | 9 | Backstage Beat is inspired by the following projects: 10 | 11 | * [StrongLoop/Loopback](https://github.com/strongloop/loopback) 12 | * [Parse.com](http://parse.com/) 13 | -------------------------------------------------------------------------------- /errors/errors.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "encoding/json" 5 | originalErrors "errors" 6 | "fmt" 7 | "strings" 8 | ) 9 | 10 | // Error is a interface to inform the user about the error ocorred. 11 | type Error interface { 12 | StatusCode() int 13 | Error() string // to implement the common error interface 14 | } 15 | 16 | // wrappedError implements Error, created only for wraps common golang 17 | // errors in this interface. 18 | type wrappedError struct { 19 | originalError error 20 | statusCode int 21 | } 22 | 23 | // Wraps return new Error based on a common golang error. 24 | func Wraps(err error, statusCode int) Error { 25 | return &wrappedError{originalError: err, statusCode: statusCode} 26 | } 27 | 28 | func (w *wrappedError) Error() string { 29 | return w.originalError.Error() 30 | } 31 | 32 | // StatusCode return the code to be used in http response. 33 | func (w *wrappedError) StatusCode() int { 34 | return w.statusCode 35 | } 36 | 37 | // MarshalJSON returns the json format or Error 38 | func (w *wrappedError) MarshalJSON() ([]byte, error) { 39 | errorDescription := map[string]interface{}{ 40 | "_all": []string{ 41 | w.Error(), 42 | }, 43 | } 44 | data := map[string]interface{}{ 45 | "errors": []interface{}{ 46 | errorDescription, 47 | }, 48 | } 49 | return json.Marshal(&data) 50 | } 51 | 52 | // New returns new Error based on string. 53 | func New(text string, statusCode int) Error { 54 | return Wraps(originalErrors.New(text), statusCode) 55 | } 56 | 57 | // Newf returns new Error based on string formated by fmt.Errorf. 58 | func Newf(statusCode int, text string, params ...interface{}) Error { 59 | return Wraps(fmt.Errorf(text, params...), statusCode) 60 | } 61 | 62 | // ValidationError is another implementation of Error interface. 63 | // used to group field errors in one. 64 | type ValidationError struct { 65 | items []validateItemError 66 | } 67 | 68 | func (v *ValidationError) StatusCode() int { 69 | return 422 70 | } 71 | 72 | func (v *ValidationError) Error() string { 73 | parts := []string{} 74 | 75 | for _, item := range v.items { 76 | parts = append( 77 | parts, 78 | fmt.Sprintf("%s: %s", item.field, item.message), 79 | ) 80 | } 81 | 82 | return strings.Join(parts, ", ") 83 | } 84 | 85 | func (v *ValidationError) Length() int { 86 | return len(v.items) 87 | } 88 | 89 | func (v *ValidationError) Put(field, message string) { 90 | item := validateItemError{field: field, message: message} 91 | v.items = append(v.items, item) 92 | } 93 | 94 | func (v *ValidationError) MarshalJSON() ([]byte, error) { 95 | errorParts := []interface{}{} 96 | for _, item := range v.items { 97 | errorParts = append(errorParts, map[string]interface{}{ 98 | item.field: []string{ 99 | item.message, 100 | }, 101 | }) 102 | } 103 | data := map[string]interface{}{ 104 | "errors": errorParts, 105 | } 106 | return json.Marshal(&data) 107 | } 108 | 109 | type validateItemError struct { 110 | field string 111 | message string 112 | } 113 | -------------------------------------------------------------------------------- /errors/errors_test.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "encoding/json" 5 | originalErrors "errors" 6 | "net/http" 7 | "testing" 8 | 9 | simplejson "github.com/bitly/go-simplejson" 10 | "gopkg.in/check.v1" 11 | ) 12 | 13 | var _ = check.Suite(&S{}) 14 | 15 | type S struct{} 16 | 17 | func Test(t *testing.T) { 18 | check.TestingT(t) 19 | } 20 | 21 | func (s *S) TestWrapsNewError(c *check.C) { 22 | err := Wraps(originalErrors.New("test error 123"), http.StatusInternalServerError) 23 | c.Assert(err, check.Not(check.IsNil)) 24 | c.Assert(err.Error(), check.Equals, "test error 123") 25 | c.Assert(err.StatusCode(), check.Equals, http.StatusInternalServerError) 26 | } 27 | 28 | func (s *S) TestNewError(c *check.C) { 29 | err := New("test error 123", http.StatusInternalServerError) 30 | c.Assert(err, check.Not(check.IsNil)) 31 | c.Assert(err.Error(), check.Equals, "test error 123") 32 | c.Assert(err.StatusCode(), check.Equals, http.StatusInternalServerError) 33 | } 34 | 35 | func (s *S) TestNewfError(c *check.C) { 36 | err := Newf(http.StatusInternalServerError, "test error %s", "123") 37 | c.Assert(err, check.Not(check.IsNil)) 38 | c.Assert(err.Error(), check.Equals, "test error 123") 39 | c.Assert(err.StatusCode(), check.Equals, http.StatusInternalServerError) 40 | } 41 | 42 | func (s *S) TestMarshallJSONWrappedError(c *check.C) { 43 | errWrapped := Wraps(originalErrors.New("test error 123"), http.StatusInternalServerError) 44 | 45 | data, err1 := json.Marshal(errWrapped) 46 | c.Assert(err1, check.IsNil) 47 | 48 | errJSON, err2 := simplejson.NewJson(data) 49 | c.Assert(err2, check.IsNil) 50 | 51 | msg, err3 := errJSON.Get("errors").GetIndex(0).Get("_all").GetIndex(0).String() 52 | c.Assert(err3, check.IsNil) 53 | c.Assert(msg, check.Equals, "test error 123") 54 | } 55 | 56 | func (s *S) TestValidationError(c *check.C) { 57 | err := &ValidationError{} 58 | c.Assert(err.StatusCode(), check.Equals, 422) 59 | c.Assert(err.Error(), check.Equals, "") 60 | c.Assert(err.Length(), check.Equals, 0) 61 | 62 | err.Put("name", "is required") 63 | c.Assert(err.Error(), check.Equals, "name: is required") 64 | c.Assert(err.Length(), check.Equals, 1) 65 | } 66 | 67 | func (s *S) TestValidationErrorMarshallJSON(c *check.C) { 68 | err := &ValidationError{} 69 | err.Put("name", "is required") 70 | 71 | data, err1 := json.Marshal(err) 72 | c.Assert(err1, check.IsNil) 73 | 74 | errJSON, err2 := simplejson.NewJson(data) 75 | c.Assert(err2, check.IsNil) 76 | 77 | msg, err3 := errJSON.Get("errors").GetIndex(0).Get("name").GetIndex(0).String() 78 | c.Assert(err3, check.IsNil) 79 | c.Assert(msg, check.Equals, "is required") 80 | } 81 | -------------------------------------------------------------------------------- /examples/config.yml: -------------------------------------------------------------------------------- 1 | auth: 2 | tokens: 3 | example1: 4 | email: admin@example.net 5 | 6 | example2: 7 | email: guest@example.net 8 | -------------------------------------------------------------------------------- /go.mk: -------------------------------------------------------------------------------- 1 | # go.mk 2 | # 3 | # Copyright (c) 2015, Herbert G. Fischer 4 | # All rights reserved. 5 | # 6 | # Redistribution and use in source and binary forms, with or without 7 | # modification, are permitted provided that the following conditions are met: 8 | # * Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # * Redistributions in binary form must reproduce the above copyright 11 | # notice, this list of conditions and the following disclaimer in the 12 | # documentation and/or other materials provided with the distribution. 13 | # * Neither the name of the organization nor the 14 | # names of its contributors may be used to endorse or promote products 15 | # derived from this software without specific prior written permission. 16 | # 17 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 18 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 19 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | # DISCLAIMED. IN NO EVENT SHALL HERBERT G. FISCHER BE LIABLE FOR ANY 21 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 22 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 23 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 24 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 26 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | 28 | APPBIN := $(shell basename $(PWD)) 29 | GOSOURCES := $(shell find . -type f -name '*.go' ! -path '*Godeps/_workspace*') 30 | GOPKGS := $(shell go list ./... 2>/dev/null) 31 | GOPKG := $(shell go list 2>/dev/null) 32 | COVERAGEOUT := coverage.out 33 | COVERAGETMP := coverage.tmp 34 | GODEPPATH := $(PWD)/Godeps/_workspace 35 | LOCALGOPATH := $(GODEPPATH):$(GOPATH) 36 | ORIGGOPATH := $(GOPATH) 37 | GOMKVERSION := 0.8.1 38 | 39 | # Check GOPATH 40 | ifndef GOPATH 41 | $(error ERROR!! GOPATH must be declared. Check [http://golang.org/doc/code.html#GOPATH]) 42 | endif 43 | 44 | # Check GOBIN, and automatically export it 45 | ifndef GOBIN 46 | export GOBIN=$(GOPATH)/bin 47 | GOBIN := $(GOBIN) 48 | endif 49 | 50 | # Include GODEPPATH 51 | export GOPATH=$(LOCALGOPATH) 52 | 53 | # Check current path 54 | ifeq ($(shell go list ./... 2>/dev/null | grep -q '^_'; echo $$?), 0) 55 | $(error ERROR!! This directory should be at $(GOPATH)/src/$(REPO)) 56 | endif 57 | 58 | 59 | .PHONY: gomkhelp 60 | gomkhelp: 61 | $(info Available go.mk targets: ) 62 | $(info | gomkhelp ) 63 | $(info | gomkbuild ) 64 | $(info | gomkxbuild ) 65 | $(info | gomkclean ) 66 | $(info | gomkupdate ) 67 | $(info | vet ) 68 | $(info | lint ) 69 | $(info | fmt ) 70 | $(info | test ) 71 | $(info | bench ) 72 | $(info | race ) 73 | $(info | deps ) 74 | $(info | cover ) 75 | $(info | present ) 76 | $(info | savegodeps ) 77 | $(info | restoregodeps ) 78 | $(info | updategodeps ) 79 | $(info | printvars ) 80 | @exit 0 81 | 82 | 83 | ########################################################################################## 84 | ## Project targets 85 | ########################################################################################## 86 | 87 | $(APPBIN): gomkbuild 88 | 89 | ########################################################################################## 90 | ## Main targets 91 | ########################################################################################## 92 | 93 | .PHONY: gomkbuild 94 | gomkbuild: $(GOSOURCES) ; @go build 95 | 96 | .PHONY: gomkxbuild 97 | gomkxbuild: ; $(GOX) 98 | 99 | .PHONY: gomkenv 100 | gomkenv: ; @go env 101 | 102 | .PHONY: gomkclean 103 | gomkclean: 104 | @rm -vf $(APPBIN)_*_386 $(APPBIN)_*_amd64 $(APPBIN)_*_arm $(APPBIN)_*.exe 105 | @rm -vf $(COVERAGEOUT) $(COVERAGETMP) 106 | @go clean 107 | 108 | .PHONY: gomkupdate 109 | gomkupdate: 110 | @curl -o go.mk https://raw.githubusercontent.com/hgfischer/gomk/master/go.mk?$(shell date +%s) 111 | 112 | ########################################################################################## 113 | ## Go tools 114 | ########################################################################################## 115 | 116 | GOTOOLDIR := $(shell go env GOTOOLDIR) 117 | BENCHCMP := $(GOTOOLDIR)/benchcmp 118 | CALLGRAPH := $(GOTOOLDIR)/callgraph 119 | COVER := $(GOTOOLDIR)/cover 120 | DIGRAPH := $(GOTOOLDIR)/digraph 121 | EG := $(GOTOOLDIR)/eg 122 | GODEX := $(GOTOOLDIR)/godex 123 | GODOC := $(GOTOOLDIR)/godoc 124 | GOIMPORTS := $(GOTOOLDIR)/goimports 125 | GOMVPKG := $(GOTOOLDIR)/gomvpkg 126 | GOTYPE := $(GOTOOLDIR)/gotype 127 | ORACLE := $(GOTOOLDIR)/oracle 128 | SSADUMP := $(GOTOOLDIR)/ssadump 129 | STRINGER := $(GOTOOLDIR)/stringer 130 | VET := $(GOTOOLDIR)/vet 131 | GOX := $(GOBIN)/gox 132 | LINT := $(GOBIN)/lint 133 | GODEP := $(GOBIN)/godep 134 | PRESENT := $(GOBIN)/present 135 | 136 | $(BENCHCMP) : ; @go get -v golang.org/x/tools/cmd/benchcmp 137 | $(CALLGRAPH) : ; @go get -v golang.org/x/tools/cmd/callgraph 138 | $(COVER) : ; @go get -v golang.org/x/tools/cmd/cover 139 | $(DIGRAPH) : ; @go get -v golang.org/x/tools/cmd/digraph 140 | $(EG) : ; @go get -v golang.org/x/tools/cmd/eg 141 | $(GODEX) : ; @go get -v golang.org/x/tools/cmd/godex 142 | $(GODOC) : ; @go get -v golang.org/x/tools/cmd/godoc 143 | $(GOIMPORTS) : ; @go get -v golang.org/x/tools/cmd/goimports 144 | $(GOMVPKG) : ; @go get -v golang.org/x/tools/cmd/gomvpkgs 145 | $(GOTYPE) : ; @go get -v golang.org/x/tools/cmd/gotype 146 | $(ORACLE) : ; @go get -v golang.org/x/tools/cmd/oracle 147 | $(SSADUMP) : ; @go get -v golang.org/x/tools/cmd/ssadump 148 | $(STRINGER) : ; @go get -v golang.org/x/tools/cmd/stringer 149 | $(VET) : ; @go get -v golang.org/x/tools/cmd/vet 150 | $(PRESENT) : ; @go get -v golang.org/x/tools/cmd/present 151 | $(LINT) : ; @go get -v github.com/golang/lint/golint 152 | $(GOX) : ; @go get -v github.com/mitchellh/gox 153 | $(GODEP) : ; @go get -v github.com/tools/godep 154 | 155 | .PHONY: vet 156 | vet: $(VET) ; @for src in $(GOSOURCES); do GOPATH=$(ORIGGOPATH) go tool vet $$src; done 157 | 158 | .PHONY: lint 159 | lint: $(LINT) ; @for src in $(GOSOURCES); do GOPATH=$(ORIGGOPATH) golint $$src || exit 1; done 160 | 161 | .PHONY: fmt 162 | fmt: ; @GOPATH=$(ORIGGOPATH) go fmt ./... 163 | 164 | .PHONY: test 165 | test: ; @go test -v ./... 166 | 167 | .PHONY: bench 168 | bench: ; @go test -v -bench=. ./... 169 | 170 | .PHONY: race 171 | race: ; @for pkg in $(GOPKGS); do go test -v -race $$pkg || exit 1; done 172 | 173 | .PHONY: deps 174 | deps: ; @GOPATH=$(ORIGGOPATH) go get -u -v -t ./... 175 | 176 | .PHONY: cover 177 | cover: $(COVER) 178 | @echo 'mode: set' > $(COVERAGEOUT) 179 | @for pkg in $(GOPKGS); do \ 180 | go test -v -coverprofile=$(COVERAGETMP) $$pkg; \ 181 | if [ -f $(COVERAGETMP) ]; then \ 182 | grep -v 'mode: set' $(COVERAGETMP) >> $(COVERAGEOUT); \ 183 | rm $(COVERAGETMP); \ 184 | fi; \ 185 | done 186 | @go tool cover -html=$(COVERAGEOUT) 187 | 188 | .PHONY: present 189 | present: $(PRESENT) 190 | @present 191 | 192 | ########################################################################################## 193 | ## Godep support 194 | ########################################################################################## 195 | 196 | .PHONY: savegodeps 197 | savegodeps: $(GODEP) ; @GOPATH=$(ORIGGOPATH) $(GODEP) save ./... 198 | 199 | .PHONY: restoregodeps 200 | restoregodeps: $(GODEP) ; @GOPATH=$(ORIGGOPATH) $(GODEP) restore 201 | 202 | .PHONY: updategodeps 203 | updategodeps: $(GODEP) ; @GOPATH=$(ORIGGOPATH) $(GODEP) update ./... 204 | 205 | ########################################################################################## 206 | ## Make utilities 207 | ########################################################################################## 208 | 209 | .PHONY: printvars 210 | printvars: 211 | @$(foreach V, $(sort $(.VARIABLES)), $(if $(filter-out environment% default automatic, $(origin $V)), $(warning $V=$($V) ))) 212 | @exit 0 213 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Backstage Beat 2 | theme: readthedocs 3 | repo_url: https://github.com/backstage/beat 4 | site_description: Backstage Beat is an open-source backend-as-a-service in heavy development by Globo.com 5 | site_author: Backstage Developers 6 | copyright: Backstage is Globo.com's Open Source CMS -------------------------------------------------------------------------------- /mocks/mock_db/mock_db.go: -------------------------------------------------------------------------------- 1 | // Automatically generated by MockGen. DO NOT EDIT! 2 | // Source: github.com/backstage/beat/db (interfaces: Database) 3 | 4 | package mock_db 5 | 6 | import ( 7 | db "github.com/backstage/beat/db" 8 | errors "github.com/backstage/beat/errors" 9 | schemas "github.com/backstage/beat/schemas" 10 | gomock "github.com/golang/mock/gomock" 11 | ) 12 | 13 | // Mock of Database interface 14 | type MockDatabase struct { 15 | ctrl *gomock.Controller 16 | recorder *_MockDatabaseRecorder 17 | } 18 | 19 | // Recorder for MockDatabase (not exported) 20 | type _MockDatabaseRecorder struct { 21 | mock *MockDatabase 22 | } 23 | 24 | func NewMockDatabase(ctrl *gomock.Controller) *MockDatabase { 25 | mock := &MockDatabase{ctrl: ctrl} 26 | mock.recorder = &_MockDatabaseRecorder{mock} 27 | return mock 28 | } 29 | 30 | func (_m *MockDatabase) EXPECT() *_MockDatabaseRecorder { 31 | return _m.recorder 32 | } 33 | 34 | func (_m *MockDatabase) CreateItemSchema(_param0 *schemas.ItemSchema) errors.Error { 35 | ret := _m.ctrl.Call(_m, "CreateItemSchema", _param0) 36 | ret0, _ := ret[0].(errors.Error) 37 | return ret0 38 | } 39 | 40 | func (_mr *_MockDatabaseRecorder) CreateItemSchema(arg0 interface{}) *gomock.Call { 41 | return _mr.mock.ctrl.RecordCall(_mr.mock, "CreateItemSchema", arg0) 42 | } 43 | 44 | func (_m *MockDatabase) DeleteItemSchema(_param0 string) errors.Error { 45 | ret := _m.ctrl.Call(_m, "DeleteItemSchema", _param0) 46 | ret0, _ := ret[0].(errors.Error) 47 | return ret0 48 | } 49 | 50 | func (_mr *_MockDatabaseRecorder) DeleteItemSchema(arg0 interface{}) *gomock.Call { 51 | return _mr.mock.ctrl.RecordCall(_mr.mock, "DeleteItemSchema", arg0) 52 | } 53 | 54 | func (_m *MockDatabase) FindItemSchema(_param0 *db.Filter) (*db.ItemSchemasReply, errors.Error) { 55 | ret := _m.ctrl.Call(_m, "FindItemSchema", _param0) 56 | ret0, _ := ret[0].(*db.ItemSchemasReply) 57 | ret1, _ := ret[1].(errors.Error) 58 | return ret0, ret1 59 | } 60 | 61 | func (_mr *_MockDatabaseRecorder) FindItemSchema(arg0 interface{}) *gomock.Call { 62 | return _mr.mock.ctrl.RecordCall(_mr.mock, "FindItemSchema", arg0) 63 | } 64 | 65 | func (_m *MockDatabase) FindItemSchemaByCollectionName(_param0 string) (*schemas.ItemSchema, errors.Error) { 66 | ret := _m.ctrl.Call(_m, "FindItemSchemaByCollectionName", _param0) 67 | ret0, _ := ret[0].(*schemas.ItemSchema) 68 | ret1, _ := ret[1].(errors.Error) 69 | return ret0, ret1 70 | } 71 | 72 | func (_mr *_MockDatabaseRecorder) FindItemSchemaByCollectionName(arg0 interface{}) *gomock.Call { 73 | return _mr.mock.ctrl.RecordCall(_mr.mock, "FindItemSchemaByCollectionName", arg0) 74 | } 75 | 76 | func (_m *MockDatabase) FindOneItemSchema(_param0 *db.Filter) (*schemas.ItemSchema, errors.Error) { 77 | ret := _m.ctrl.Call(_m, "FindOneItemSchema", _param0) 78 | ret0, _ := ret[0].(*schemas.ItemSchema) 79 | ret1, _ := ret[1].(errors.Error) 80 | return ret0, ret1 81 | } 82 | 83 | func (_mr *_MockDatabaseRecorder) FindOneItemSchema(arg0 interface{}) *gomock.Call { 84 | return _mr.mock.ctrl.RecordCall(_mr.mock, "FindOneItemSchema", arg0) 85 | } 86 | 87 | func (_m *MockDatabase) UpdateItemSchema(_param0 *schemas.ItemSchema) errors.Error { 88 | ret := _m.ctrl.Call(_m, "UpdateItemSchema", _param0) 89 | ret0, _ := ret[0].(errors.Error) 90 | return ret0 91 | } 92 | 93 | func (_mr *_MockDatabaseRecorder) UpdateItemSchema(arg0 interface{}) *gomock.Call { 94 | return _mr.mock.ctrl.RecordCall(_mr.mock, "UpdateItemSchema", arg0) 95 | } 96 | -------------------------------------------------------------------------------- /requirements_docs.txt: -------------------------------------------------------------------------------- 1 | mkdocs==0.15.0 2 | -------------------------------------------------------------------------------- /schemas/collection_schema.go: -------------------------------------------------------------------------------- 1 | package schemas 2 | 3 | import "fmt" 4 | 5 | type CollectionSchema struct { 6 | Schema string `json:"$schema"` 7 | CollectionName string `json:"collectionName"` 8 | Type string `json:"type"` 9 | Title string `json:"title,omitempty"` 10 | Properties colProps `json:"properties"` 11 | Links *Links `json:"links,omitempty"` 12 | } 13 | 14 | func NewCollectionSchema(itemSchema *ItemSchema) *CollectionSchema { 15 | collectionSchema := &CollectionSchema{ 16 | Schema: itemSchema.Schema, 17 | CollectionName: itemSchema.CollectionName, 18 | Type: "object", 19 | Title: itemSchema.CollectionTitle, 20 | Links: itemSchema.CollectionLinks, 21 | Properties: colProps{itemSchema.url()}, 22 | } 23 | 24 | customLinks := itemSchema.CollectionLinks 25 | collectionSchema.Links = collectionSchema.defaultLinks(itemSchema) 26 | 27 | if customLinks != nil { 28 | collectionSchema.Links = collectionSchema.Links.ConcatenateLinks(customLinks) 29 | } 30 | 31 | return collectionSchema 32 | } 33 | 34 | func (schema *CollectionSchema) ApplyBaseURL(baseURL string) { 35 | schema.Properties.ref = baseURL + schema.Properties.ref 36 | schema.Links.ApplyBaseURL(baseURL) 37 | } 38 | 39 | func (schema *CollectionSchema) defaultLinks(itemSchema *ItemSchema) *Links { 40 | collectionURL := itemSchema.collectionURL() 41 | itemSchemaURL := itemSchema.url() 42 | 43 | return &Links{ 44 | &Link{Rel: "self", Href: collectionURL}, 45 | &Link{Rel: "list", Href: collectionURL}, 46 | &Link{Rel: "add", Method: "POST", Href: collectionURL, 47 | Schema: map[string]interface{}{ 48 | "$ref": itemSchemaURL, 49 | }, 50 | }, 51 | &Link{ 52 | Rel: "previous", 53 | Href: fmt.Sprintf("%s?filter[perPage]={perPage}&filter[page]={previousPage}{&paginateQs*}", collectionURL), 54 | }, 55 | &Link{ 56 | Rel: "next", 57 | Href: fmt.Sprintf("%s?filter[perPage]={perPage}&filter[page]={nextPage}{&paginateQs*}", collectionURL), 58 | }, 59 | &Link{ 60 | Rel: "page", 61 | Href: fmt.Sprintf("%s?filter[perPage]={perPage}&filter[page]={page}{&paginateQs*}", collectionURL), 62 | }, 63 | &Link{ 64 | Rel: "order", 65 | Href: fmt.Sprintf("%s?filter[order]={orderAttribute}%s{orderDirection}{&orderQs*}", collectionURL, "%20"), 66 | }, 67 | } 68 | } 69 | 70 | type colProps struct { 71 | ref string 72 | } 73 | 74 | func (c colProps) MarshalJSON() ([]byte, error) { 75 | data := fmt.Sprintf(`{ 76 | "items": { 77 | "items": { 78 | "$ref": "%s" 79 | }, 80 | "type": "array" 81 | }, 82 | "limit": { 83 | "type": "integer" 84 | }, 85 | "previousOffset": { 86 | "type": "integer" 87 | }, 88 | "nextOffset": { 89 | "type": "integer" 90 | }, 91 | "perPage": { 92 | "type": "integer" 93 | }, 94 | "previousPage": { 95 | "type": "integer" 96 | }, 97 | "nextPage": { 98 | "type": "integer" 99 | }, 100 | "itemCount": { 101 | "type": "integer" 102 | }, 103 | "paginateQs": { 104 | "type": "object" 105 | }, 106 | "orderQs": { 107 | "type": "object" 108 | } 109 | }`, c.ref) 110 | return []byte(data), nil 111 | } 112 | -------------------------------------------------------------------------------- /schemas/collection_schema_test.go: -------------------------------------------------------------------------------- 1 | package schemas 2 | 3 | import ( 4 | "strings" 5 | 6 | "gopkg.in/check.v1" 7 | ) 8 | 9 | var _ = check.Suite(&CollectionSchemaSuite{}) 10 | 11 | type CollectionSchemaSuite struct { 12 | collectionSchema *CollectionSchema 13 | } 14 | 15 | func (s *CollectionSchemaSuite) SetUpTest(c *check.C) { 16 | schema := `{ 17 | "$schema": "http://json-schema.org/draft-04/hyper-schema#", 18 | "collectionName": "backstage-users", 19 | "collectionTitle": "my collection Title", 20 | "type": "object", 21 | "collectionLinks": [ 22 | {"rel": "top10", "href": "http://github.com/jedi"}, 23 | {"rel": "history", "href": "/juniors"} 24 | ] 25 | }` 26 | reader := strings.NewReader(schema) 27 | itemSchema, err := NewItemSchemaFromReader(reader) 28 | c.Assert(err, check.IsNil) 29 | 30 | s.collectionSchema = NewCollectionSchema(itemSchema) 31 | } 32 | 33 | var ( 34 | DefaultCollectionSchemaLinks = Links{ 35 | &Link{Rel: "self", Href: "/backstage-users"}, 36 | &Link{Rel: "list", Href: "/backstage-users"}, 37 | &Link{Rel: "add", Href: "/backstage-users", Method: "POST", 38 | Schema: map[string]interface{}{ 39 | "$ref": "/item-schemas/backstage-users", 40 | }, 41 | }, 42 | &Link{Rel: "previous", Href: "/backstage-users?filter[perPage]={perPage}&filter[page]={previousPage}{&paginateQs*}"}, 43 | &Link{Rel: "next", Href: "/backstage-users?filter[perPage]={perPage}&filter[page]={nextPage}{&paginateQs*}"}, 44 | &Link{Rel: "page", Href: "/backstage-users?filter[perPage]={perPage}&filter[page]={page}{&paginateQs*}"}, 45 | &Link{Rel: "order", Href: "/backstage-users?filter[order]={orderAttribute}%20{orderDirection}{&orderQs*}"}, 46 | } 47 | ) 48 | 49 | func (s *CollectionSchemaSuite) TestNewCollectionSchemaProperties(c *check.C) { 50 | 51 | c.Assert(s.collectionSchema.Schema, check.Equals, "http://json-schema.org/draft-04/hyper-schema#") 52 | c.Assert(s.collectionSchema.CollectionName, check.Equals, "backstage-users") 53 | c.Assert(s.collectionSchema.Title, check.Equals, "my collection Title") 54 | c.Assert(s.collectionSchema.Type, check.Equals, "object") 55 | } 56 | 57 | func (s *CollectionSchemaSuite) TestNewCollectionSchemaLinks(c *check.C) { 58 | for i, expectedLink := range DefaultCollectionSchemaLinks { 59 | link := *(*s.collectionSchema.Links)[i] 60 | c.Assert(link, check.DeepEquals, *expectedLink) 61 | } 62 | 63 | lenDefaultLinks := len(DefaultCollectionSchemaLinks) 64 | 65 | link := *(*s.collectionSchema.Links)[lenDefaultLinks] 66 | c.Assert(link, check.DeepEquals, Link{Rel: "top10", Href: "http://github.com/jedi"}) 67 | 68 | link = *(*s.collectionSchema.Links)[lenDefaultLinks+1] 69 | c.Assert(link, check.DeepEquals, Link{Rel: "history", Href: "/juniors"}) 70 | 71 | } 72 | 73 | func (s *CollectionSchemaSuite) TestNewCollectionSchemaApplyBaseUrl(c *check.C) { 74 | s.collectionSchema.ApplyBaseURL("https://my-beat.com/api") 75 | 76 | c.Assert(s.collectionSchema.Properties.ref, check.Equals, "https://my-beat.com/api/item-schemas/backstage-users") 77 | 78 | lenDefaultLinks := len(DefaultCollectionSchemaLinks) 79 | 80 | link := *(*s.collectionSchema.Links)[lenDefaultLinks] 81 | c.Assert(link, check.DeepEquals, Link{Rel: "top10", Href: "http://github.com/jedi"}) 82 | 83 | link = *(*s.collectionSchema.Links)[lenDefaultLinks+1] 84 | c.Assert(link, check.DeepEquals, Link{Rel: "history", Href: "https://my-beat.com/api/juniors"}) 85 | } 86 | -------------------------------------------------------------------------------- /schemas/item_schema.go: -------------------------------------------------------------------------------- 1 | package schemas 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "regexp" 9 | 10 | "github.com/backstage/beat/errors" 11 | ) 12 | 13 | const ItemSchemaPrimaryKey = "collectionName" 14 | const ItemSchemaCollectionName = "item-schemas" 15 | const draft3Schema = "http://json-schema.org/draft-03/hyper-schema#" 16 | const draft4Schema = "http://json-schema.org/draft-04/hyper-schema#" 17 | const defaultSchema = draft4Schema 18 | 19 | var CollectionNameRegex = regexp.MustCompile(`^[a-z0-9-]+$`) 20 | var CollectionNameSpaceRegex = regexp.MustCompile(`^(\w+)-(.*)$`) 21 | 22 | type Properties map[string]map[string]interface{} 23 | 24 | // ItemSchema is the main struct for each collection, that describe 25 | // the data contract and data services. 26 | // This struct is based on json-schema specification, 27 | // see more in: http://json-schema.org 28 | type ItemSchema struct { 29 | Schema string `json:"$schema" bson:"%20schema"` 30 | CollectionName string `json:"collectionName" bson:"_id"` 31 | Title string `json:"title,omitempty"` 32 | CollectionTitle string `json:"collectionTitle,omitempty"` 33 | GlobalCollectionName bool `json:"globalCollectionName,omitempty"" bson:"globalCollectionName"` 34 | AditionalProperties *bool `json:"aditionalProperties,omitempty" bson:"aditionalProperties"` 35 | Type string `json:"type"` 36 | Properties Properties `json:"properties,omitempty"` 37 | Required []string `json:"required,omitempty"` // used only in draft4 38 | Links *Links `json:"links,omitempty"` 39 | CollectionLinks *Links `json:"collectionLinks,omitempty"` 40 | } 41 | 42 | // NewItemSchemaFromReader return a new ItemSchema by an io.Reader. 43 | // return a error if the buffer not is valid. 44 | func NewItemSchemaFromReader(r io.Reader) (*ItemSchema, errors.Error) { 45 | itemSchema := &ItemSchema{} 46 | err := json.NewDecoder(r).Decode(itemSchema) 47 | if err != nil { 48 | return nil, errors.Wraps(err, http.StatusBadRequest) 49 | } 50 | itemSchema.fillDefaultValues() 51 | return itemSchema, itemSchema.validate() 52 | } 53 | 54 | func (schema *ItemSchema) UpdateFromReader(r io.Reader) errors.Error { 55 | err := json.NewDecoder(r).Decode(schema) 56 | if err != nil { 57 | return errors.Wraps(err, http.StatusBadRequest) 58 | } 59 | 60 | return schema.validate() 61 | } 62 | 63 | func (schema *ItemSchema) fillDefaultValues() { 64 | if schema.Schema == "" { 65 | schema.Schema = defaultSchema 66 | } 67 | 68 | if schema.Type == "" { 69 | schema.Type = "object" 70 | } 71 | } 72 | 73 | func (schema *ItemSchema) String() string { 74 | return fmt.Sprintf(``, schema.CollectionName) 75 | } 76 | 77 | func (schema *ItemSchema) AttachDefaultLinks(baseURL string) { 78 | customLinks := schema.Links 79 | schema.Links = schema.defaultLinks() 80 | 81 | if customLinks != nil { 82 | schema.Links = schema.Links.ConcatenateLinks(customLinks) 83 | } 84 | schema.Links.ApplyBaseURL(baseURL) 85 | } 86 | 87 | func (schema *ItemSchema) DiscardDefaultLinks() { 88 | if schema.Links != nil { 89 | schema.Links = schema.Links.DiscardDefaultLinks() 90 | } 91 | } 92 | 93 | func (schema *ItemSchema) validate() errors.Error { 94 | validation := &errors.ValidationError{} 95 | 96 | if schema.Schema != draft3Schema && schema.Schema != draft4Schema { 97 | validation.Put("$schema", fmt.Sprintf(`must be "%s" or "%s"`, draft3Schema, draft4Schema)) 98 | } 99 | 100 | if schema.Type != "object" { 101 | validation.Put("type", "Root type must be an object.") 102 | } 103 | 104 | isInvalidGlobalCollectionName := (!schema.GlobalCollectionName && !CollectionNameSpaceRegex.MatchString(schema.CollectionName)) 105 | 106 | if schema.CollectionName == "" { 107 | validation.Put("collectionName", "must not be blank.") 108 | } else if isInvalidGlobalCollectionName || !CollectionNameRegex.MatchString(schema.CollectionName) { 109 | validation.Put("collectionName", "invalid format, use {namespace}-{name}, with characters a-z and 0-9, ex: backstage-users") 110 | } 111 | 112 | if validation.Length() > 0 { 113 | return validation 114 | } 115 | 116 | return nil 117 | } 118 | 119 | func (schema *ItemSchema) collectionURL() string { 120 | return fmt.Sprintf("/%s", schema.CollectionName) 121 | } 122 | 123 | func (schema *ItemSchema) url() string { 124 | return fmt.Sprintf("/%s/%s", ItemSchemaCollectionName, schema.CollectionName) 125 | } 126 | 127 | func (schema *ItemSchema) defaultLinks() *Links { 128 | collectionURL := schema.collectionURL() 129 | schemaURL := schema.url() 130 | itemURL := fmt.Sprintf("/%s/{id}", schema.CollectionName) 131 | 132 | return &Links{ 133 | &Link{Rel: "self", Href: itemURL}, 134 | &Link{Rel: "item", Href: itemURL}, 135 | &Link{Rel: "create", Method: "POST", Href: collectionURL, 136 | Schema: map[string]interface{}{ 137 | "$ref": schemaURL, 138 | }, 139 | }, 140 | &Link{Rel: "update", Method: "PUT", Href: itemURL}, 141 | &Link{Rel: "delete", Method: "DELETE", Href: itemURL}, 142 | &Link{Rel: "parent", Href: collectionURL}, 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /schemas/item_schema_test.go: -------------------------------------------------------------------------------- 1 | package schemas 2 | 3 | import ( 4 | "gopkg.in/check.v1" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | var _ = check.Suite(&S{}) 10 | 11 | type S struct{} 12 | 13 | func Test(t *testing.T) { 14 | check.TestingT(t) 15 | } 16 | 17 | func (s *S) TestNewItemSchemaFromReader(c *check.C) { 18 | schema := `{ 19 | "collectionName": "example-my-schema", 20 | "$schema": "http://json-schema.org/draft-03/hyper-schema#", 21 | "globalCollectionName": true, 22 | "aditionalProperties": true, 23 | "type": "object", 24 | "properties": { 25 | "name": { 26 | "type": "string" 27 | } 28 | } 29 | }` 30 | reader := strings.NewReader(schema) 31 | itemSchema, err := NewItemSchemaFromReader(reader) 32 | 33 | c.Assert(err, check.IsNil) 34 | c.Assert(itemSchema.CollectionName, check.Equals, "example-my-schema") 35 | c.Assert(itemSchema.Schema, check.Equals, "http://json-schema.org/draft-03/hyper-schema#") 36 | c.Assert(*itemSchema.AditionalProperties, check.Equals, true) 37 | c.Assert(itemSchema.Type, check.Equals, "object") 38 | c.Assert(itemSchema.Properties["name"]["type"], check.Equals, "string") 39 | } 40 | 41 | func (s *S) TestNewItemSchemaWhenOmmitAditionalProperties(c *check.C) { 42 | schema := `{ 43 | "collectionName": "example-my-schema", 44 | "$schema": "http://json-schema.org/draft-03/hyper-schema#" 45 | }` 46 | reader := strings.NewReader(schema) 47 | itemSchema, err := NewItemSchemaFromReader(reader) 48 | 49 | c.Assert(err, check.IsNil) 50 | c.Assert(itemSchema.AditionalProperties, check.IsNil) 51 | } 52 | 53 | func (s *S) TestNewItemSchemaWithDefaultValues(c *check.C) { 54 | schema := `{ 55 | "collectionName": "example-my-schema" 56 | }` 57 | reader := strings.NewReader(schema) 58 | itemSchema, err := NewItemSchemaFromReader(reader) 59 | 60 | c.Assert(err, check.IsNil) 61 | c.Assert(itemSchema.Schema, check.Equals, "http://json-schema.org/draft-04/hyper-schema#") 62 | } 63 | 64 | func (s *S) TestNewItemSchemaWithInvalidSchema(c *check.C) { 65 | schema := `{ 66 | "$schema": "http://globo.com/invalid-schema", 67 | "collectionName" : "backstage-valid" 68 | }` 69 | 70 | _, err := NewItemSchemaFromReader(strings.NewReader(schema)) 71 | 72 | c.Assert(err, check.Not(check.IsNil)) 73 | c.Assert(err.Error(), check.Equals, `$schema: must be "http://json-schema.org/draft-03/hyper-schema#" or "http://json-schema.org/draft-04/hyper-schema#"`) 74 | 75 | schema = `{ 76 | "collectionName": "backstage-users", 77 | "type": "array" 78 | }` 79 | _, err = NewItemSchemaFromReader(strings.NewReader(schema)) 80 | 81 | c.Assert(err, check.Not(check.IsNil)) 82 | c.Assert(err.Error(), check.Equals, "type: Root type must be an object.") 83 | c.Assert(err.StatusCode(), check.Equals, 422) 84 | 85 | schema = `{}` 86 | _, err = NewItemSchemaFromReader(strings.NewReader(schema)) 87 | 88 | c.Assert(err, check.Not(check.IsNil)) 89 | c.Assert(err.Error(), check.Equals, "collectionName: must not be blank.") 90 | c.Assert(err.StatusCode(), check.Equals, 422) 91 | 92 | schema = `{ 93 | "collectionName": "123$!" 94 | }` 95 | _, err = NewItemSchemaFromReader(strings.NewReader(schema)) 96 | 97 | c.Assert(err, check.Not(check.IsNil)) 98 | c.Assert(err.Error(), check.Equals, "collectionName: invalid format, use {namespace}-{name}, with characters a-z and 0-9, ex: backstage-users") 99 | c.Assert(err.StatusCode(), check.Equals, 422) 100 | } 101 | 102 | func (s *S) TestNewItemSchemaWithoutNameSpace(c *check.C) { 103 | schema := `{ 104 | "collectionName": "users" 105 | }` 106 | _, err := NewItemSchemaFromReader(strings.NewReader(schema)) 107 | 108 | c.Assert(err, check.Not(check.IsNil)) 109 | c.Assert(err.Error(), check.Equals, "collectionName: invalid format, use {namespace}-{name}, with characters a-z and 0-9, ex: backstage-users") 110 | c.Assert(err.StatusCode(), check.Equals, 422) 111 | } 112 | 113 | func (s *S) TestNewItemSchemaWithGlobalCollectionName(c *check.C) { 114 | schema := `{ 115 | "collectionName": "users", 116 | "globalCollectionName": true 117 | }` 118 | itemSchema, err := NewItemSchemaFromReader(strings.NewReader(schema)) 119 | 120 | c.Assert(err, check.IsNil) 121 | c.Assert(itemSchema.GlobalCollectionName, check.Equals, true) 122 | } 123 | 124 | var ( 125 | DefaultItemSchemaLinks = Links{ 126 | &Link{Rel: "self", Href: "http://api.mysite.com/backstage-users/{id}"}, 127 | &Link{Rel: "item", Href: "http://api.mysite.com/backstage-users/{id}"}, 128 | &Link{Rel: "create", Href: "http://api.mysite.com/backstage-users", Method: "POST", 129 | Schema: map[string]interface{}{ 130 | "$ref": "http://api.mysite.com/item-schemas/backstage-users", 131 | }, 132 | }, 133 | &Link{Rel: "update", Href: "http://api.mysite.com/backstage-users/{id}", Method: "PUT"}, 134 | &Link{Rel: "delete", Href: "http://api.mysite.com/backstage-users/{id}", Method: "DELETE"}, 135 | &Link{Rel: "parent", Href: "http://api.mysite.com/backstage-users"}, 136 | } 137 | ) 138 | 139 | func (s *S) TestAttachDefaultLinks(c *check.C) { 140 | schema := `{ 141 | "collectionName": "backstage-users" 142 | }` 143 | itemSchema, err := NewItemSchemaFromReader(strings.NewReader(schema)) 144 | c.Assert(err, check.IsNil) 145 | itemSchema.AttachDefaultLinks("http://api.mysite.com") 146 | 147 | for i, expectedLink := range DefaultItemSchemaLinks { 148 | link := *(*itemSchema.Links)[i] 149 | c.Assert(link, check.DeepEquals, *expectedLink) 150 | } 151 | } 152 | 153 | func (s *S) TestAttachDefaultLinksWithCustomLinks(c *check.C) { 154 | schema := `{ 155 | "collectionName": "backstage-users", 156 | "links": [ 157 | {"rel": "permissions", "href": "/backstage-permissions/{id}"} 158 | ] 159 | }` 160 | itemSchema, err := NewItemSchemaFromReader(strings.NewReader(schema)) 161 | c.Assert(err, check.IsNil) 162 | itemSchema.AttachDefaultLinks("http://api.mysite.com") 163 | 164 | lenDefaultLinks := len(DefaultItemSchemaLinks) 165 | link := *(*itemSchema.Links)[lenDefaultLinks] 166 | c.Assert(link, check.DeepEquals, Link{Rel: "permissions", Href: "http://api.mysite.com/backstage-permissions/{id}"}) 167 | } 168 | 169 | func (s *S) TestAttachDefaultLinksWithCustomLinksWithAbsoluteLink(c *check.C) { 170 | schema := `{ 171 | "collectionName": "backstage-users", 172 | "links": [ 173 | {"rel": "logs", "href": "http://mylog-service/by-user/{id}"} 174 | ] 175 | }` 176 | itemSchema, err := NewItemSchemaFromReader(strings.NewReader(schema)) 177 | c.Assert(err, check.IsNil) 178 | itemSchema.AttachDefaultLinks("http://api.mysite.com") 179 | 180 | lenDefaultLinks := len(DefaultItemSchemaLinks) 181 | link := *(*itemSchema.Links)[lenDefaultLinks] 182 | 183 | c.Assert(link, check.DeepEquals, Link{Rel: "logs", Href: "http://mylog-service/by-user/{id}"}) 184 | } 185 | 186 | func (s *S) TestAttachDefaultLinksWithCustomLinksWithTemplateLink(c *check.C) { 187 | schema := `{ 188 | "collectionName": "backstage-users", 189 | "links": [ 190 | {"rel": "view", "href": "{+url}"} 191 | ] 192 | }` 193 | itemSchema, err := NewItemSchemaFromReader(strings.NewReader(schema)) 194 | c.Assert(err, check.IsNil) 195 | itemSchema.AttachDefaultLinks("http://api.mysite.com") 196 | 197 | lenDefaultLinks := len(DefaultItemSchemaLinks) 198 | link := *(*itemSchema.Links)[lenDefaultLinks] 199 | 200 | c.Assert(link, check.DeepEquals, Link{Rel: "view", Href: "{+url}"}) 201 | } 202 | 203 | func (s *S) TestAttachDefaultLinksWithCustomLinksWithRefSchema(c *check.C) { 204 | schema := `{ 205 | "collectionName": "backstage-users", 206 | "links": [ 207 | { 208 | "rel": "view", "href": "/blah", 209 | "schema": {"$ref": "/api/kaka1"}, 210 | "targetSchema": {"$ref": "/api/kaka2"} 211 | } 212 | ] 213 | }` 214 | itemSchema, err := NewItemSchemaFromReader(strings.NewReader(schema)) 215 | c.Assert(err, check.IsNil) 216 | itemSchema.AttachDefaultLinks("http://api.mysite.com") 217 | 218 | lenDefaultLinks := len(DefaultItemSchemaLinks) 219 | link := *(*itemSchema.Links)[lenDefaultLinks] 220 | 221 | c.Assert(link, check.DeepEquals, Link{ 222 | Rel: "view", Href: "http://api.mysite.com/blah", 223 | Schema: map[string]interface{}{ 224 | "$ref": "http://api.mysite.com/api/kaka1", 225 | }, 226 | TargetSchema: map[string]interface{}{ 227 | "$ref": "http://api.mysite.com/api/kaka2", 228 | }, 229 | }) 230 | } 231 | 232 | func (s *S) TestDiscardDefaultLinks(c *check.C) { 233 | schema := `{ 234 | "collectionName": "backstage-users", 235 | "links": [ 236 | { 237 | "rel": "self", 238 | "href": "/hacked-url" 239 | } 240 | ] 241 | }` 242 | itemSchema, err := NewItemSchemaFromReader(strings.NewReader(schema)) 243 | c.Assert(err, check.IsNil) 244 | itemSchema.DiscardDefaultLinks() 245 | c.Assert(*itemSchema.Links, check.HasLen, 0) 246 | } 247 | 248 | func (s *S) TestDiscardDefaultLinksWithCustomLinks(c *check.C) { 249 | schema := `{ 250 | "collectionName": "backstage-users", 251 | "links": [ 252 | { 253 | "rel": "customLink1", 254 | "href": "/hacked-url1" 255 | }, 256 | { 257 | "rel": "create", 258 | "href": "/api/user" 259 | }, 260 | { 261 | "rel": "customLink2", 262 | "href": "/hacked-url2" 263 | } 264 | ] 265 | }` 266 | itemSchema, err := NewItemSchemaFromReader(strings.NewReader(schema)) 267 | c.Assert(err, check.IsNil) 268 | itemSchema.DiscardDefaultLinks() 269 | 270 | c.Assert(*itemSchema.Links, check.HasLen, 2) 271 | 272 | link := *(*itemSchema.Links)[0] 273 | c.Assert(link, check.DeepEquals, Link{Rel: "customLink1", Href: "/hacked-url1"}) 274 | 275 | link = *(*itemSchema.Links)[1] 276 | c.Assert(link, check.DeepEquals, Link{Rel: "customLink2", Href: "/hacked-url2"}) 277 | } 278 | -------------------------------------------------------------------------------- /schemas/link.go: -------------------------------------------------------------------------------- 1 | package schemas 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | ) 7 | 8 | type Link struct { 9 | Rel string `json:"rel" bson:"rel"` 10 | Href string `json:"href" bson:"href"` 11 | Title string `json:"title,omitempty" bson:"title,omitempty"` 12 | TargetSchema map[string]interface{} `json:"targetSchema,omitempty" bson:"targetSchema,omitempty"` 13 | MediaType string `json:"mediaType,omitempty" bson:"mediaType,omitempty"` 14 | Method string `json:"method,omitempty" bson:"method,omitempty"` 15 | EncType string `json:"encType,omitempty" bson:"encType,omitempty"` 16 | Schema map[string]interface{} `json:"schema,omitempty" bson:"schema,omitempty"` 17 | } 18 | 19 | var ( 20 | DefaultLinkRels = []string{"self", "item", "create", "update", "delete", "parent"} 21 | ) 22 | 23 | type Links []*Link 24 | 25 | func (l Links) ApplyBaseURL(baseURL string) { 26 | for _, link := range l { 27 | if isRelativeLink(link.Href) { 28 | link.Href = fmt.Sprintf("%s%s", baseURL, link.Href) 29 | } 30 | 31 | if ref, ok := link.Schema["$ref"].(string); ok && isRelativeLink(ref) { 32 | link.Schema["$ref"] = fmt.Sprintf("%s%s", baseURL, ref) 33 | } 34 | 35 | if ref, ok := link.TargetSchema["$ref"].(string); ok && isRelativeLink(ref) { 36 | link.TargetSchema["$ref"] = fmt.Sprintf("%s%s", baseURL, ref) 37 | } 38 | } 39 | } 40 | 41 | // ConcatenateLinks generate new links with merge with tailLinks 42 | func (l Links) ConcatenateLinks(tailLinks *Links) *Links { 43 | currentSize := len(l) 44 | expandSize := len(*tailLinks) 45 | 46 | newLinks := make(Links, currentSize+expandSize) 47 | copy(newLinks, l) 48 | 49 | for i, link := range *tailLinks { 50 | newLinks[currentSize+i] = link 51 | } 52 | 53 | return &newLinks 54 | } 55 | 56 | // DiscardDefaultLinks remove all default links to store only custom links 57 | func (l Links) DiscardDefaultLinks() *Links { 58 | newLinks := make(Links, 0, len(l)) 59 | for _, link := range l { 60 | if !isDefaultRel(link.Rel) { 61 | newLinks = append(newLinks, link) 62 | } 63 | } 64 | return &newLinks 65 | } 66 | 67 | func isRelativeLink(link string) bool { 68 | url, err := url.Parse(link) 69 | 70 | if err != nil { 71 | return false 72 | } 73 | 74 | return url.Host == "" && url.Scheme == "" && !isURITemplate(link) 75 | } 76 | 77 | func isURITemplate(link string) bool { 78 | return len(link) > 0 && link[0] == '{' 79 | } 80 | 81 | func isDefaultRel(linkRel string) bool { 82 | for _, defaultLinkRel := range DefaultLinkRels { 83 | if defaultLinkRel == linkRel { 84 | return true 85 | } 86 | } 87 | return false 88 | } 89 | -------------------------------------------------------------------------------- /server/resource_routes.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | 7 | "github.com/backstage/beat/errors" 8 | "github.com/backstage/beat/transaction" 9 | simplejson "github.com/bitly/go-simplejson" 10 | ) 11 | 12 | var ( 13 | ErrResourceNotAnObject = errors.New("Json root not is an object", http.StatusBadRequest) 14 | ErrEmptyResource = errors.New("Empty resource", http.StatusBadRequest) 15 | ) 16 | 17 | func (s *Server) createResource(t *transaction.Transaction) { 18 | resource, err := simplejson.NewFromReader(t.Req.Body) 19 | 20 | if err == io.EOF { 21 | t.WriteError(ErrEmptyResource) 22 | return 23 | } else if err != nil { 24 | t.WriteError(errors.Newf(http.StatusBadRequest, "Invalid json: %s", err.Error())) 25 | return 26 | } 27 | _, err = resource.Map() 28 | 29 | if err != nil { 30 | t.WriteError(ErrResourceNotAnObject) 31 | return 32 | } 33 | 34 | t.WriteResultWithStatusCode(http.StatusCreated, resource) 35 | } 36 | 37 | func (s *Server) findResource(t *transaction.Transaction) { 38 | t.WriteError(errors.New("TODO: Find resource", http.StatusNotImplemented)) 39 | } 40 | 41 | func (s *Server) findOneResource(t *transaction.Transaction) { 42 | t.WriteError(errors.New("TODO: findOne resource", http.StatusNotImplemented)) 43 | } 44 | 45 | func (s *Server) findResourceByID(t *transaction.Transaction) { 46 | t.WriteError(errors.New("TODO: find resource by id", http.StatusNotImplemented)) 47 | } 48 | 49 | func (s *Server) deleteResourceByID(t *transaction.Transaction) { 50 | t.WriteError(errors.New("TODO: delete resource by id", http.StatusNotImplemented)) 51 | } 52 | -------------------------------------------------------------------------------- /server/resource_routes_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "bytes" 5 | "net/http" 6 | 7 | "github.com/backstage/beat/db" 8 | "github.com/backstage/beat/schemas" 9 | simplejson "github.com/bitly/go-simplejson" 10 | "gopkg.in/check.v1" 11 | ) 12 | 13 | func (s *S) TestCreateResourceWithNotSupportedRoot(c *check.C) { 14 | mockCtrl := s.mockDatabase(c) 15 | defer mockCtrl.Finish() 16 | 17 | itemSchema := &schemas.ItemSchema{CollectionName: "photos"} 18 | bufs := []string{ 19 | `[{"name": "fail"}]`, 20 | `"not-valid"`, 21 | `10`, 22 | } 23 | for _, buf := range bufs { 24 | s.db.EXPECT().FindItemSchemaByCollectionName("photos").Return(itemSchema, nil) 25 | 26 | r, _ := http.NewRequest("POST", "/api/photos", bytes.NewBufferString(buf)) 27 | response := s.Request(r) 28 | c.Assert(response.Code, check.Equals, http.StatusBadRequest) 29 | 30 | jsonErr, err := simplejson.NewFromReader(response.Body) 31 | c.Assert(err, check.IsNil) 32 | 33 | msg := jsonErr.Get("errors").GetIndex(0).Get("_all").GetIndex(0).MustString() 34 | c.Assert(msg, check.Equals, "Json root not is an object") 35 | } 36 | } 37 | 38 | func (s *S) TestCreateResourceWithInvalidJson(c *check.C) { 39 | mockCtrl := s.mockDatabase(c) 40 | defer mockCtrl.Finish() 41 | 42 | itemSchema := &schemas.ItemSchema{CollectionName: "photos"} 43 | bufs := []string{ 44 | `["name"}`, 45 | `{1"adf"`, 46 | } 47 | for _, buf := range bufs { 48 | s.db.EXPECT().FindItemSchemaByCollectionName("photos").Return(itemSchema, nil) 49 | 50 | r, _ := http.NewRequest("POST", "/api/photos", bytes.NewBufferString(buf)) 51 | response := s.Request(r) 52 | c.Assert(response.Code, check.Equals, http.StatusBadRequest) 53 | 54 | jsonErr, err := simplejson.NewFromReader(response.Body) 55 | c.Assert(err, check.IsNil) 56 | 57 | msg := jsonErr.Get("errors").GetIndex(0).Get("_all").GetIndex(0).MustString() 58 | c.Assert(msg, check.Matches, "Invalid json: .*") 59 | } 60 | } 61 | 62 | func (s *S) TestCreateResourceWithoutBody(c *check.C) { 63 | mockCtrl := s.mockDatabase(c) 64 | defer mockCtrl.Finish() 65 | 66 | itemSchema := &schemas.ItemSchema{CollectionName: "photos"} 67 | s.db.EXPECT().FindItemSchemaByCollectionName("photos").Return(itemSchema, nil) 68 | 69 | r, _ := http.NewRequest("POST", "/api/photos", bytes.NewBufferString("")) 70 | response := s.Request(r) 71 | c.Assert(response.Code, check.Equals, http.StatusBadRequest) 72 | 73 | jsonErr, err := simplejson.NewFromReader(response.Body) 74 | c.Assert(err, check.IsNil) 75 | 76 | msg := jsonErr.Get("errors").GetIndex(0).Get("_all").GetIndex(0).MustString() 77 | c.Assert(msg, check.Equals, "Empty resource") 78 | } 79 | 80 | func (s *S) TestCreateResource(c *check.C) { 81 | mockCtrl := s.mockDatabase(c) 82 | defer mockCtrl.Finish() 83 | 84 | itemSchema := &schemas.ItemSchema{CollectionName: "photos"} 85 | s.db.EXPECT().FindItemSchemaByCollectionName("photos").Return(itemSchema, nil) 86 | 87 | buf := bytes.NewBufferString(`{"name": "ok"}`) 88 | r, _ := http.NewRequest("POST", "/api/photos", buf) 89 | response := s.Request(r) 90 | c.Assert(response.Code, check.Equals, http.StatusCreated) 91 | 92 | json, err := simplejson.NewFromReader(response.Body) 93 | c.Assert(err, check.IsNil) 94 | 95 | c.Assert(json.Get("name").MustString(), check.Equals, "ok") 96 | } 97 | 98 | func (s *S) TestCreateResourceWhenItemSchemaNotIsFound(c *check.C) { 99 | mockCtrl := s.mockDatabase(c) 100 | defer mockCtrl.Finish() 101 | 102 | s.db.EXPECT().FindItemSchemaByCollectionName("photos").Return(nil, db.ErrItemSchemaNotFound) 103 | 104 | buf := bytes.NewBufferString(`{"name": "ok"}`) 105 | r, _ := http.NewRequest("POST", "/api/photos", buf) 106 | response := s.Request(r) 107 | c.Assert(response.Code, check.Equals, http.StatusNotFound) 108 | 109 | json, err := simplejson.NewFromReader(response.Body) 110 | c.Assert(err, check.IsNil) 111 | 112 | msg := json.Get("errors").GetIndex(0).Get("_all").GetIndex(0).MustString() 113 | c.Assert(msg, check.Equals, "item-schema not found") 114 | } 115 | -------------------------------------------------------------------------------- /server/schema_routes.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/backstage/beat/db" 7 | "github.com/backstage/beat/errors" 8 | "github.com/backstage/beat/schemas" 9 | "github.com/backstage/beat/transaction" 10 | ) 11 | 12 | func (s *Server) createItemSchema(t *transaction.Transaction) { 13 | itemSchema, err := schemas.NewItemSchemaFromReader(t.Req.Body) 14 | 15 | if err != nil { 16 | t.WriteError(err) 17 | return 18 | } 19 | itemSchema.DiscardDefaultLinks() 20 | err = s.DB.CreateItemSchema(itemSchema) 21 | 22 | if err != nil { 23 | t.WriteError(err) 24 | return 25 | } 26 | 27 | itemSchema.AttachDefaultLinks(t.BaseURL()) 28 | t.WriteResultWithStatusCode(http.StatusCreated, itemSchema) 29 | } 30 | 31 | func (s *Server) listItemSchemas(t *transaction.Transaction) { 32 | filter, err := db.NewFilterFromQueryString(t.Req.URL.RawQuery) 33 | 34 | if err != nil { 35 | t.WriteError(errors.Wraps(err, http.StatusBadRequest)) 36 | return 37 | } 38 | 39 | reply, findErr := s.DB.FindItemSchema(filter) 40 | if findErr != nil { 41 | t.WriteError(findErr) 42 | return 43 | } 44 | 45 | baseURL := t.BaseURL() 46 | for _, itemSchema := range reply.Items { 47 | itemSchema.AttachDefaultLinks(baseURL) 48 | } 49 | 50 | t.WriteResult(reply) 51 | } 52 | 53 | func (s *Server) findItemSchema(t *transaction.Transaction) { 54 | t.ItemSchema.AttachDefaultLinks(t.BaseURL()) 55 | t.WriteResult(t.ItemSchema) 56 | } 57 | 58 | func (s *Server) findOneItemSchema(t *transaction.Transaction) { 59 | filter, err := db.NewFilterFromQueryString(t.Req.URL.RawQuery) 60 | 61 | if err != nil { 62 | t.WriteError(errors.Wraps(err, http.StatusBadRequest)) 63 | return 64 | } 65 | 66 | itemSchema, findErr := s.DB.FindOneItemSchema(filter) 67 | if findErr != nil { 68 | t.WriteError(findErr) 69 | return 70 | } 71 | 72 | itemSchema.AttachDefaultLinks(t.BaseURL()) 73 | t.WriteResult(itemSchema) 74 | } 75 | 76 | func (s *Server) deleteItemSchema(t *transaction.Transaction) { 77 | err := s.DB.DeleteItemSchema(t.CollectionName) 78 | if err != nil { 79 | t.WriteError(err) 80 | return 81 | } 82 | 83 | t.NoResultWithStatusCode(http.StatusNoContent) 84 | } 85 | 86 | func (s *Server) updateItemSchema(t *transaction.Transaction) { 87 | err := t.ItemSchema.UpdateFromReader(t.Req.Body) 88 | 89 | if err != nil { 90 | t.WriteError(err) 91 | return 92 | } 93 | t.ItemSchema.DiscardDefaultLinks() 94 | err = s.DB.UpdateItemSchema(t.ItemSchema) 95 | 96 | if err != nil { 97 | t.WriteError(err) 98 | return 99 | } 100 | 101 | t.ItemSchema.AttachDefaultLinks(t.BaseURL()) 102 | t.WriteResult(t.ItemSchema) 103 | } 104 | 105 | func (s *Server) findCollectionSchema(t *transaction.Transaction) { 106 | collectionSchema := schemas.NewCollectionSchema(t.ItemSchema) 107 | collectionSchema.ApplyBaseURL(t.BaseURL()) 108 | 109 | t.WriteResult(collectionSchema) 110 | } 111 | -------------------------------------------------------------------------------- /server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | log "github.com/Sirupsen/logrus" 8 | "github.com/backstage/beat/auth" 9 | "github.com/backstage/beat/db" 10 | "github.com/backstage/beat/transaction" 11 | "github.com/dimfeld/httptreemux" 12 | "github.com/spf13/viper" 13 | ) 14 | 15 | type Server struct { 16 | *httptreemux.TreeMux 17 | Authentication auth.Authable 18 | DB db.Database 19 | } 20 | 21 | type ServerOpts struct { 22 | Authentication auth.Authable 23 | DB db.Database 24 | } 25 | 26 | func init() { 27 | viper.SetDefault("host", "0.0.0.0") 28 | viper.SetDefault("port", 3000) 29 | viper.SetDefault("database", "mongo") 30 | viper.SetDefault("authentication", "static") 31 | } 32 | 33 | func New() (*Server, error) { 34 | db, err := db.New(viper.GetString("database")) 35 | 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | auth, err := auth.New(viper.GetString("authentication")) 41 | 42 | if err != nil { 43 | return nil, err 44 | } 45 | return NewWithOpts(&ServerOpts{ 46 | Authentication: auth, 47 | DB: db, 48 | }), nil 49 | 50 | } 51 | 52 | func NewWithOpts(opts *ServerOpts) *Server { 53 | router := httptreemux.New() 54 | server := &Server{ 55 | TreeMux: router, 56 | Authentication: opts.Authentication, 57 | DB: opts.DB, 58 | } 59 | server.initRoutes() 60 | return server 61 | } 62 | 63 | func (s *Server) Run() { 64 | bind := fmt.Sprintf("%s:%d", viper.GetString("host"), viper.GetInt("port")) 65 | log.Infof("Backstage Beat is running on http://%s/", bind) 66 | log.Fatal(http.ListenAndServe(bind, s)) 67 | } 68 | 69 | func (s *Server) initRoutes() { 70 | s.GET("/", s.healthCheck) 71 | s.GET("/healthcheck", s.healthCheck) 72 | 73 | s.POST("/api/item-schemas", transaction.Handle(s.createItemSchema)) 74 | s.GET("/api/item-schemas", transaction.Handle(s.listItemSchemas)) 75 | s.GET("/api/item-schemas/findOne", transaction.Handle(s.findOneItemSchema)) 76 | s.GET("/api/item-schemas/:collectionName", s.collectionHandle(s.findItemSchema)) 77 | s.PUT("/api/item-schemas/:collectionName", s.collectionHandle(s.updateItemSchema)) 78 | s.DELETE("/api/item-schemas/:collectionName", s.collectionHandle(s.deleteItemSchema)) 79 | 80 | s.GET("/api/collection-schemas/:collectionName", s.collectionHandle(s.findCollectionSchema)) 81 | 82 | s.POST("/api/:collectionName", s.collectionHandle(s.createResource)) 83 | s.GET("/api/:collectionName", s.collectionHandle(s.findResource)) 84 | s.GET("/api/:collectionName/findOne", s.collectionHandle(s.findOneResource)) 85 | s.GET("/api/:collectionName/:resourceId", s.collectionHandle(s.findResourceByID)) 86 | s.DELETE("/api/:collectionName/:resourceId", s.collectionHandle(s.deleteResourceByID)) 87 | } 88 | 89 | func (s *Server) healthCheck(w http.ResponseWriter, r *http.Request, _ map[string]string) { 90 | fmt.Fprintf(w, "WORKING") 91 | } 92 | 93 | func (s *Server) collectionHandle(handler transaction.TransactionHandler) httptreemux.HandlerFunc { 94 | return transaction.CollectionHandle(func(t *transaction.Transaction) { 95 | itemSchema, err := s.DB.FindItemSchemaByCollectionName(t.CollectionName) 96 | 97 | if err != nil { 98 | t.WriteError(err) 99 | return 100 | } 101 | 102 | t.ItemSchema = itemSchema 103 | handler(t) 104 | }) 105 | } 106 | -------------------------------------------------------------------------------- /server/server_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "log" 7 | "net/http" 8 | "net/http/httptest" 9 | "os" 10 | "testing" 11 | 12 | "github.com/Sirupsen/logrus" 13 | "github.com/backstage/beat/auth" 14 | "github.com/backstage/beat/db" 15 | "github.com/backstage/beat/mocks/mock_db" 16 | "github.com/golang/mock/gomock" 17 | "gopkg.in/check.v1" 18 | 19 | _ "github.com/backstage/beat/auth/static" 20 | _ "github.com/backstage/beat/db/mongo" 21 | ) 22 | 23 | var _ = check.Suite(&S{}) 24 | 25 | type S struct { 26 | server *Server 27 | db *mock_db.MockDatabase 28 | } 29 | 30 | func Test(t *testing.T) { 31 | check.TestingT(t) 32 | } 33 | 34 | func (s *S) SetUpSuite(c *check.C) { 35 | logrus.SetOutput(ioutil.Discard) 36 | } 37 | 38 | func (s *S) SetUpTest(c *check.C) { 39 | s.server = NewWithOpts(&ServerOpts{}) 40 | } 41 | 42 | func (s *S) TestNewWithInvalidDatabase(c *check.C) { 43 | os.Setenv("DATABASE", "not-found") 44 | os.Setenv("AUTHENTICATION", "static") 45 | 46 | server, err := New() 47 | c.Assert(server, check.IsNil) 48 | c.Assert(err, check.FitsTypeOf, db.ErrNotFound{}) 49 | } 50 | 51 | func (s *S) TestNewWithInvalidAuthentication(c *check.C) { 52 | os.Setenv("DATABASE", "mongo") 53 | os.Setenv("AUTHENTICATION", "not-found") 54 | 55 | server, err := New() 56 | c.Assert(server, check.IsNil) 57 | c.Assert(err, check.FitsTypeOf, auth.ErrNotFound{}) 58 | } 59 | 60 | func (s *S) TestHealthcheckInRootPath(c *check.C) { 61 | response := s.SimpleRequest("GET", "/") 62 | c.Assert(response.Code, check.Equals, http.StatusOK) 63 | c.Assert(response.Body.String(), check.Equals, "WORKING") 64 | } 65 | 66 | func (s *S) TestHealthcheck(c *check.C) { 67 | response := s.SimpleRequest("GET", "/healthcheck") 68 | c.Assert(response.Code, check.Equals, http.StatusOK) 69 | c.Assert(response.Body.String(), check.Equals, "WORKING") 70 | } 71 | 72 | func (s *S) SimpleRequest(method, path string) *httptest.ResponseRecorder { 73 | r, err := http.NewRequest(method, fmt.Sprintf("http://localhost%s", path), nil) 74 | if err != nil { 75 | log.Fatal(err) 76 | } 77 | return s.Request(r) 78 | } 79 | 80 | func (s *S) Request(r *http.Request) *httptest.ResponseRecorder { 81 | w := httptest.NewRecorder() 82 | s.server.ServeHTTP(w, r) 83 | return w 84 | } 85 | 86 | func (s *S) mockDatabase(c *check.C) *gomock.Controller { 87 | mockCtrl := gomock.NewController(c) 88 | s.db = mock_db.NewMockDatabase(mockCtrl) 89 | s.server = NewWithOpts(&ServerOpts{DB: s.db}) 90 | 91 | return mockCtrl 92 | } 93 | -------------------------------------------------------------------------------- /transaction/transaction.go: -------------------------------------------------------------------------------- 1 | package transaction 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | "time" 9 | 10 | log "github.com/Sirupsen/logrus" 11 | "github.com/backstage/beat/errors" 12 | "github.com/backstage/beat/schemas" 13 | "github.com/dimfeld/httptreemux" 14 | "github.com/satori/go.uuid" 15 | ) 16 | 17 | const ( 18 | TransactionHeader = "Backstage-Transaction" 19 | MaxTransactionHeader = 22 20 | SlowTransactionWarn = time.Millisecond * 100 21 | ) 22 | 23 | type TransactionHandler func(*Transaction) 24 | type Transaction struct { 25 | writer http.ResponseWriter 26 | statusCode int 27 | ID string 28 | CollectionName string 29 | ItemSchema *schemas.ItemSchema 30 | Params map[string]string 31 | Req *http.Request 32 | Log *log.Entry 33 | } 34 | 35 | func (t *Transaction) WriteError(err errors.Error) { 36 | t.statusCode = err.StatusCode() 37 | t.writer.WriteHeader(err.StatusCode()) 38 | json.NewEncoder(t.writer).Encode(err) 39 | } 40 | 41 | func (t *Transaction) WriteResultWithStatusCode(statusCode int, result interface{}) { 42 | t.statusCode = statusCode 43 | t.writer.WriteHeader(statusCode) 44 | json.NewEncoder(t.writer).Encode(result) 45 | } 46 | 47 | func (t *Transaction) WriteResult(result interface{}) { 48 | t.WriteResultWithStatusCode(http.StatusOK, result) 49 | } 50 | 51 | func (t *Transaction) NoResultWithStatusCode(statusCode int) { 52 | t.statusCode = statusCode 53 | t.writer.WriteHeader(statusCode) 54 | } 55 | 56 | func (t *Transaction) BaseURL() string { 57 | host := t.Req.URL.Host 58 | scheme := t.Req.URL.Scheme 59 | 60 | if host == "" { 61 | host = t.Req.Host 62 | } 63 | 64 | if scheme == "" { 65 | scheme = "http" 66 | } 67 | return fmt.Sprintf("%s://%s/api", scheme, host) 68 | } 69 | 70 | func Handle(handler TransactionHandler) httptreemux.HandlerFunc { 71 | return func(w http.ResponseWriter, r *http.Request, ps map[string]string) { 72 | start := time.Now() 73 | id := IDFromRequest(r) 74 | 75 | t := &Transaction{ 76 | ID: id, 77 | Req: r, 78 | writer: w, 79 | Params: ps, 80 | Log: log.WithFields(log.Fields{ 81 | "transaction": id, 82 | }), 83 | } 84 | 85 | handler(t) 86 | logTransaction(t, time.Since(start)) 87 | } 88 | } 89 | 90 | func CollectionHandle(handler TransactionHandler) httptreemux.HandlerFunc { 91 | return func(w http.ResponseWriter, r *http.Request, ps map[string]string) { 92 | start := time.Now() 93 | collectionName := ps["collectionName"] 94 | id := IDFromRequest(r) 95 | 96 | t := &Transaction{ 97 | ID: id, 98 | CollectionName: collectionName, 99 | Req: r, 100 | writer: w, 101 | Params: ps, 102 | Log: log.WithFields(log.Fields{ 103 | "transaction": id, 104 | "collectionName": collectionName, 105 | }), 106 | } 107 | 108 | handler(t) 109 | logTransaction(t, time.Since(start)) 110 | } 111 | } 112 | 113 | func IDFromRequest(r *http.Request) string { 114 | header := r.Header.Get(TransactionHeader) 115 | if header == "" || len(header) > MaxTransactionHeader { 116 | header = base64.RawStdEncoding.EncodeToString(uuid.NewV4().Bytes()) 117 | } 118 | return header 119 | } 120 | 121 | func logTransaction(t *Transaction, latency time.Duration) { 122 | msg := fmt.Sprintf( 123 | "%s %s %d %s", t.Req.Method, t.Req.URL.RequestURI(), t.statusCode, 124 | latency.String(), 125 | ) 126 | 127 | switch { 128 | case t.statusCode >= http.StatusInternalServerError: 129 | t.Log.Error(msg) 130 | case t.statusCode >= http.StatusBadRequest || latency > SlowTransactionWarn: 131 | t.Log.Warn(msg) 132 | default: 133 | t.Log.Info(msg) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /transaction/transaction_test.go: -------------------------------------------------------------------------------- 1 | package transaction 2 | 3 | import ( 4 | "io/ioutil" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | 9 | "github.com/Sirupsen/logrus" 10 | "github.com/backstage/beat/errors" 11 | simplejson "github.com/bitly/go-simplejson" 12 | "gopkg.in/check.v1" 13 | ) 14 | 15 | var _ = check.Suite(&S{}) 16 | 17 | type S struct { 18 | Writer *httptest.ResponseRecorder 19 | Req *http.Request 20 | T *Transaction 21 | } 22 | 23 | func Test(t *testing.T) { 24 | check.TestingT(t) 25 | } 26 | 27 | func (s *S) SetUpSuite(c *check.C) { 28 | logrus.SetOutput(ioutil.Discard) 29 | } 30 | 31 | func (s *S) SetUpTest(c *check.C) { 32 | var err error 33 | s.Writer = httptest.NewRecorder() 34 | s.Req, err = http.NewRequest("GET", "http://localhost/path", nil) 35 | c.Assert(err, check.IsNil) 36 | 37 | s.T = &Transaction{ 38 | Req: s.Req, 39 | writer: s.Writer, 40 | } 41 | } 42 | 43 | func (s *S) TestHandle(c *check.C) { 44 | var capturedTransaction *Transaction 45 | 46 | handler := Handle(func(t *Transaction) { 47 | capturedTransaction = t 48 | }) 49 | 50 | handler(s.Writer, s.Req, map[string]string{"collectionName": "users"}) 51 | 52 | c.Assert(capturedTransaction.ID, check.HasLen, 22) 53 | c.Assert(capturedTransaction.writer, check.Equals, s.Writer) 54 | c.Assert(capturedTransaction.Req, check.Equals, s.Req) 55 | c.Assert(capturedTransaction.Params, check.DeepEquals, map[string]string{"collectionName": "users"}) 56 | } 57 | 58 | func (s *S) TestWriteError(c *check.C) { 59 | s.T.WriteError(errors.New("my error", http.StatusInternalServerError)) 60 | c.Assert(s.T.statusCode, check.Equals, http.StatusInternalServerError) 61 | c.Assert(s.Writer.Code, check.Equals, http.StatusInternalServerError) 62 | 63 | json, err := simplejson.NewFromReader(s.Writer.Body) 64 | c.Assert(err, check.IsNil) 65 | 66 | msg := json.Get("errors").GetIndex(0).Get("_all").GetIndex(0).MustString() 67 | c.Assert(msg, check.Equals, "my error") 68 | } 69 | 70 | func (s *S) TestNoResultWithStatusCode(c *check.C) { 71 | s.T.NoResultWithStatusCode(http.StatusCreated) 72 | c.Assert(s.T.statusCode, check.Equals, http.StatusCreated) 73 | c.Assert(s.Writer.Code, check.Equals, http.StatusCreated) 74 | } 75 | 76 | func (s *S) TestWriteResult(c *check.C) { 77 | result := map[string]string{ 78 | "test": "ok", 79 | } 80 | s.T.WriteResult(&result) 81 | 82 | c.Assert(s.T.statusCode, check.Equals, http.StatusOK) 83 | c.Assert(s.Writer.Code, check.Equals, http.StatusOK) 84 | 85 | json, err := simplejson.NewFromReader(s.Writer.Body) 86 | c.Assert(err, check.IsNil) 87 | msg := json.Get("test").MustString() 88 | c.Assert(msg, check.Equals, "ok") 89 | } 90 | 91 | func (s *S) TestWriteResultWithStatusCode(c *check.C) { 92 | result := map[string]string{ 93 | "test": "with-status-code", 94 | } 95 | s.T.WriteResultWithStatusCode(http.StatusMethodNotAllowed, &result) 96 | 97 | c.Assert(s.T.statusCode, check.Equals, http.StatusMethodNotAllowed) 98 | c.Assert(s.Writer.Code, check.Equals, http.StatusMethodNotAllowed) 99 | 100 | json, err := simplejson.NewFromReader(s.Writer.Body) 101 | c.Assert(err, check.IsNil) 102 | 103 | msg := json.Get("test").MustString() 104 | c.Assert(msg, check.Equals, "with-status-code") 105 | } 106 | 107 | func (s *S) TestBaseUrl(c *check.C) { 108 | r, err := http.NewRequest("GET", "http://my-host.com/healtcheck", nil) 109 | c.Assert(err, check.IsNil) 110 | s.T.Req = r 111 | 112 | c.Assert(s.T.BaseURL(), check.Equals, "http://my-host.com/api") 113 | } 114 | 115 | func (s *S) TestIDFromRequestWithEmptyHeader(c *check.C) { 116 | r, err := http.NewRequest("GET", "http://localhost", nil) 117 | c.Assert(err, check.IsNil) 118 | 119 | id := IDFromRequest(r) 120 | c.Assert(id, check.HasLen, 22) 121 | } 122 | 123 | func (s *S) TestIDFromRequestWithFilledHeader(c *check.C) { 124 | r, err := http.NewRequest("GET", "http://localhost", nil) 125 | c.Assert(err, check.IsNil) 126 | 127 | r.Header.Set("Backstage-Transaction", "BBBBBBBBBBBBBBBBBBBBBZ") 128 | id := IDFromRequest(r) 129 | c.Assert(id, check.Equals, "BBBBBBBBBBBBBBBBBBBBBZ") 130 | } 131 | 132 | func (s *S) TestIDFromRequestWithBigHeader(c *check.C) { 133 | r, err := http.NewRequest("GET", "http://localhost", nil) 134 | c.Assert(err, check.IsNil) 135 | 136 | r.Header.Set("Backstage-Transaction", "ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ") 137 | id := IDFromRequest(r) 138 | c.Assert(id, check.HasLen, 22) 139 | } 140 | --------------------------------------------------------------------------------