goal

package module
v0.0.0-...-1be3e91 Latest Latest
Warning

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

Go to latest
Published: Jul 20, 2016 License: MIT Imports: 14 Imported by: 0

README

Build Status

I like Parse for its ease of use and I think it's really nice example about well-designed API. However it is probably best for mobile backend where you don't need too much control at server side. Its query can be slow since we don't have control over index, and many services are not available like cache or websocket.

My objective for Goal is to provide basic Parse features: solid implementation for CRUD, query, authentication and permission model, so it can be quickly setup and run as a backend for your mobile app. It is also flexible and easily extensible.

Goal relies on 3 popular Go libraries: Gorm, Gorilla Mux and Sessions. You can use familiar SQL database to store your data.

Setup Model and database connection

Since Goal relies on Gorm, you can just define your models as described in Gorm documentation. For example:

type testuser struct {
	ID       uint `gorm:"primary_key"`
	Username string
	Password string
	Name     string
	Age      int
}

type article struct {
	ID     uint `gorm:"primary_key"`
	Author *testuser
	Title  string
	Read   string
	Write  string
}

func setupDB()  {
  var err error
  db, err = gorm.Open("sqlite3", ":memory:")
  if err != nil {
    panic(err)
  }

  db.SingularTable(true)

  // Setup database
  goal.InitGormDb(&db)
}

Setup basic CRUD and Query

Goal simplifies many ideas from Sleepy Framework, where each model defines methods to handle basic CRUD and Query for its table. Goal provides ready-to-use implementation for each of these method. If you don't want to support any method in your API, just do not implement it, Goal will return 405 HTTP error code to the client.

// Define HTTP methods to support
func (user *testuser) Get(w http.ResponseWriter, request *http.Request) (int, interface{}, error) {
	return goal.Read(reflect.TypeOf(user), request)
}

func (user *testuser) Post(w http.ResponseWriter, request *http.Request) (int, interface{}, error) {
	return goal.Create(reflect.TypeOf(user), request)
}

func (user *testuser) Put(w http.ResponseWriter, request *http.Request) (int, interface{}, error) {
	return goal.Update(reflect.TypeOf(user), request)
}

func (user *testuser) Delete(w http.ResponseWriter, request *http.Request) (int, interface{}, error) {
	return goal.Delete(reflect.TypeOf(user), request)
}

Goal uses Gorilla Mux to route the request correctly to the handler. You can use all features of Gorilla Mux with api.Mux()

func main() {
  // Initialize API
  api := goal.NewAPI()

  // Add paths to correct model
  user := &testuser{}
  db.AutoMigrate(user)
  api.AddCrudResource(user, "/testuser", "/testuser/{id:[a-zA-Z0-9]+}")
  api.AddQueryResource(user, "/query/testuser/{query}")

  // Run the API
  http.ListenAndServe(":8080", api.Mux())
}

Goal predefines a set of default paths based on the model's table name (as defined by Gorm):

// Extract name of resource type
name := TableName(resource)

// Default path to interact with resource
createPath := fmt.Sprintf("/%s", name)
detailPath := fmt.Sprintf("/%s/{id:[a-zA-Z0-9]+}", name)

// Query path
queryPath := fmt.Sprintf("/query/%s/{query}", TableName(resource))

So if you want to quickly setup your API with default paths, use below methods:

models := []interface{}{&testuser{}, &article{}}

// Add default path
for _, model := range models {
  goal.RegisterModel(model)
}

Interact with Goal

Client sends JSON in the request body to interact with Goal. You need to make sure the JSON format can be parsed into your model:

// Create a test user
var json = []byte(`{"Name":"Thomas", "Age": 28}`)
req, _ := http.NewRequest("POST", "/testuser", bytes.NewBuffer(json))
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
res, err := client.Do(req)

// Get user with id 10
req, _ := http.NewRequest("GET", "/testuser/10", nil)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
res, err := client.Do(req)

For query, the payload data is a struct represents query filters you normally find with SQL:

