mirror of
https://github.com/kataras/iris.git
synced 2025-12-18 02:17:05 +00:00
reorganization of _examples and add some new examples such as iris+groupcache+mysql+docker
Former-commit-id: ed635ee95de7160cde11eaabc0c1dcb0e460a620
This commit is contained in:
97
_examples/database/mysql/api/api.go
Normal file
97
_examples/database/mysql/api/api.go
Normal file
@@ -0,0 +1,97 @@
|
||||
// Package api contains the handlers for our HTTP Endpoints.
|
||||
package api
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"myapp/service"
|
||||
"myapp/sql"
|
||||
|
||||
"github.com/kataras/iris/v12"
|
||||
"github.com/kataras/iris/v12/middleware/jwt"
|
||||
"github.com/kataras/iris/v12/middleware/requestid"
|
||||
)
|
||||
|
||||
// Router accepts any required dependencies and returns the main server's handler.
|
||||
func Router(db sql.Database, secret string) func(iris.Party) {
|
||||
return func(r iris.Party) {
|
||||
j := jwt.HMAC(15*time.Minute, secret)
|
||||
|
||||
r.Use(requestid.New())
|
||||
r.Use(verifyToken(j))
|
||||
// Generate a token for testing by navigating to
|
||||
// http://localhost:8080/token endpoint.
|
||||
// Copy-paste it to a ?token=$token url parameter or
|
||||
// open postman and put an Authentication: Bearer $token to get
|
||||
// access on create, update and delete endpoinds.
|
||||
|
||||
r.Get("/token", writeToken(j))
|
||||
|
||||
var (
|
||||
categoryService = service.NewCategoryService(db)
|
||||
productService = service.NewProductService(db)
|
||||
)
|
||||
|
||||
cat := r.Party("/category")
|
||||
{
|
||||
// TODO: new Use to add middlewares to specific
|
||||
// routes per METHOD ( we already have the per path through parties.)
|
||||
handler := NewCategoryHandler(categoryService)
|
||||
|
||||
cat.Get("/", handler.List)
|
||||
cat.Post("/", handler.Create)
|
||||
cat.Put("/", handler.Update)
|
||||
|
||||
cat.Get("/{id:int64}", handler.GetByID)
|
||||
cat.Patch("/{id:int64}", handler.PartialUpdate)
|
||||
cat.Delete("/{id:int64}", handler.Delete)
|
||||
/* You can also do something like that:
|
||||
cat.PartyFunc("/{id:int64}", func(c iris.Party) {
|
||||
c.Get("/", handler.GetByID)
|
||||
c.Post("/", handler.PartialUpdate)
|
||||
c.Delete("/", handler.Delete)
|
||||
})
|
||||
*/
|
||||
|
||||
cat.Get("/{id:int64}/products", handler.ListProducts)
|
||||
cat.Post("/{id:int64}/products", handler.InsertProducts(productService))
|
||||
}
|
||||
|
||||
prod := r.Party("/product")
|
||||
{
|
||||
handler := NewProductHandler(productService)
|
||||
|
||||
prod.Get("/", handler.List)
|
||||
prod.Post("/", handler.Create)
|
||||
prod.Put("/", handler.Update)
|
||||
|
||||
prod.Get("/{id:int64}", handler.GetByID)
|
||||
prod.Patch("/{id:int64}", handler.PartialUpdate)
|
||||
prod.Delete("/{id:int64}", handler.Delete)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func writeToken(j *jwt.JWT) iris.Handler {
|
||||
return func(ctx iris.Context) {
|
||||
claims := jwt.Claims{
|
||||
Issuer: "https://iris-go.com",
|
||||
Audience: jwt.Audience{requestid.Get(ctx)},
|
||||
}
|
||||
|
||||
j.WriteToken(ctx, claims)
|
||||
}
|
||||
}
|
||||
|
||||
func verifyToken(j *jwt.JWT) iris.Handler {
|
||||
return func(ctx iris.Context) {
|
||||
// Allow all GET.
|
||||
if ctx.Method() == iris.MethodGet {
|
||||
ctx.Next()
|
||||
return
|
||||
}
|
||||
|
||||
j.Verify(ctx)
|
||||
}
|
||||
}
|
||||
251
_examples/database/mysql/api/category_handler.go
Normal file
251
_examples/database/mysql/api/category_handler.go
Normal file
@@ -0,0 +1,251 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"myapp/entity"
|
||||
"myapp/service"
|
||||
"myapp/sql"
|
||||
|
||||
"github.com/kataras/iris/v12"
|
||||
)
|
||||
|
||||
// CategoryHandler is the http mux for categories.
|
||||
type CategoryHandler struct {
|
||||
// [...options]
|
||||
|
||||
service *service.CategoryService
|
||||
}
|
||||
|
||||
// NewCategoryHandler returns the main controller for the categories API.
|
||||
func NewCategoryHandler(service *service.CategoryService) *CategoryHandler {
|
||||
return &CategoryHandler{service}
|
||||
}
|
||||
|
||||
// GetByID fetches a single record from the database and sends it to the client.
|
||||
// Method: GET.
|
||||
func (h *CategoryHandler) GetByID(ctx iris.Context) {
|
||||
id := ctx.Params().GetInt64Default("id", 0)
|
||||
|
||||
var cat entity.Category
|
||||
err := h.service.GetByID(ctx.Request().Context(), &cat, id)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
writeEntityNotFound(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
debugf("CategoryHandler.GetByID(id=%d): %v", id, err)
|
||||
writeInternalServerError(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(cat)
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
type (
|
||||
List struct {
|
||||
Data interface{} `json:"data"`
|
||||
Order string `json:"order"`
|
||||
Next Range `json:"next,omitempty"`
|
||||
Prev Range `json:"prev,omitempty"`
|
||||
}
|
||||
|
||||
Range struct {
|
||||
Offset int64 `json:"offset"`
|
||||
Limit int64 `json:"limit`
|
||||
}
|
||||
)
|
||||
*/
|
||||
|
||||
// List lists a set of records from the database.
|
||||
// Method: GET.
|
||||
func (h *CategoryHandler) List(ctx iris.Context) {
|
||||
q := ctx.Request().URL.Query()
|
||||
opts := sql.ParseListOptions(q)
|
||||
|
||||
// initialize here in order to return an empty json array `[]` instead of `null`.
|
||||
categories := entity.Categories{}
|
||||
err := h.service.List(ctx.Request().Context(), &categories, opts)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
debugf("CategoryHandler.List(DB) (limit=%d offset=%d where=%s=%v): %v",
|
||||
opts.Limit, opts.Offset, opts.WhereColumn, opts.WhereValue, err)
|
||||
|
||||
writeInternalServerError(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(categories)
|
||||
}
|
||||
|
||||
// Create adds a record to the database.
|
||||
// Method: POST.
|
||||
func (h *CategoryHandler) Create(ctx iris.Context) {
|
||||
var cat entity.Category
|
||||
if err := ctx.ReadJSON(&cat); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
id, err := h.service.Insert(ctx.Request().Context(), cat)
|
||||
if err != nil {
|
||||
if err == sql.ErrUnprocessable {
|
||||
ctx.StopWithJSON(iris.StatusUnprocessableEntity, newError(iris.StatusUnprocessableEntity, ctx.Request().Method, ctx.Path(), "required fields are missing"))
|
||||
return
|
||||
}
|
||||
|
||||
debugf("CategoryHandler.Create(DB): %v", err)
|
||||
writeInternalServerError(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
// Send 201 with body of {"id":$last_inserted_id"}.
|
||||
ctx.StatusCode(iris.StatusCreated)
|
||||
ctx.JSON(iris.Map{cat.PrimaryKey(): id})
|
||||
}
|
||||
|
||||
// Update performs a full-update of a record in the database.
|
||||
// Method: PUT.
|
||||
func (h *CategoryHandler) Update(ctx iris.Context) {
|
||||
var cat entity.Category
|
||||
if err := ctx.ReadJSON(&cat); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
affected, err := h.service.Update(ctx.Request().Context(), cat)
|
||||
if err != nil {
|
||||
if err == sql.ErrUnprocessable {
|
||||
ctx.StopWithJSON(iris.StatusUnprocessableEntity, newError(iris.StatusUnprocessableEntity, ctx.Request().Method, ctx.Path(), "required fields are missing"))
|
||||
return
|
||||
}
|
||||
|
||||
debugf("CategoryHandler.Update(DB): %v", err)
|
||||
writeInternalServerError(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
status := iris.StatusOK
|
||||
if affected == 0 {
|
||||
status = iris.StatusNotModified
|
||||
}
|
||||
|
||||
ctx.StatusCode(status)
|
||||
}
|
||||
|
||||
// PartialUpdate is the handler for partially update one or more fields of the record.
|
||||
// Method: PATCH.
|
||||
func (h *CategoryHandler) PartialUpdate(ctx iris.Context) {
|
||||
id := ctx.Params().GetInt64Default("id", 0)
|
||||
|
||||
var attrs map[string]interface{}
|
||||
if err := ctx.ReadJSON(&attrs); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
affected, err := h.service.PartialUpdate(ctx.Request().Context(), id, attrs)
|
||||
if err != nil {
|
||||
if err == sql.ErrUnprocessable {
|
||||
ctx.StopWithJSON(iris.StatusUnprocessableEntity, newError(iris.StatusUnprocessableEntity, ctx.Request().Method, ctx.Path(), "unsupported value(s)"))
|
||||
return
|
||||
}
|
||||
|
||||
debugf("CategoryHandler.PartialUpdate(DB): %v", err)
|
||||
writeInternalServerError(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
status := iris.StatusOK
|
||||
if affected == 0 {
|
||||
status = iris.StatusNotModified
|
||||
}
|
||||
|
||||
ctx.StatusCode(status)
|
||||
}
|
||||
|
||||
// Delete removes a record from the database.
|
||||
// Method: DELETE.
|
||||
func (h *CategoryHandler) Delete(ctx iris.Context) {
|
||||
id := ctx.Params().GetInt64Default("id", 0)
|
||||
|
||||
affected, err := h.service.DeleteByID(ctx.Request().Context(), id)
|
||||
if err != nil {
|
||||
debugf("CategoryHandler.Delete(DB): %v", err)
|
||||
writeInternalServerError(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
status := iris.StatusOK // StatusNoContent
|
||||
if affected == 0 {
|
||||
status = iris.StatusNotModified
|
||||
}
|
||||
|
||||
ctx.StatusCode(status)
|
||||
}
|
||||
|
||||
// Products.
|
||||
|
||||
// ListProducts lists products of a Category.
|
||||
// Example: from cheap to expensive:
|
||||
// http://localhost:8080/category/3/products?offset=0&limit=30&by=price&order=asc
|
||||
// Method: GET.
|
||||
func (h *CategoryHandler) ListProducts(ctx iris.Context) {
|
||||
id := ctx.Params().GetInt64Default("id", 0)
|
||||
|
||||
// NOTE: could add cache here too.
|
||||
|
||||
q := ctx.Request().URL.Query()
|
||||
opts := sql.ParseListOptions(q).Where("category_id", id)
|
||||
opts.Table = "products"
|
||||
if opts.OrderByColumn == "" {
|
||||
opts.OrderByColumn = "updated_at"
|
||||
}
|
||||
|
||||
var products entity.Products
|
||||
err := h.service.List(ctx.Request().Context(), &products, opts)
|
||||
if err != nil {
|
||||
debugf("CategoryHandler.ListProducts(DB) (table=%s where=%s=%v limit=%d offset=%d): %v",
|
||||
opts.Table, opts.WhereColumn, opts.WhereValue, opts.Limit, opts.Offset, err)
|
||||
|
||||
writeInternalServerError(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(products)
|
||||
}
|
||||
|
||||
// InsertProducts assigns new products to a Category (accepts a list of products).
|
||||
// Method: POST.
|
||||
func (h *CategoryHandler) InsertProducts(productService *service.ProductService) iris.Handler {
|
||||
return func(ctx iris.Context) {
|
||||
categoryID := ctx.Params().GetInt64Default("id", 0)
|
||||
|
||||
var products []entity.Product
|
||||
if err := ctx.ReadJSON(&products); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for i := range products {
|
||||
products[i].CategoryID = categoryID
|
||||
}
|
||||
|
||||
inserted, err := productService.BatchInsert(ctx.Request().Context(), products)
|
||||
if err != nil {
|
||||
if err == sql.ErrUnprocessable {
|
||||
ctx.StopWithJSON(iris.StatusUnprocessableEntity, newError(iris.StatusUnprocessableEntity, ctx.Request().Method, ctx.Path(), "required fields are missing"))
|
||||
return
|
||||
}
|
||||
|
||||
debugf("CategoryHandler.InsertProducts(DB): %v", err)
|
||||
writeInternalServerError(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
if inserted == 0 {
|
||||
ctx.StatusCode(iris.StatusNotModified)
|
||||
return
|
||||
}
|
||||
|
||||
// Send 201 with body of {"inserted":$inserted"}.
|
||||
ctx.StatusCode(iris.StatusCreated)
|
||||
ctx.JSON(iris.Map{"inserted": inserted})
|
||||
}
|
||||
}
|
||||
25
_examples/database/mysql/api/helper.go
Normal file
25
_examples/database/mysql/api/helper.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/kataras/iris/v12"
|
||||
)
|
||||
|
||||
const debug = true
|
||||
|
||||
func debugf(format string, args ...interface{}) {
|
||||
if !debug {
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf(format, args...)
|
||||
}
|
||||
|
||||
func writeInternalServerError(ctx iris.Context) {
|
||||
ctx.StopWithJSON(iris.StatusInternalServerError, newError(iris.StatusInternalServerError, ctx.Request().Method, ctx.Path(), ""))
|
||||
}
|
||||
|
||||
func writeEntityNotFound(ctx iris.Context) {
|
||||
ctx.StopWithJSON(iris.StatusNotFound, newError(iris.StatusNotFound, ctx.Request().Method, ctx.Path(), "entity does not exist"))
|
||||
}
|
||||
60
_examples/database/mysql/api/httperror.go
Normal file
60
_examples/database/mysql/api/httperror.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/kataras/iris/v12"
|
||||
)
|
||||
|
||||
// Error holds the error sent by server to clients (JSON).
|
||||
type Error struct {
|
||||
StatusCode int `json:"code"`
|
||||
Method string `json:"-"`
|
||||
Path string `json:"-"`
|
||||
Message string `json:"message"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
}
|
||||
|
||||
func newError(statusCode int, method, path, format string, args ...interface{}) Error {
|
||||
msg := format
|
||||
if len(args) > 0 {
|
||||
// why we check for that? If the original error message came from our database
|
||||
// and it contains fmt-reserved words
|
||||
// like %s or %d we will get MISSING(=...) in our error message and we don't want that.
|
||||
msg = fmt.Sprintf(msg, args...)
|
||||
}
|
||||
|
||||
if msg == "" {
|
||||
msg = iris.StatusText(statusCode)
|
||||
}
|
||||
|
||||
return Error{
|
||||
StatusCode: statusCode,
|
||||
Method: method,
|
||||
Path: path,
|
||||
Message: msg,
|
||||
Timestamp: time.Now().Unix(),
|
||||
}
|
||||
}
|
||||
|
||||
// Error implements the internal Go error interface.
|
||||
func (e Error) Error() string {
|
||||
return fmt.Sprintf("[%d] %s: %s: %s", e.StatusCode, e.Method, e.Path, e.Message)
|
||||
}
|
||||
|
||||
// Is implements the standard `errors.Is` internal interface.
|
||||
// Usage: errors.Is(e, target)
|
||||
func (e Error) Is(target error) bool {
|
||||
if target == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
err, ok := target.(Error)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
return (err.StatusCode == e.StatusCode || e.StatusCode == 0) &&
|
||||
(err.Message == e.Message || e.Message == "")
|
||||
}
|
||||
0
_examples/database/mysql/api/middleware/.gitkeep
Normal file
0
_examples/database/mysql/api/middleware/.gitkeep
Normal file
173
_examples/database/mysql/api/product_handler.go
Normal file
173
_examples/database/mysql/api/product_handler.go
Normal file
@@ -0,0 +1,173 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"myapp/cache"
|
||||
"myapp/entity"
|
||||
"myapp/service"
|
||||
"myapp/sql"
|
||||
|
||||
"github.com/kataras/iris/v12"
|
||||
)
|
||||
|
||||
// ProductHandler is the http mux for products.
|
||||
type ProductHandler struct {
|
||||
service *service.ProductService
|
||||
cache *cache.Cache
|
||||
}
|
||||
|
||||
// NewProductHandler returns the main controller for the products API.
|
||||
func NewProductHandler(service *service.ProductService) *ProductHandler {
|
||||
return &ProductHandler{
|
||||
service: service,
|
||||
cache: cache.New(service, "products", time.Minute),
|
||||
}
|
||||
}
|
||||
|
||||
// GetByID fetches a single record from the database and sends it to the client.
|
||||
// Method: GET.
|
||||
func (h *ProductHandler) GetByID(ctx iris.Context) {
|
||||
id := ctx.Params().GetString("id")
|
||||
|
||||
var product []byte
|
||||
err := h.cache.GetByID(ctx.Request().Context(), id, &product)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
writeEntityNotFound(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
debugf("ProductHandler.GetByID(id=%v): %v", id, err)
|
||||
writeInternalServerError(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.ContentType("application/json")
|
||||
ctx.Write(product)
|
||||
|
||||
// ^ Could use our simple `noCache` or implement a Cache-Control (see kataras/iris/cache for that)
|
||||
// but let's keep it simple.
|
||||
}
|
||||
|
||||
// List lists a set of records from the database.
|
||||
// Method: GET.
|
||||
func (h *ProductHandler) List(ctx iris.Context) {
|
||||
key := ctx.Request().URL.RawQuery
|
||||
|
||||
products := []byte("[]")
|
||||
err := h.cache.List(ctx.Request().Context(), key, &products)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
debugf("ProductHandler.List(DB) (%s): %v",
|
||||
key, err)
|
||||
|
||||
writeInternalServerError(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.ContentType("application/json")
|
||||
ctx.Write(products)
|
||||
}
|
||||
|
||||
// Create adds a record to the database.
|
||||
// Method: POST.
|
||||
func (h *ProductHandler) Create(ctx iris.Context) {
|
||||
var product entity.Product
|
||||
if err := ctx.ReadJSON(&product); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
id, err := h.service.Insert(ctx.Request().Context(), product)
|
||||
if err != nil {
|
||||
if err == sql.ErrUnprocessable {
|
||||
ctx.StopWithJSON(iris.StatusUnprocessableEntity, newError(iris.StatusUnprocessableEntity, ctx.Request().Method, ctx.Path(), "required fields are missing"))
|
||||
return
|
||||
}
|
||||
|
||||
debugf("ProductHandler.Create(DB): %v", err)
|
||||
writeInternalServerError(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
// Send 201 with body of {"id":$last_inserted_id"}.
|
||||
ctx.StatusCode(iris.StatusCreated)
|
||||
ctx.JSON(iris.Map{product.PrimaryKey(): id})
|
||||
}
|
||||
|
||||
// Update performs a full-update of a record in the database.
|
||||
// Method: PUT.
|
||||
func (h *ProductHandler) Update(ctx iris.Context) {
|
||||
var product entity.Product
|
||||
if err := ctx.ReadJSON(&product); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
affected, err := h.service.Update(ctx.Request().Context(), product)
|
||||
if err != nil {
|
||||
if err == sql.ErrUnprocessable {
|
||||
ctx.StopWithJSON(iris.StatusUnprocessableEntity, newError(iris.StatusUnprocessableEntity, ctx.Request().Method, ctx.Path(), "required fields are missing"))
|
||||
return
|
||||
}
|
||||
|
||||
debugf("ProductHandler.Update(DB): %v", err)
|
||||
writeInternalServerError(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
status := iris.StatusOK
|
||||
if affected == 0 {
|
||||
status = iris.StatusNotModified
|
||||
}
|
||||
|
||||
ctx.StatusCode(status)
|
||||
}
|
||||
|
||||
// PartialUpdate is the handler for partially update one or more fields of the record.
|
||||
// Method: PATCH.
|
||||
func (h *ProductHandler) PartialUpdate(ctx iris.Context) {
|
||||
id := ctx.Params().GetInt64Default("id", 0)
|
||||
|
||||
var attrs map[string]interface{}
|
||||
if err := ctx.ReadJSON(&attrs); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
affected, err := h.service.PartialUpdate(ctx.Request().Context(), id, attrs)
|
||||
if err != nil {
|
||||
if err == sql.ErrUnprocessable {
|
||||
ctx.StopWithJSON(iris.StatusUnprocessableEntity, newError(iris.StatusUnprocessableEntity, ctx.Request().Method, ctx.Path(), "unsupported value(s)"))
|
||||
return
|
||||
}
|
||||
|
||||
debugf("ProductHandler.PartialUpdate(DB): %v", err)
|
||||
writeInternalServerError(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
status := iris.StatusOK
|
||||
if affected == 0 {
|
||||
status = iris.StatusNotModified
|
||||
}
|
||||
|
||||
ctx.StatusCode(status)
|
||||
}
|
||||
|
||||
// Delete removes a record from the database.
|
||||
// Method: DELETE.
|
||||
func (h *ProductHandler) Delete(ctx iris.Context) {
|
||||
id := ctx.Params().GetInt64Default("id", 0)
|
||||
|
||||
affected, err := h.service.DeleteByID(ctx.Request().Context(), id)
|
||||
if err != nil {
|
||||
debugf("ProductHandler.Delete(DB): %v", err)
|
||||
writeInternalServerError(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
status := iris.StatusOK // StatusNoContent
|
||||
if affected == 0 {
|
||||
status = iris.StatusNotModified
|
||||
}
|
||||
|
||||
ctx.StatusCode(status)
|
||||
}
|
||||
Reference in New Issue
Block a user