// QueryItem defines most basic element of a query.
// For example: name = Thomas
type QueryItem struct {
	Key string       `json:"key"`
	Op  string       `json:"op"`
	Val interface{}  `json:"val"`
	Or  []*QueryItem `json:"or"`
}

type QueryParams struct {
	Where   []*QueryItem    `json:"where"`
	Limit   int64           `json:"limit"`
	Order   map[string]bool `json:"order"`
	Include []string        `json:"include"`
}

Goal validates all operators and column name to protect your database from SQL injection. To send a query request, client should construct the QueryParams, convert it to json, escape it to be URL safe and send that to Goal API server:

func sendQuery() {
  item := &goal.QueryItem{}
	item.Key = "name"
	item.Op = "="
	item.Val = "Thomas"

	params := &goal.QueryParams{}
	params.Where = []*goal.QueryItem{item}

	query, _ := json.Marshal(params)
  queryPath := fmt.Sprint("/query/testuser/", url.QueryEscape(string(query)))

	req, _ := http.NewRequest("GET", queryPath, nil)
	req.Header.Set("Content-Type", "application/json")

	// Get response
	client := &http.Client{}
	res, err := client.Do(req)
}

Caching

Goal supports caching to quickly retrieve data, and also includes basic implementation for Redis. If you have setup Redis in your server, use it like below:

var (
	redisAddress   = flag.String("redis-address", ":6379", "Address to the Redis server")
	maxConnections = flag.Int("max-connections", 10, "Max connections to Redis")
)

func SetupRedis() {
	pool := redis.NewPool(func() (redis.Conn, error) {
		c, err := redis.Dial("tcp", *redisAddress)

		if err != nil {
			return nil, err
		}

		return c, err
	}, *maxConnections)

	redisCache := &goal.RedisCache{}
	err = redisCache.InitRedisPool(pool)
	if err == nil {
		goal.RegisterCacher(redisCache)
	}
}

redisCache is an instance of goal.Cacher interface. By calling goal.RegisterCacher, goal can use the cacher to quickly get and set your data into cache. If you use Memcached or other type of cache, just implement Cacher interface for your respective cache and register it with Goal.

Authentication

Goal uses Gorilla Session to support user authentication. First you need to let Goal know which model represents your user:

goal.SetUserModel(&testuser{})

You can also uses Goal default paths for routing authentication requests, or change it if you like.

// AddDefaultAuthPaths route request to the model which implement
// authentications
func (api *API) AddDefaultAuthPaths(resource interface{}) {
	api.Mux().Handle("/auth/register", api.registerHandler(resource))
	api.Mux().Handle("/auth/login", api.loginHandler(resource))
	api.Mux().Handle("/auth/logout", api.logoutHandler(resource))
}

Now in the model that you represent your user, implement goal.Registerer, goal.Loginer and goal.Logouter interface. As usual, Goal provides basic implementations for these use cases.

// Setup methods to conform to auth interfaces
func (user *testuser) Register(w http.ResponseWriter, req *http.Request) (int, interface{}, error) {
	currentUser, err := goal.RegisterWithPassword(w, req, "username", "password")

	if err != nil {
		return 500, nil, err
	}

	return 200, currentUser, nil
}

func (user *testuser) Login(w http.ResponseWriter, req *http.Request) (int, interface{}, error) {
	currentUser, err := goal.LoginWithPassword(w, req, "username", "password")

	if err != nil {
		return 500, nil, err
	}

	return 200, currentUser, nil
}

func (user *testuser) Logout(w http.ResponseWriter, req *http.Request) (int, interface{}, error) {
	goal.HandleLogout(w, req)
	return 200, nil, nil
}

You can utilize above implementations or roll out your own authentication mechanism, for example login with Facebook/Google etc. To properly set request/response session, use goal.SetUserSession(w, request, user). After user authenticated successfully, you can retrieve current user by goal.GetCurrentUser(request)

Access Controls

Goal defines simple system based on roles to guard your record. First your user model needs to implement goal.Roler interface, so Goal knows which role current request has:

// Satisfy Roler interface
func (user *testuser) Role() []string {
	ownRole := fmt.Sprintf("testuser:%v", user.ID)
	roles := []string{ownRole}

	return roles
}

For record you want to protect, implements goal.PermitWriter and goal.PermitReader interface:

func (art *article) PermitRead() []string {
	return []string{"admin", "ceo"}
}

func (art *article) PermitWrite() []string {
	return []string{"admin", "ceo"}
}

To make things easier, Goal provides goal.Permission struct so you can embed directly into your own model. This will add a "read" and "write" string column to the table in your database. The format is simply a json array of roles, and it already conforms to PermitReader and PermitWriter interface.

type article struct {
	ID     uint `gorm:"primary_key"`
	Author *testuser
	Title  string
	goal.Permission
}

You could initialize the Permission like below:

art.Permission = goal.Permission{
  Read:  `["admin", "ceo"]`,
  Write: `["admin", "ceo"]`,
}

If a record doesn't implement any Permit* interfaces above, Goal assumes it can be accessed by public

Revision

In order to prevent a record being changed from multiple sources, Goal supports simple strategy based on revision number. The client sends current revision of data to be updated, and server will check if the revision is the latest in database. If it's the latest, server allow data to be updated, else it returns error with the record in the database and client can decide how to resolve the conflict.

It's easy to add revision support by implementing Revisioner interface:

func (user *testuser) CurrentRevision() uint64 {
	return user.Rev
}

func (user *testuser) SetNextRevision() {
	user.Rev = user.Rev + 1
}

License

MIT License

Documentation

Index

Constants

View Source
const (
	GET    = "GET"
	POST   = "POST"
	PUT    = "PUT"
	DELETE = "DELETE"
	HEAD   = "HEAD"
	PATCH  = "PATCH"
)

HTTP Methods

View Source
const (
	// SessionName is default name for user session
	SessionName = "goal.UserSessionName"

	// SessionKey is default key for user object
	SessionKey = "goal.UserSessionKey"
)

Variables

View Source
var SharedSessionStore sessions.Store

SharedSessionStore used to generate session for multiple requests

Functions

func Cache

func Cache(scope *gorm.Scope)

Cache data to cacher

func CacheKey

func CacheKey(resource interface{}) string

CacheKey defines by the struct or fallback to name:id format

func CanMerge

func CanMerge(current Revisioner, updated Revisioner) bool

CanMerge check if the updated object can be safely merged to current object

func CanPerform

func CanPerform(resource interface{}, request *http.Request, read bool) error

CanPerform check if a roler can access a resource (read/write) If read is false, then it will check for write permission It will return error if the check is failed

func ClearUserSession

func ClearUserSession(w http.ResponseWriter, req *http.Request) error

ClearUserSession removes the current user from session

func Create

func Create(rType reflect.Type, request *http.Request) (int, interface{}, error)

Create provides basic implementation to create a record into the database

func DB

func DB() *gorm.DB

DB returns global variable db

func DefaultCacheKey

func DefaultCacheKey(name string, id interface{}) string

DefaultCacheKey returns default format for redis key

func Delete

func Delete(rType reflect.Type, request *http.Request) (int, interface{}, error)

Delete provides basic implementation to delete a record inside a database

func GetCurrentUser

func GetCurrentUser(req *http.Request) (interface{}, error)

GetCurrentUser returns current user based on the request header

func HandleLogout

func HandleLogout(w http.ResponseWriter, request *http.Request)

HandleLogout let user logout from the system

func HandleQuery

func HandleQuery(rType reflect.Type, request *http.Request) (int, interface{}, error)

HandleQuery retrieves results filtered by request parameters

func InitGormDb

func InitGormDb(newDb *gorm.DB)

InitGormDb initializes global variable db

func InitSessionStore

func InitSessionStore(store sessions.Store)

InitSessionStore initializes SharedSessionStore

func LoginWithPassword

func LoginWithPassword(
	w http.ResponseWriter, request *http.Request,
	usernameCol string, passwordCol string) (interface{}, error)

LoginWithPassword checks if username and password correct and set user into session

func Pool

func Pool() *redis.Pool

Pool returns global connection pool

func Read

func Read(rType reflect.Type, request *http.Request) (int, interface{}, error)

Read provides basic implementation to retrieve object based on request parameters

func RedisClearAll

func RedisClearAll() error

RedisClearAll clear all data from connection's CURRENT database

func RegisterCacher

func RegisterCacher(cache Cacher)

RegisterCacher initializes SharedCache

func RegisterModel

func RegisterModel(resource interface{})

RegisterModel initializes default routes for a model

func RegisterWithPassword

func RegisterWithPassword(
	w http.ResponseWriter, request *http.Request,
	usernameCol string, passwordCol string) (interface{}, error)

RegisterWithPassword checks if username exists and sets password with bcrypt algorithm Client can provides extra data to be saved into database for user

func SetUserModel

func SetUserModel(user interface{})

SetUserModel lets goal which model act as user

func SetUserSession

func SetUserSession(w http.ResponseWriter, req *http.Request, user interface{}) error

SetUserSession sets current user to session

func TableName

func TableName(resource interface{}) string

TableName returns table name for the resource

func Uncache

func Uncache(scope *gorm.Scope)

Uncache data from cacher

func Update

func Update(rType reflect.Type, request *http.Request) (int, interface{}, error)

Update provides basic implementation to update a record inside database

Types

type API

type API struct {
	// contains filtered or unexported fields
}

An API manages a group of resources by routing requests to the correct method on a matching resource and marshalling the returned data to JSON for the HTTP response.

You can instantiate multiple APIs on separate ports. Each API will manage its own set of resources.

func NewAPI

func NewAPI() *API

NewAPI allocates and returns a new API.

func SharedAPI

func SharedAPI() *API

SharedAPI return API instance

func (*API) AddCrudResource

func (api *API) AddCrudResource(resource interface{}, paths ...string)

AddCrudResource adds a new resource to an API. The API will route requests that match one of the given paths to the matching HTTP method on the resource.

func (*API) AddDefaultAuthPaths

func (api *API) AddDefaultAuthPaths(resource interface{})

AddDefaultAuthPaths route request to the model which implement authentications

func (*API) AddDefaultCrudPaths

func (api *API) AddDefaultCrudPaths(resource interface{})

AddDefaultCrudPaths adds default path for a resource. The default path is based on the struct name

func (*API) AddDefaultQueryPath

func (api *API) AddDefaultQueryPath(resource interface{})

AddDefaultQueryPath allows model to support query based on request data, return filtered results back to client. The path is created base on struct name

func (*API) AddLoginPath

func (api *API) AddLoginPath(resource interface{}, path string)

AddLoginPath let user login to system

func (*API) AddLogoutPath

func (api *API) AddLogoutPath(resource interface{}, path string)

AddLogoutPath let user logout from the system

func (*API) AddQueryResource

func (api *API) AddQueryResource(resource interface{}, path string)

AddQueryResource allows model to support query based on request data, return filtered results back to client

func (*API) AddRegisterPath

func (api *API) AddRegisterPath(resource interface{}, path string)

AddRegisterPath let user to register into a system

func (*API) Mux

func (api *API) Mux() *mux.Router

Mux returns Gorilla's mux.Router used by an API. If a mux does not yet exist, a new one will be created and returned

type Cacher

type Cacher interface {
	Get(string, interface{}) error
	Set(string, interface{}) error
	Delete(string) error
	Exists(string) (bool, error)
}

Cacher defines a interface for fast key-value caching

var SharedCache Cacher

SharedCache is global variable to cache data

type DeleteSupporter

type DeleteSupporter interface {
	Delete(http.ResponseWriter, *http.Request) (int, interface{}, error)
}

DeleteSupporter is the interface that provides the Delete method a resource must support to receive HTTP DELETEs.

type GetSupporter

type GetSupporter interface {
	Get(http.ResponseWriter, *http.Request) (int, interface{}, error)
}

GetSupporter is the interface that provides the Get method a resource must support to receive HTTP GETs.

type HeadSupporter

type HeadSupporter interface {
	Head(http.ResponseWriter, *http.Request) (int, interface{}, error)
}

HeadSupporter is the interface that provides the Head method a resource must support to receive HTTP HEADs.

type Loginer

type Loginer interface {
	Login(http.ResponseWriter, *http.Request) (int, interface{}, error)
}

Loginer authenticates user into the system

type Logouter

type Logouter interface {
	Logout(http.ResponseWriter, *http.Request) (int, interface{}, error)
}

Logouter clear sessions and log user out

type PatchSupporter

type PatchSupporter interface {
	Patch(http.ResponseWriter, *http.Request) (int, interface{}, error)
}

PatchSupporter is the interface that provides the Patch method a resource must support to receive HTTP PATCHs.

type Permission

type Permission struct {
	Read  string
	Write string
}

Permission makes it easier to implement access control

func (*Permission) PermitRead

func (p *Permission) PermitRead() []string

PermitRead conforms to PermitReader interface

func (*Permission) PermitWrite

func (p *Permission) PermitWrite() []string

PermitWrite conforms to PermitWriter interface

type PermitReader

type PermitReader interface {
	PermitRead() []string
}

PermitReader allows authenticated user to read the record

type PermitWriter

type PermitWriter interface {
	PermitWrite() []string
}

PermitWriter allows authenticated user to write the record

type PostSupporter

type PostSupporter interface {
	Post(http.ResponseWriter, *http.Request) (int, interface{}, error)
}

PostSupporter is the interface that provides the Post method a resource must support to receive HTTP POSTs.

type PutSupporter

type PutSupporter interface {
	Put(http.ResponseWriter, *http.Request) (int, interface{}, error)
}

PutSupporter is the interface that provides the Put method a resource must support to receive HTTP PUTs.

type QueryItem

type QueryItem struct {
	Key string       `json:"key"`
	Op  string       `json:"op"`
	Val interface{}  `json:"val"`
	Or  []*QueryItem `json:"or"`
}

QueryItem defines most basic element of a query. For example: name = Thomas

type QueryParams

type QueryParams struct {
	Where   []*QueryItem    `json:"where"`
	Limit   int64           `json:"limit"`
	Order   map[string]bool `json:"order"`
	Include []string        `json:"include"`
}

QueryParams defines structure of a query. Where clause may include multiple QueryItem and connect by "AND" operator

func (*QueryParams) Find

func (params *QueryParams) Find(resource interface{}, results interface{}) error

Find constructs the query, return error immediately if query is invalid, and query database if everything is valid

type QuerySupporter

type QuerySupporter interface {
	Query(http.ResponseWriter, *http.Request) (int, interface{}, error)
}

QuerySupporter is the interface that return filtered results based on request paramters

type RedisCache

type RedisCache struct{}

RedisCache implements Cacher interface

func (*RedisCache) Delete

func (cache *RedisCache) Delete(key string) error

Delete a key from Redis

func (*RedisCache) Exists

func (cache *RedisCache) Exists(key string) (bool, error)

Exists checks if a key exists inside Redis

func (*RedisCache) Get

func (cache *RedisCache) Get(key string, val interface{}) error

Get returns data for a key

func (*RedisCache) InitRedisPool

func (cache *RedisCache) InitRedisPool(p *redis.Pool) error

InitRedisPool initializes Redis and connection pool

func (*RedisCache) Set

func (cache *RedisCache) Set(key string, val interface{}) error

Set a val for a key into Redis

type Registerer

type Registerer interface {
	Register(http.ResponseWriter, *http.Request) (int, interface{}, error)
}

Registerer register a new user to system

type Revisioner

type Revisioner interface {
	CurrentRevision() int64
	SetNextRevision()
}

Revisioner tell the revision of current record. It is typically used to avoid data being overridden by multiple clients

type Roler

type Roler interface {
	Roles() []string
}

Roler is usually assigned to User class, which define which role user has: ["admin", "user_id"]

Jump to

Keyboard shortcuts

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