reorganization of _examples and add some new examples such as iris+groupcache+mysql+docker
Former-commit-id: ed635ee95de7160cde11eaabc0c1dcb0e460a620
19
HISTORY.md
|
@ -159,7 +159,7 @@ Prior to this version the `iris.Context` was the only one dependency that has be
|
|||
| `float, float32, float64`, | |
|
||||
| `bool`, | |
|
||||
| `slice` | [Path Parameter](https://github.com/kataras/iris/wiki/Routing-path-parameter-types) |
|
||||
| Struct | [Request Body](https://github.com/kataras/iris/tree/master/_examples/http_request) of `JSON`, `XML`, `YAML`, `Form`, `URL Query`, `Protobuf`, `MsgPack` |
|
||||
| Struct | [Request Body](https://github.com/kataras/iris/tree/master/_examples/request-body) of `JSON`, `XML`, `YAML`, `Form`, `URL Query`, `Protobuf`, `MsgPack` |
|
||||
|
||||
Here is a preview of what the new Hero handlers look like:
|
||||
|
||||
|
@ -393,7 +393,7 @@ Other Improvements:
|
|||
|
||||
- New `iris.WithLowercaseRouting` option which forces all routes' paths to be lowercase and converts request paths to their lowercase for matching.
|
||||
|
||||
- New `app.Validator { Struct(interface{}) error }` field and `app.Validate` method were added. The `app.Validator = ` can be used to integrate a 3rd-party package such as [go-playground/validator](https://github.com/go-playground/validator). If set-ed then Iris `Context`'s `ReadJSON`, `ReadXML`, `ReadMsgPack`, `ReadYAML`, `ReadForm`, `ReadQuery`, `ReadBody` methods will return the validation error on data validation failures. The [read-json-struct-validation](_examples/http_request/read-json-struct-validation) example was updated.
|
||||
- New `app.Validator { Struct(interface{}) error }` field and `app.Validate` method were added. The `app.Validator = ` can be used to integrate a 3rd-party package such as [go-playground/validator](https://github.com/go-playground/validator). If set-ed then Iris `Context`'s `ReadJSON`, `ReadXML`, `ReadMsgPack`, `ReadYAML`, `ReadForm`, `ReadQuery`, `ReadBody` methods will return the validation error on data validation failures. The [read-json-struct-validation](_examples/request-body/read-json-struct-validation) example was updated.
|
||||
|
||||
- A result of <T> can implement the new `hero.PreflightResult` interface which contains a single method of `Preflight(iris.Context) error`. If this method exists on a custom struct value which is returned from a handler then it will fire that `Preflight` first and if not errored then it will cotninue by sending the struct value as JSON(by-default) response body.
|
||||
|
||||
|
@ -415,7 +415,7 @@ New Package-level Variables:
|
|||
|
||||
New Context Methods:
|
||||
|
||||
- `Context.GzipReader(enable bool)` method and `iris.GzipReader` middleware to enable future request read body calls to decompress data using gzip, [example](_examples/http_request/read-gzip).
|
||||
- `Context.GzipReader(enable bool)` method and `iris.GzipReader` middleware to enable future request read body calls to decompress data using gzip, [example](_examples/request-body/read-gzip).
|
||||
- `Context.RegisterDependency(v interface{})` and `Context.RemoveDependency(typ reflect.Type)` to register/remove struct dependencies on serve-time through a middleware.
|
||||
- `Context.SetID(id interface{})` and `Context.GetID() interface{}` added to register a custom unique indetifier to the Context, if necessary.
|
||||
- `Context.GetDomain() string` returns the domain.
|
||||
|
@ -476,7 +476,6 @@ Implement **new** `SetRegisterRule(iris.RouteOverride, RouteSkip, RouteError)` t
|
|||
|
||||
New Examples:
|
||||
|
||||
- [_examples/Docker](_examples/Docker)
|
||||
- [_examples/routing/route-register-rule](_examples/routing/route-register-rule)
|
||||
|
||||
# We, 05 February 2020 | v12.1.6
|
||||
|
@ -570,10 +569,10 @@ Navigate through: https://github.com/kataras/iris/wiki/Sitemap for more.
|
|||
## New Examples
|
||||
|
||||
2. [_examples/i18n](_examples/i18n)
|
||||
1. [_examples/sitemap](_examples/sitemap)
|
||||
3. [_examples/desktop-app/blink](_examples/desktop-app/blink)
|
||||
4. [_examples/desktop-app/lorca](_examples/desktop-app/lorca)
|
||||
5. [_examples/desktop-app/webview](_examples/desktop-app/webview)
|
||||
1. [_examples/sitemap](_examples/routing/sitemap)
|
||||
3. [_examples/desktop/blink](_examples/desktop/blink)
|
||||
4. [_examples/desktop/lorca](_examples/desktop/lorca)
|
||||
5. [_examples/desktop/webview](_examples/desktop/webview)
|
||||
|
||||
# Sa, 26 October 2019 | v12.0.0
|
||||
|
||||
|
@ -603,7 +602,7 @@ The iris-contrib/middleare and examples are updated to use the new `github.com/k
|
|||
# Fr, 16 August 2019 | v11.2.8
|
||||
|
||||
- Set `Cookie.SameSite` to `Lax` when subdomains sessions share is enabled[*](https://github.com/kataras/iris/commit/6bbdd3db9139f9038641ce6f00f7b4bab6e62550)
|
||||
- Add and update all [experimental handlers](https://github.com/kataras/iris/tree/master/_examples/experimental-handlers)
|
||||
- Add and update all [experimental handlers](https://github.com/iris-contrib/middleware)
|
||||
- New `XMLMap` function which wraps a `map[string]interface{}` and converts it to a valid xml content to render through `Context.XML` method
|
||||
- Add new `ProblemOptions.XML` and `RenderXML` fields to render the `Problem` as XML(application/problem+xml) instead of JSON("application/problem+json) and enrich the `Negotiate` to easily accept the `application/problem+xml` mime.
|
||||
|
||||
|
@ -657,7 +656,7 @@ Commit log: https://github.com/kataras/iris/compare/v11.2.3...v11.2.4
|
|||
|
||||
- [New Feature: Handle different parameter types in the same path](https://github.com/kataras/iris/issues/1315)
|
||||
- [New Feature: Content Negotiation](https://github.com/kataras/iris/issues/1319)
|
||||
- [Context.ReadYAML](https://github.com/kataras/iris/tree/master/_examples/http_request/read-yaml)
|
||||
- [Context.ReadYAML](https://github.com/kataras/iris/tree/master/_examples/request-body/read-yaml)
|
||||
- Fixes https://github.com/kataras/neffos/issues/1#issuecomment-515698536
|
||||
|
||||
# We, 24 July 2019 | v11.2.2
|
||||
|
|
|
@ -108,7 +108,7 @@ Registro de commits: https://github.com/kataras/iris/compare/v11.2.3...v11.2.4
|
|||
|
||||
- [Nueva característica: Manejar diferentes tipos de parámetros en la misma ruta](https://github.com/kataras/iris/issues/1315)
|
||||
- [Nueva característica: Negociación de contenido](https://github.com/kataras/iris/issues/1319)
|
||||
- [Context.ReadYAML](https://github.com/kataras/iris/tree/master/_examples/http_request/read-yaml)
|
||||
- [Context.ReadYAML](https://github.com/kataras/iris/tree/master/_examples/request-body/read-yaml)
|
||||
- Ajustes https://github.com/kataras/neffos/issues/1#issuecomment-515698536
|
||||
|
||||
# Miércoles, 24 de julio 2019 | v11.2.2
|
||||
|
|
|
@ -1,31 +1,30 @@
|
|||
# Table of Contents
|
||||
|
||||
* Tutorials
|
||||
* [Dockerize](tutorial/docker)
|
||||
* [Caddy](tutorial/caddy)
|
||||
* [MongoDB](tutorial/mongodb)
|
||||
* [Dropzone.js](tutorial/dropzonejs)
|
||||
* [URL Shortener](tutorial/url-shortener/main.go)
|
||||
* [Online Visitors](tutorial/online-visitors/main.go)
|
||||
* [REST API for Apache Kafka](tutorial/api-for-apache-kafka)
|
||||
* [Vue.js Todo (MVC)](tutorial/vuejs-todo-mvc)
|
||||
* [gRPC (MVC)](mvc/grpc-compatible)
|
||||
* HTTP Listening
|
||||
* [HOST:PORT](http-listening/listen-addr/main.go)
|
||||
* [Public Test Domain](http-listening/listen-addr-public/main.go)
|
||||
* [UNIX socket file](http-listening/listen-unix/main.go)
|
||||
* [TLS](http-listening/listen-tls/main.go)
|
||||
* [Letsencrypt (Automatic Certifications)](http-listening/listen-letsencrypt/main.go)
|
||||
* [Graceful Shutdown](http-listening/graceful-shutdown/default-notifier/main.go)
|
||||
* [Notify on shutdown](http-listening/notify-on-shutdown/main.go)
|
||||
* [REST API for Apache Kafka](kafka-api)
|
||||
* [URL Shortener](url-shortener)
|
||||
* [Dropzone.js](dropzonejs)
|
||||
* [Caddy](caddy)
|
||||
* Database
|
||||
* [MySQL, Groupcache & Docker](database/mysql)
|
||||
* [MongoDB](database/mongodb)
|
||||
* [Xorm](database/orm/xorm/main.go)
|
||||
* [Gorm](database/orm/gorm/main.go)
|
||||
* HTTP Server
|
||||
* [HOST:PORT](http-server/listen-addr/main.go)
|
||||
* [Public Test Domain](http-server/listen-addr-public/main.go)
|
||||
* [UNIX socket file](http-server/listen-unix/main.go)
|
||||
* [TLS](http-server/listen-tls/main.go)
|
||||
* [Letsencrypt (Automatic Certifications)](http-server/listen-letsencrypt/main.go)
|
||||
* [Graceful Shutdown](http-server/graceful-shutdown/default-notifier/main.go)
|
||||
* [Notify on shutdown](http-server/notify-on-shutdown/main.go)
|
||||
* Custom TCP Listener
|
||||
* [Common net.Listener](http-listening/custom-listener/main.go)
|
||||
* [SO_REUSEPORT for unix systems](http-listening/custom-listener/unix-reuseport/main.go)
|
||||
* [Common net.Listener](http-server/custom-listener/main.go)
|
||||
* [SO_REUSEPORT for unix systems](http-server/custom-listener/unix-reuseport/main.go)
|
||||
* Custom HTTP Server
|
||||
* [Pass a custom Server](http-listening/custom-httpserver/easy-way/main.go)
|
||||
* [Use Iris as a single http.Handler](http-listening/custom-httpserver/std-way/main.go)
|
||||
* [Multi Instances](http-listening/custom-httpserver/multi/main.go)
|
||||
* [HTTP/3 Quic](http-listening/http3-quic)
|
||||
* [Pass a custom Server](http-server/custom-httpserver/easy-way/main.go)
|
||||
* [Use Iris as a single http.Handler](http-server/custom-httpserver/std-way/main.go)
|
||||
* [Multi Instances](http-server/custom-httpserver/multi/main.go)
|
||||
* [HTTP/3 Quic](http-server/http3-quic)
|
||||
* Configuration
|
||||
* [Functional](configuration/functional/main.go)
|
||||
* [Configuration Struct](configuration/from-configuration-structure/main.go)
|
||||
|
@ -57,18 +56,26 @@
|
|||
* [Method Overriding](routing/custom-context/method-overriding/main.go)
|
||||
* [New Implementation](routing/custom-context/new-implementation/main.go)
|
||||
* Subdomains
|
||||
* [Single](subdomains/single/main.go)
|
||||
* [Multi](subdomains/multi/main.go)
|
||||
* [Wildcard](subdomains/wildcard/main.go)
|
||||
* [WWW](subdomains/www/main.go)
|
||||
* [Redirection](subdomains/redirect/main.go)
|
||||
* API Versioning
|
||||
* [How it works](https://github.com/kataras/iris/wiki/API-versioning)
|
||||
* [Example](versioning/main.go)
|
||||
* [Single](routing/subdomains/single/main.go)
|
||||
* [Multi](routing/subdomains/multi/main.go)
|
||||
* [Wildcard](routing/subdomains/wildcard/main.go)
|
||||
* [WWW](routing/subdomains/www/main.go)
|
||||
* [Redirection](routing/subdomains/redirect/main.go)
|
||||
* [HTTP Method Override](https://github.com/kataras/iris/blob/master/middleware/methodoverride/methodoverride_test.go)
|
||||
* [API Versioning](routing/versioning/main.go)
|
||||
* [Sitemap](routing/sitemap/main.go)
|
||||
* Logging
|
||||
* [Request Logger](logging/request-logger/main.go)
|
||||
* [Log Requests to a File](logging/request-logger/request-logger-file/main.go)
|
||||
* [Log Requests to a JSON File](logging/request-logger/request-logger-file-json/main.go)
|
||||
* [Application File Logger](logging/file-logger/main.go)
|
||||
* [Application JSON Logger](logging/json-logger/main.go)
|
||||
* API Documentation
|
||||
* [yaag](apidoc/yaag/main.go)
|
||||
* Testing
|
||||
* [Example](testing/httptest/main_test.go)
|
||||
* [Yaag](apidoc/yaag/main.go)
|
||||
* [Swagger](https://github.com/iris-contrib/swagger/tree/master/example)
|
||||
* [Testing](testing/httptest/main_test.go)
|
||||
* [Recovery](recover/main.go)
|
||||
* [Profiling](pprof/main.go)
|
||||
* File Server
|
||||
* [Favicon](file-server/favicon/main.go)
|
||||
* [Basic](file-server/basic/main.go)
|
||||
|
@ -79,6 +86,8 @@
|
|||
* [Basic SPA](file-server/single-page-application/basic/main.go)
|
||||
* [Embedded Single Page Application](file-server/single-page-application/embedded-single-page-application/main.go)
|
||||
* [Embedded Single Page Application with other routes](file-server/single-page-application/embedded-single-page-application-with-other-routes/main.go)
|
||||
* [Upload File](file-server/upload-file/main.go)
|
||||
* [Upload Multiple Files](file-server/upload-files/main.go)
|
||||
* View
|
||||
* [Overview](view/overview/main.go)
|
||||
* [Basic](view/template_html_0/main.go)
|
||||
|
@ -93,51 +102,51 @@
|
|||
* [Pug: `Actions`](view/template_pug_1)
|
||||
* [Pug: `Includes`](view/template_pug_2)
|
||||
* [Pug: `Extends`](view/template_pug_3)
|
||||
* [Jet Template](/view/template_jet_0)
|
||||
* [Jet Template](view/template_jet_0)
|
||||
* [Jet Embedded](view/template_jet_1_embedded)
|
||||
* [Jet 'urlpath' tmpl func](/view/template_jet_2)
|
||||
* [Jet Template Funcs from Struct](/view/template_jet_3)
|
||||
* [Jet 'urlpath' tmpl func](view/template_jet_2)
|
||||
* [Jet Template Funcs from Struct](view/template_jet_3)
|
||||
* Third-Parties
|
||||
* [Render `valyala/quicktemplate` templates](http_responsewriter/quicktemplate)
|
||||
* [Render `shiyanhui/hero` templates](http_responsewriter/herotemplate)
|
||||
* Request Body
|
||||
* [Bind JSON](http_request/read-json/main.go)
|
||||
* * [Struct Validation](http_request/read-json-struct-validation/main.go)
|
||||
* [Bind XML](http_request/read-xml/main.go)
|
||||
* [Bind MsgPack](http_request/read-msgpack/main.go)
|
||||
* [Bind YAML](http_request/read-yaml/main.go)
|
||||
* [Bind Form](http_request/read-form/main.go)
|
||||
* [Bind Query](http_request/read-query/main.go)
|
||||
* [Bind Body](http_request/read-body/main.go)
|
||||
* [Bind Custom per type](http_request/read-custom-per-type/main.go)
|
||||
* [Bind Custom via Unmarshaler](http_request/read-custom-via-unmarshaler/main.go)
|
||||
* [Bind Many times](http_request/read-many/main.go)
|
||||
* [Read/Bind Gzip compressed data](http_request/read-gzip/main.go)
|
||||
* [Upload/Read File](http_request/upload-file/main.go)
|
||||
* [Upload multiple Files](http_request/upload-files/main.go)
|
||||
* [Extract Referrer](http_request/extract-referer/main.go)
|
||||
* Response Writer
|
||||
* [Content Negotiation](http_responsewriter/content-negotiation)
|
||||
* [Text, Markdown, YAML, HTML, JSON, JSONP, Msgpack, XML and Binary](http_responsewriter/write-rest/main.go)
|
||||
* [Write Gzip](http_responsewriter/write-gzip/main.go)
|
||||
* [Stream Writer](http_responsewriter/stream-writer/main.go)
|
||||
* [Transactions](http_responsewriter/transactions/main.go)
|
||||
* [SSE](http_responsewriter/sse/main.go)
|
||||
* [SSE (third-party package usage for server sent events)](http_responsewriter/sse-third-party/main.go)
|
||||
* [Render `valyala/quicktemplate` templates](view/quicktemplate)
|
||||
* [Render `shiyanhui/hero` templates](view/herotemplate)
|
||||
* [Request ID](https://github.com/kataras/iris/blob/master/middleware/requestid/requestid_test.go)
|
||||
* [Request Rate Limit](request-ratelimit/main.go)
|
||||
* [Request Referrer](request-referrer/main.go)
|
||||
* [Webassembly](webassembly/basic/main.go)
|
||||
* Request Body
|
||||
* [Bind JSON](request-body/read-json/main.go)
|
||||
* * [Struct Validation](request-body/read-json-struct-validation/main.go)
|
||||
* [Bind XML](request-body/read-xml/main.go)
|
||||
* [Bind MsgPack](request-body/read-msgpack/main.go)
|
||||
* [Bind YAML](request-body/read-yaml/main.go)
|
||||
* [Bind Form](request-body/read-form/main.go)
|
||||
* [Bind Query](request-body/read-query/main.go)
|
||||
* [Bind Body](request-body/read-body/main.go)
|
||||
* [Bind Custom per type](request-body/read-custom-per-type/main.go)
|
||||
* [Bind Custom via Unmarshaler](request-body/read-custom-via-unmarshaler/main.go)
|
||||
* [Bind Many times](request-body/read-many/main.go)
|
||||
* [Read/Bind Gzip compressed data](request-body/read-gzip/main.go)
|
||||
* Response Writer
|
||||
* [Content Negotiation](response-writer/content-negotiation)
|
||||
* [Text, Markdown, YAML, HTML, JSON, JSONP, Msgpack, XML and Binary](response-writer/write-rest/main.go)
|
||||
* [Write Gzip](response-writer/write-gzip/main.go)
|
||||
* [Stream Writer](response-writer/stream-writer/main.go)
|
||||
* [Transactions](response-writer/transactions/main.go)
|
||||
* [SSE](response-writer/sse/main.go)
|
||||
* [SSE (third-party package usage for server sent events)](response-writer/sse-third-party/main.go)
|
||||
* Cache
|
||||
* [Simple](cache/simple/main.go)
|
||||
* [Client-Side (304)](cache/client-side/main.go)
|
||||
* [Simple](response-writer/simple/main.go)
|
||||
* [Client-Side (304)](response-writer/client-side/main.go)
|
||||
* Localization and Internationalization
|
||||
* [i18n](i18n/main.go)
|
||||
* Sitemaps
|
||||
* [Sitemap](sitemap/main.go)
|
||||
* Authentication
|
||||
* [Basic Authentication](authentication/basicauth/main.go)
|
||||
* [JWT](miscellaneous/jwt/main.go)
|
||||
* [JWT (community edition)](experimental-handlers/jwt/main.go)
|
||||
* [OAUth2](authentication/oauth2/main.go)
|
||||
* [Manage Permissions](permissions/main.go)
|
||||
* Authentication, Authorization & Bot Detection
|
||||
* [Basic Authentication](auth/basicauth/main.go)
|
||||
* [JWT](auth/jwt/main.go)
|
||||
* [JWT (community edition)](https://github.com/iris-contrib/middleware/tree/v12/jwt/_example/main.go)
|
||||
* [OAUth2](auth/goth/main.go)
|
||||
* [Manage Permissions](auth/permissions/main.go)
|
||||
* [Google reCAPTCHA](auth/recaptcha/main.go)
|
||||
* [hCaptcha](auth/hcaptcha/main.go)
|
||||
* Cookies
|
||||
* [Basic](cookies/basic/main.go)
|
||||
* [Options](cookies/options/main.go)
|
||||
|
@ -160,6 +169,7 @@
|
|||
* [Browser NPM Client (browserify)](websocket/basic/browserify/app.js)
|
||||
* [Native Messages](websocket/native-messages/main.go)
|
||||
* [TLS](websocket/secure/README.md)
|
||||
* [Online Visitors](websocket/online-visitors/main.go)
|
||||
* Dependency Injection
|
||||
* [Overview (Movies Service)](ependency-injection/overview/main.go)
|
||||
* [Basic](dependency-injection/basic/main.go)
|
||||
|
@ -167,9 +177,9 @@
|
|||
* [Sessions](dependency-injection/sessions/main.go)
|
||||
* [Smart Contract](dependency-injection/smart-contract/main.go)
|
||||
* [JWT](dependency-injection/jwt/main.go)
|
||||
* [JWT (iris-contrib)](dependency-injection/jwt/contrib/main.go)
|
||||
* MVC
|
||||
* [Overview - Repository and Service layers](mvc/overview)
|
||||
* [Login - Repository and Service layers](mvc/login)
|
||||
* [Hello world](mvc/hello-world/main.go)
|
||||
* [Basic](mvc/basic/main.go)
|
||||
* [Wildcard](mvc/basic/wildcard/main.go)
|
||||
|
@ -179,27 +189,13 @@
|
|||
* [Authenticated Controller](mvc/authenticated-controller/main.go)
|
||||
* [Websocket Controller](mvc/websocket)
|
||||
* [Register Middleware](mvc/middleware)
|
||||
* Object-Relational Mapping
|
||||
* [Using `go-xorm/xorm` (Mysql, MyMysql, Postgres, Tidb, SQLite, MsSql, MsSql, Oracle)](orm/xorm/main.go)
|
||||
* [Using `jinzhu/gorm`](orm/gorm/main.go)
|
||||
* Project Structure
|
||||
* [Bootstrapper](structuring/bootstrap)
|
||||
* [MVC with Repository and Service layer Overview](structuring/mvc-plus-repository-and-service-layers)
|
||||
* [Login (MVC with Single Responsibility package)](structuring/login-mvc-single-responsibility-package)
|
||||
* [Login (MVC with Datamodels, Datasource, Repository and Service layer)](structuring/login-mvc)
|
||||
* [gRPC](mvc/grpc-compatible)
|
||||
* [Login (Repository and Service layers)](mvc/login)
|
||||
* [Login (Single Responsibility)](mvc/login-mvc-single-responsibility)
|
||||
* [Vue.js Todo App](mvc/vuejs-todo-mvc)
|
||||
* [Bootstrapper](bootstrap)
|
||||
* Desktop Applications
|
||||
* [The blink package](desktop-app/blink)
|
||||
* [The lorca package](desktop-app/lorca)
|
||||
* [The webview package](desktop-app/webview)
|
||||
* Middlewares (Builtin)
|
||||
* [JWT](miscellaneous/jwt/main.go)
|
||||
* [Rate Limit](miscellaneous/ratelimit/main.go)
|
||||
* [HTTP Method Override](https://github.com/kataras/iris/blob/master/middleware/methodoverride/methodoverride_test.go)
|
||||
* [Request Logger](http_request/request-logger/main.go)
|
||||
* [Log Requests to a File](http_request/request-logger/request-logger-file/main.go)
|
||||
* [Recovery](miscellaneous/recover/main.go)
|
||||
* [Profiling (pprof)](miscellaneous/pprof/main.go)
|
||||
* [Internal Application File Logger](miscellaneous/file-logger/main.go)
|
||||
* [Google reCAPTCHA](miscellaneous/recaptcha/main.go)
|
||||
* [hCaptcha](miscellaneous/hcaptcha/main.go)
|
||||
* [The blink package](desktop/blink)
|
||||
* [The lorca package](desktop/lorca)
|
||||
* [The webview package](desktop/webview)
|
||||
* Middlewares [(Community)](https://github.com/iris-contrib/middleware)
|
||||
|
|
3
_examples/apidoc/swagger/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# Swagger 2.0
|
||||
|
||||
Visit https://github.com/iris-contrib/swagger instead.
|
|
@ -19,7 +19,7 @@ func main() {
|
|||
// or defaults to "secret" and "itsa16bytesecret" respectfully.
|
||||
//
|
||||
// Use the `jwt.New` instead for more flexibility, if necessary.
|
||||
j := jwt.DefaultHMAC(15*time.Minute, "secret", "itsa16bytesecret")
|
||||
j := jwt.HMAC(15*time.Minute, "secret", "itsa16bytesecret")
|
||||
|
||||
app := iris.New()
|
||||
app.Logger().SetLevel("debug")
|
||||
|
@ -68,7 +68,7 @@ func main() {
|
|||
|
||||
/*
|
||||
func default_RSA_Example() {
|
||||
j := jwt.DefaultRSA(1 * time.Minute)
|
||||
j := jwt.RSA(15*time.Minute)
|
||||
}
|
||||
|
||||
Same as:
|
||||
|
@ -115,7 +115,7 @@ func hmac_Example() {
|
|||
|
||||
/*
|
||||
func load_From_File_With_Password_Example() {
|
||||
b, err := ioutil.ReadFile("./private_rsa.pem")
|
||||
b, err := ioutil.ReadFile("./rsa_password_protected.key")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
# Authentication
|
||||
|
||||
- [Basic Authentication](basicauth/main.go)
|
||||
- [OAUth2](oauth2/main.go)
|
||||
- [JWT](https://github.com/kataras/iris/tree/master/_examples/miscellaneous/jwt)
|
||||
- [JWT (community edition)](https://github.com/iris-contrib/middleware/blob/master/jwt)
|
||||
- [Sessions](https://github.com/kataras/iris/tree/master/_examples/sessions)
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
@ -1,9 +1,9 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/kataras/iris/v12/_examples/structuring/bootstrap/bootstrap"
|
||||
"github.com/kataras/iris/v12/_examples/structuring/bootstrap/middleware/identity"
|
||||
"github.com/kataras/iris/v12/_examples/structuring/bootstrap/routes"
|
||||
"github.com/kataras/iris/v12/_examples/bootstrap/bootstrap"
|
||||
"github.com/kataras/iris/v12/_examples/bootstrap/middleware/identity"
|
||||
"github.com/kataras/iris/v12/_examples/bootstrap/routes"
|
||||
)
|
||||
|
||||
func newApp() *bootstrap.Bootstrapper {
|
|
@ -14,11 +14,11 @@ func TestApp(t *testing.T) {
|
|||
// test our routes
|
||||
e.GET("/").Expect().Status(httptest.StatusOK)
|
||||
e.GET("/follower/42").Expect().Status(httptest.StatusOK).
|
||||
Body().Equal("from /follower/{id:long} with ID: 42")
|
||||
Body().Equal("from /follower/{id:int64} with ID: 42")
|
||||
e.GET("/following/52").Expect().Status(httptest.StatusOK).
|
||||
Body().Equal("from /following/{id:long} with ID: 52")
|
||||
Body().Equal("from /following/{id:int64} with ID: 52")
|
||||
e.GET("/like/64").Expect().Status(httptest.StatusOK).
|
||||
Body().Equal("from /like/{id:long} with ID: 64")
|
||||
Body().Equal("from /like/{id:int64} with ID: 64")
|
||||
|
||||
// test not found
|
||||
e.GET("/notfound").Expect().Status(httptest.StatusNotFound)
|
|
@ -5,7 +5,7 @@ import (
|
|||
|
||||
"github.com/kataras/iris/v12"
|
||||
|
||||
"github.com/kataras/iris/v12/_examples/structuring/bootstrap/bootstrap"
|
||||
"github.com/kataras/iris/v12/_examples/bootstrap/bootstrap"
|
||||
)
|
||||
|
||||
// New returns a new handler which adds some headers and view data
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
13
_examples/bootstrap/routes/routes.go
Normal file
|
@ -0,0 +1,13 @@
|
|||
package routes
|
||||
|
||||
import (
|
||||
"github.com/kataras/iris/v12/_examples/bootstrap/bootstrap"
|
||||
)
|
||||
|
||||
// Configure registers the necessary routes to the app.
|
||||
func Configure(b *bootstrap.Bootstrapper) {
|
||||
b.Get("/", GetIndexHandler)
|
||||
b.Get("/follower/{id:int64}", GetFollowerHandler)
|
||||
b.Get("/following/{id:int64}", GetFollowingHandler)
|
||||
b.Get("/like/{id:int64}", GetLikeHandler)
|
||||
}
|
|
@ -4,15 +4,15 @@ The `Caddyfile` shows how you can use caddy to listen on ports 80 & 443 and sit
|
|||
|
||||
## Running our two web servers
|
||||
|
||||
1. Go to `$GOPATH/src/github.com/kataras/iris/_examples/tutorial/caddy/server1`
|
||||
1. Go to `$GOPATH/src/github.com/kataras/iris/_examples/caddy/server1`
|
||||
2. Open a terminal window and execute `go run main.go`
|
||||
3. Go to `$GOPATH/src/github.com/kataras/iris/_examples/tutorial/caddy/server2`
|
||||
3. Go to `$GOPATH/src/github.com/kataras/iris/_examples/caddy/server2`
|
||||
4. Open a new terminal window and execute `go run main.go`
|
||||
|
||||
## Caddy installation
|
||||
|
||||
1. Download caddy: https://caddyserver.com/download
|
||||
2. Extract its contents where the `Caddyfile` is located, the `$GOPATH/src/github.com/kataras/iris/_examples/tutorial/caddy` in this case
|
||||
2. Extract its contents where the `Caddyfile` is located, the `$GOPATH/src/github.com/kataras/iris/_examples/caddy` in this case
|
||||
3. Open, read and modify the `Caddyfile` to see by yourself how easy it is to configure the servers
|
||||
4. Run `caddy` directly or open a terminal window and execute `caddy`
|
||||
5. Go to `https://example.com` and `https://api.example.com/user/42`
|
|
@ -35,7 +35,7 @@ func (c *UserController) Get() string {
|
|||
// User is our test User model, nothing tremendous here.
|
||||
type User struct{ ID int64 }
|
||||
|
||||
// GetBy handles GET /user/42, equal to .Get("/user/{id:long}")
|
||||
// GetBy handles GET /user/42, equal to .Get("/user/{id:int64}")
|
||||
func (c *UserController) GetBy(id int64) User {
|
||||
// Select User by ID == $id.
|
||||
return User{id}
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 92 KiB |
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 69 KiB |
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 102 KiB |
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 83 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
17
_examples/database/mongodb/Dockerfile
Normal file
|
@ -0,0 +1,17 @@
|
|||
# docker build -t myapp .
|
||||
# docker run --rm -it -p 8080:8080 myapp:latest
|
||||
FROM golang:latest AS builder
|
||||
RUN apt-get update
|
||||
ENV GO111MODULE=on \
|
||||
CGO_ENABLED=0 \
|
||||
GOOS=linux \
|
||||
GOARCH=amd64
|
||||
WORKDIR /go/src/app
|
||||
COPY go.mod .
|
||||
RUN go mod download
|
||||
COPY . .
|
||||
RUN go install
|
||||
|
||||
FROM scratch
|
||||
COPY --from=builder /go/bin/myapp .
|
||||
ENTRYPOINT ["./myapp"]
|
|
@ -7,11 +7,17 @@ Article is coming soon, follow and stay tuned
|
|||
|
||||
Read [the fully functional example](main.go).
|
||||
|
||||
## Run
|
||||
|
||||
### Docker
|
||||
|
||||
Install [Docker](https://www.docker.com/) and execute the command below
|
||||
|
||||
```sh
|
||||
$ go get -u go.mongodb.org/mongo-driver/...
|
||||
$ go get -u github.com/joho/godotenv
|
||||
$ docker-compose up
|
||||
```
|
||||
|
||||
### Manually
|
||||
|
||||
```sh
|
||||
# .env file contents
|
||||
|
@ -22,7 +28,7 @@ DSN=mongodb://localhost:27017
|
|||
```sh
|
||||
$ go run main.go
|
||||
> 2019/01/28 05:17:59 Loading environment variables from file: .env
|
||||
> 2019/01/28 05:17:59 ◽ PORT=8080
|
||||
> 2019/01/28 05:17:59 ◽ Port=8080
|
||||
> 2019/01/28 05:17:59 ◽ DSN=mongodb://localhost:27017
|
||||
> Now listening on: http://localhost:8080
|
||||
```
|
|
@ -1,8 +1,8 @@
|
|||
package storeapi
|
||||
|
||||
import (
|
||||
"github.com/kataras/iris/v12/_examples/tutorial/mongodb/httputil"
|
||||
"github.com/kataras/iris/v12/_examples/tutorial/mongodb/store"
|
||||
"myapp/httputil"
|
||||
"myapp/store"
|
||||
|
||||
"github.com/kataras/iris/v12"
|
||||
)
|
18
_examples/database/mongodb/docker-compose.yml
Normal file
|
@ -0,0 +1,18 @@
|
|||
version: "3.1"
|
||||
|
||||
services:
|
||||
app:
|
||||
build: .
|
||||
environment:
|
||||
Port: 8080
|
||||
DSN: db:27017
|
||||
ports:
|
||||
- 8080:8080
|
||||
depends_on:
|
||||
- db
|
||||
db:
|
||||
image: mongo
|
||||
environment:
|
||||
MONGO_INITDB_DATABASE: store
|
||||
ports:
|
||||
- 27017:27017
|
|
@ -22,6 +22,9 @@ var (
|
|||
func parse() {
|
||||
Port = getDefault("PORT", "8080")
|
||||
DSN = getDefault("DSN", "mongodb://localhost:27017")
|
||||
|
||||
log.Printf("• Port=%s\n", Port)
|
||||
log.Printf("• DSN=%s\n", DSN)
|
||||
}
|
||||
|
||||
// Load loads environment variables that are being used across the whole app.
|
||||
|
@ -34,28 +37,31 @@ func parse() {
|
|||
// After `Load` the callers can get an environment variable via `os.Getenv`.
|
||||
func Load(envFileName string) {
|
||||
if args := os.Args; len(args) > 1 && args[1] == "help" {
|
||||
fmt.Fprintln(os.Stderr, "https://github.com/kataras/iris/blob/master/_examples/tutorials/mongodb/README.md")
|
||||
fmt.Fprintln(os.Stderr, "https://github.com/kataras/iris/blob/master/_examples/database/mongodb/README.md")
|
||||
os.Exit(-1)
|
||||
}
|
||||
|
||||
log.Printf("Loading environment variables from file: %s\n", envFileName)
|
||||
// If more than one filename passed with comma separated then load from all
|
||||
// of these, a env file can be a partial too.
|
||||
envFiles := strings.Split(envFileName, ",")
|
||||
for i := range envFiles {
|
||||
if filepath.Ext(envFiles[i]) == "" {
|
||||
envFiles[i] += ".env"
|
||||
for _, envFile := range envFiles {
|
||||
if filepath.Ext(envFile) == "" {
|
||||
envFile += ".env"
|
||||
}
|
||||
|
||||
if fileExists(envFile) {
|
||||
log.Printf("Loading environment variables from file: %s\n", envFile)
|
||||
|
||||
if err := godotenv.Load(envFile); err != nil {
|
||||
panic(fmt.Sprintf("error loading environment variables from [%s]: %v", envFile, err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := godotenv.Load(envFiles...); err != nil {
|
||||
panic(fmt.Sprintf("error loading environment variables from [%s]: %v", envFileName, err))
|
||||
}
|
||||
|
||||
envMap, _ := godotenv.Read(envFiles...)
|
||||
for k, v := range envMap {
|
||||
log.Printf("◽ %s=%s\n", k, v)
|
||||
}
|
||||
// envMap, _ := godotenv.Read(envFiles...)
|
||||
// for k, v := range envMap {
|
||||
// log.Printf("◽ %s=%s\n", k, v)
|
||||
// }
|
||||
|
||||
parse()
|
||||
}
|
||||
|
@ -69,3 +75,11 @@ func getDefault(key string, def string) string {
|
|||
|
||||
return value
|
||||
}
|
||||
|
||||
func fileExists(filename string) bool {
|
||||
info, err := os.Stat(filename)
|
||||
if os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
return !info.IsDir()
|
||||
}
|
9
_examples/database/mongodb/go.mod
Normal file
|
@ -0,0 +1,9 @@
|
|||
module myapp
|
||||
|
||||
go 1.14
|
||||
|
||||
require (
|
||||
github.com/joho/godotenv v1.3.0
|
||||
github.com/kataras/iris/v12 v12.2.0
|
||||
go.mongodb.org/mongo-driver v1.3.4
|
||||
)
|
|
@ -11,11 +11,11 @@ import (
|
|||
"os"
|
||||
|
||||
// APIs
|
||||
storeapi "github.com/kataras/iris/v12/_examples/tutorial/mongodb/api/store"
|
||||
storeapi "myapp/api/store"
|
||||
|
||||
//
|
||||
"github.com/kataras/iris/v12/_examples/tutorial/mongodb/env"
|
||||
"github.com/kataras/iris/v12/_examples/tutorial/mongodb/store"
|
||||
"myapp/env"
|
||||
"myapp/store"
|
||||
|
||||
"github.com/kataras/iris/v12"
|
||||
|
||||
|
@ -30,7 +30,7 @@ func init() {
|
|||
|
||||
flagset := flag.CommandLine
|
||||
flagset.StringVar(&envFileName, "env", envFileName, "the env file which web app will use to extract its environment variables")
|
||||
flag.CommandLine.Parse(os.Args[1:])
|
||||
flagset.Parse(os.Args[1:])
|
||||
|
||||
env.Load(envFileName)
|
||||
}
|
17
_examples/database/mysql/Dockerfile
Normal file
|
@ -0,0 +1,17 @@
|
|||
# docker build -t myapp .
|
||||
# docker run --rm -it -p 8080:8080 myapp:latest
|
||||
FROM golang:latest AS builder
|
||||
RUN apt-get update
|
||||
ENV GO111MODULE=on \
|
||||
CGO_ENABLED=0 \
|
||||
GOOS=linux \
|
||||
GOARCH=amd64
|
||||
WORKDIR /go/src/app
|
||||
COPY go.mod .
|
||||
RUN go mod download
|
||||
COPY . .
|
||||
RUN go install
|
||||
|
||||
FROM scratch
|
||||
COPY --from=builder /go/bin/myapp .
|
||||
ENTRYPOINT ["./myapp"]
|
146
_examples/database/mysql/README.md
Normal file
|
@ -0,0 +1,146 @@
|
|||
# Iris, MySQL, Groupcache & Docker Example
|
||||
|
||||
## 📘 Endpoints
|
||||
|
||||
| Method | Path | Description | URL Parameters | Body | Auth Required |
|
||||
|--------|---------------------|------------------------|--------------- |----------------------------|---------------|
|
||||
| ANY | /token | Prints a new JWT Token | - | - | - |
|
||||
| GET | /category | Lists a set of Categories | offset, limit, order | - | - |
|
||||
| POST | /category | Creates a Category | - | JSON [Full Category](migration/api_category/create_category.json) | Token |
|
||||
| PUT | /category | Fully-Updates a Category | - | JSON [Full Category](migration/api_category/update_category.json) | Token |
|
||||
| PATCH | /category/{id} | Partially-Updates a Category | - | JSON [Partial Category](migration/api_category/update_partial_category.json) | Token |
|
||||
| GET | /category/{id} | Prints a Category | - | - | - |
|
||||
| DELETE | /category/{id} | Deletes a Category | - | - | Token |
|
||||
| GET | /category/{id}/products | Lists all Products from a Category | offset, limit, order | - | - |
|
||||
| POST | /category/{id}/products | (Batch) Assigns one or more Products to a Category | - | JSON [Products](migration/api_category/insert_products_category.json) | Token |
|
||||
| GET | /product | Lists a set of Products (cache) | offset, limit, order | - | - |
|
||||
| POST | /product | Creates a Product | - | JSON [Full Product](migration/api_product/create_product.json) | Token |
|
||||
| PUT | /product | Fully-Updates a Product | - | JSON [Full Product](migration/api_product/update_product.json) | Token |
|
||||
| PATCH | /product/{id} | Partially-Updates a Product | - | JSON [Partial Product](migration/api_product/update_partial_product.json) | Token |
|
||||
| GET | /product/{id} | Prints a Product (cache) | - | - | - |
|
||||
| DELETE | /product/{id} | Deletes a Product | - | - | Token |
|
||||
|
||||
|
||||
|
||||
## 📑 Responses
|
||||
|
||||
* **Content-Type** of `"application/json;charset=utf-8"`, snake_case naming (identical to the database columns)
|
||||
* **Status Codes**
|
||||
* 500 for server(db) errors,
|
||||
* 422 for validation errors, e.g.
|
||||
```json
|
||||
{
|
||||
"code": 422,
|
||||
"message": "required fields are missing",
|
||||
"timestamp": 1589306271
|
||||
}
|
||||
```
|
||||
* 400 for malformed syntax, e.g.
|
||||
```json
|
||||
{
|
||||
"code": 400,
|
||||
"message": "json: cannot unmarshal number -2 into Go struct field Category.position of type uint64",
|
||||
"timestamp": 1589306325
|
||||
}
|
||||
```
|
||||
```json
|
||||
{
|
||||
"code": 400,
|
||||
"message": "json: unknown field \"field_not_exists\"",
|
||||
"timestamp": 1589306367
|
||||
}
|
||||
```
|
||||
* 404 for entity not found, e.g.
|
||||
```json
|
||||
{
|
||||
"code": 404,
|
||||
"message": "entity does not exist",
|
||||
"timestamp": 1589306199
|
||||
}
|
||||
```
|
||||
* 304 for unaffected UPDATE or DELETE,
|
||||
* 201 for CREATE with the last inserted ID,
|
||||
* 200 for GET, UPDATE and DELETE
|
||||
|
||||
## ⚡ Get Started
|
||||
|
||||
Download the folder.
|
||||
|
||||
### Install (Docker)
|
||||
|
||||
Install [Docker](https://www.docker.com/) and execute the command below
|
||||
|
||||
```sh
|
||||
$ docker-compose up
|
||||
```
|
||||
|
||||
### Install (Manually)
|
||||
|
||||
Run `go build` or `go run main.go` and read below.
|
||||
|
||||
#### MySQL
|
||||
|
||||
Environment variables:
|
||||
|
||||
```sh
|
||||
MYSQL_USER=user_myapp
|
||||
MYSQL_PASSWORD=dbpassword
|
||||
MYSQL_HOST=localhost
|
||||
MYSQL_DATABASE=myapp
|
||||
```
|
||||
|
||||
Download the schema from [migration/myapp.sql](migration/myapp.sql) and execute it against your MySQL server instance.
|
||||
|
||||
```sql
|
||||
CREATE DATABASE IF NOT EXISTS myapp DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
USE myapp;
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
SET FOREIGN_KEY_CHECKS = 0;
|
||||
|
||||
DROP TABLE IF EXISTS categories;
|
||||
CREATE TABLE categories (
|
||||
id int(11) NOT NULL AUTO_INCREMENT,
|
||||
title varchar(255) NOT NULL,
|
||||
position int(11) NOT NULL,
|
||||
image_url varchar(255) NOT NULL,
|
||||
created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
DROP TABLE IF EXISTS products;
|
||||
CREATE TABLE products (
|
||||
id int(11) NOT NULL AUTO_INCREMENT,
|
||||
category_id int,
|
||||
title varchar(255) NOT NULL,
|
||||
image_url varchar(255) NOT NULL,
|
||||
price decimal(10,2) NOT NULL,
|
||||
description text NOT NULL,
|
||||
created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
FOREIGN KEY (category_id) REFERENCES categories(id)
|
||||
);
|
||||
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
```
|
||||
|
||||
### Requests
|
||||
|
||||
Some request bodies can be found at: [migration/api_category](migration/api_category) and [migration/api_product](migration/api_product). **However** I've provided a [postman.json](migration/myapp_postman.json) Collection that you can import to your [POSTMAN](https://learning.postman.com/docs/postman/collections/importing-and-exporting-data/#collections) and start playing with the API.
|
||||
|
||||
All write-access endpoints are "protected" via JWT, a client should "verify" itself. You'll need to manually take the **token** from the `http://localhost:8080/token` and put it on url parameter `?token=$token` or to the `Authentication: Bearer $token` request header.
|
||||
|
||||
### Unit or End-To-End Testing?
|
||||
|
||||
Testing is important. The code is written in a way that testing should be trivial (Pseudo/memory Database or SQLite local file could be integrated as well, for end-to-end tests a Docker image with MySQL and fire tests against that server). However, there is [nothing(?)](service/category_service_test.go) to see here.
|
||||
|
||||
## Packages
|
||||
|
||||
- https://github.com/dgrijalva/jwt-go (JWT parsing)
|
||||
- https://github.com/go-sql-driver/mysql (Go Driver for MySQL)
|
||||
- https://github.com/DATA-DOG/go-sqlmock (Testing DB see [service/category_service_test.go](service/category_service_test.go))
|
||||
- https://github.com/kataras/iris (HTTP)
|
||||
- https://github.com/mailgun/groupcache (Caching)
|
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
|
@ -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
|
@ -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
|
@ -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 == "")
|
||||
}
|
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)
|
||||
}
|
120
_examples/database/mysql/cache/groupcache.go
vendored
Normal file
|
@ -0,0 +1,120 @@
|
|||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"myapp/entity"
|
||||
"myapp/sql"
|
||||
|
||||
"github.com/mailgun/groupcache/v2"
|
||||
)
|
||||
|
||||
// Service that cache will use to retrieve data.
|
||||
type Service interface {
|
||||
RecordInfo() sql.Record
|
||||
GetByID(ctx context.Context, dest interface{}, id int64) error
|
||||
List(ctx context.Context, dest interface{}, opts sql.ListOptions) error
|
||||
}
|
||||
|
||||
// Cache is a simple structure which holds the groupcache and the database service, exposes
|
||||
// `GetByID` and `List` which returns cached (or stores new) items.
|
||||
type Cache struct {
|
||||
service Service
|
||||
maxAge time.Duration
|
||||
group *groupcache.Group
|
||||
}
|
||||
|
||||
// Size default size to use on groupcache, defaults to 3MB.
|
||||
var Size int64 = 3 << (10 * 3)
|
||||
|
||||
// New returns a new cache service which exposes `GetByID` and `List` methods to work with.
|
||||
// The "name" should be unique, "maxAge" for cache expiration.
|
||||
func New(service Service, name string, maxAge time.Duration) *Cache {
|
||||
c := new(Cache)
|
||||
c.service = service
|
||||
c.maxAge = maxAge
|
||||
c.group = groupcache.NewGroup(name, Size, c)
|
||||
return c
|
||||
}
|
||||
|
||||
const (
|
||||
prefixID = "#"
|
||||
prefixList = "["
|
||||
)
|
||||
|
||||
// Get implements the groupcache.Getter interface.
|
||||
// Use `GetByID` and `List` instead.
|
||||
func (c *Cache) Get(ctx context.Context, key string, dest groupcache.Sink) error {
|
||||
if len(key) < 2 { // empty or missing prefix+key, should never happen.
|
||||
return sql.ErrUnprocessable
|
||||
}
|
||||
|
||||
var v interface{}
|
||||
|
||||
prefix := key[0:1]
|
||||
key = key[1:]
|
||||
switch prefix {
|
||||
case prefixID:
|
||||
// Get by ID.
|
||||
id, err := strconv.ParseInt(key, 10, 64)
|
||||
if err != nil || id <= 0 {
|
||||
return err
|
||||
}
|
||||
|
||||
switch c.service.RecordInfo().(type) {
|
||||
case *entity.Category:
|
||||
v = new(entity.Category)
|
||||
case *entity.Product:
|
||||
v = new(entity.Product)
|
||||
}
|
||||
|
||||
err = c.service.GetByID(ctx, v, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case prefixList:
|
||||
// Get a set of records, list.
|
||||
q, err := url.ParseQuery(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opts := sql.ParseListOptions(q)
|
||||
|
||||
switch c.service.RecordInfo().(type) {
|
||||
case *entity.Category:
|
||||
v = new(entity.Categories)
|
||||
case *entity.Product:
|
||||
v = new(entity.Products)
|
||||
}
|
||||
|
||||
err = c.service.List(ctx, v, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
default:
|
||||
return sql.ErrUnprocessable
|
||||
}
|
||||
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return dest.SetBytes(b, time.Now().Add(c.maxAge))
|
||||
}
|
||||
|
||||
// GetByID binds an item to "dest" an item based on its "id".
|
||||
func (c *Cache) GetByID(ctx context.Context, id string, dest *[]byte) error {
|
||||
return c.group.Get(ctx, prefixID+id, groupcache.AllocatingByteSliceSink(dest))
|
||||
}
|
||||
|
||||
// List binds item to "dest" based on the "rawQuery" of `url.Values` for `ListOptions`.
|
||||
func (c *Cache) List(ctx context.Context, rawQuery string, dest *[]byte) error {
|
||||
return c.group.Get(ctx, prefixList+rawQuery, groupcache.AllocatingByteSliceSink(dest))
|
||||
}
|
32
_examples/database/mysql/docker-compose.yml
Normal file
|
@ -0,0 +1,32 @@
|
|||
version: '3.1'
|
||||
|
||||
services:
|
||||
db:
|
||||
image: mysql
|
||||
command: --default-authentication-plugin=mysql_native_password
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: dbpassword
|
||||
MYSQL_DATABASE: myapp
|
||||
MYSQL_USER: user_myapp
|
||||
MYSQL_PASSWORD: dbpassword
|
||||
tty: true
|
||||
volumes:
|
||||
- ./migration:/docker-entrypoint-initdb.d
|
||||
app:
|
||||
build: .
|
||||
ports:
|
||||
- 8080:8080
|
||||
environment:
|
||||
PORT: 8080
|
||||
MYSQL_USER: user_myapp
|
||||
MYSQL_PASSWORD: dbpassword
|
||||
MYSQL_DATABASE: myapp
|
||||
MYSQL_HOST: db
|
||||
restart: on-failure
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "tcp://db:3306"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
depends_on:
|
||||
- db
|
89
_examples/database/mysql/entity/category.go
Normal file
|
@ -0,0 +1,89 @@
|
|||
package entity
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Category represents the categories entity.
|
||||
// Each product belongs to a category, see `Product.CategoryID` field.
|
||||
// It implements the `sql.Record` and `sql.Sorted` interfaces.
|
||||
type Category struct {
|
||||
ID int64 `db:"id" json:"id"`
|
||||
Title string `db:"title" json:"title"`
|
||||
Position uint64 `db:"position" json:"position"`
|
||||
ImageURL string `db:"image_url" json:"image_url"`
|
||||
|
||||
// We could use: sql.NullTime or unix time seconds (as int64),
|
||||
// note that the dsn parameter "parseTime=true" is required now in order to fill this field correctly.
|
||||
CreatedAt *time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt *time.Time `db:"updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
// TableName returns the database table name of a Category.
|
||||
func (c *Category) TableName() string {
|
||||
return "categories"
|
||||
}
|
||||
|
||||
// PrimaryKey returns the primary key of a Category.
|
||||
func (c *Category) PrimaryKey() string {
|
||||
return "id"
|
||||
}
|
||||
|
||||
// SortBy returns the column name that
|
||||
// should be used as a fallback for sorting a set of Category.
|
||||
func (c *Category) SortBy() string {
|
||||
return "position"
|
||||
}
|
||||
|
||||
// Scan binds mysql rows to this Category.
|
||||
func (c *Category) Scan(rows *sql.Rows) error {
|
||||
c.CreatedAt = new(time.Time)
|
||||
c.UpdatedAt = new(time.Time)
|
||||
return rows.Scan(&c.ID, &c.Title, &c.Position, &c.ImageURL, &c.CreatedAt, &c.UpdatedAt)
|
||||
}
|
||||
|
||||
// Categories a list of categories. Implements the `Scannable` interface.
|
||||
type Categories []*Category
|
||||
|
||||
// Scan binds mysql rows to this Categories.
|
||||
func (cs *Categories) Scan(rows *sql.Rows) (err error) {
|
||||
cp := *cs
|
||||
for rows.Next() {
|
||||
c := new(Category)
|
||||
if err = c.Scan(rows); err != nil {
|
||||
return
|
||||
}
|
||||
cp = append(cp, c)
|
||||
}
|
||||
|
||||
if len(cp) == 0 {
|
||||
return sql.ErrNoRows
|
||||
}
|
||||
|
||||
*cs = cp
|
||||
|
||||
return rows.Err()
|
||||
}
|
||||
|
||||
/*
|
||||
// The requests.
|
||||
type (
|
||||
CreateCategoryRequest struct {
|
||||
Title string `json:"title"` // all required.
|
||||
Position uint64 `json:"position"`
|
||||
ImageURL string `json:"imageURL"`
|
||||
}
|
||||
|
||||
UpdateCategoryRequest CreateCategoryRequest // at least 1 required.
|
||||
|
||||
GetCategoryRequest struct {
|
||||
ID int64 `json:"id"` // required.
|
||||
}
|
||||
|
||||
DeleteCategoryRequest GetCategoryRequest
|
||||
|
||||
GetCategoriesRequest struct {
|
||||
// [limit, offset...]
|
||||
}
|
||||
)*/
|
95
_examples/database/mysql/entity/product.go
Normal file
|
@ -0,0 +1,95 @@
|
|||
package entity
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Product represents the products entity.
|
||||
// It implements the `sql.Record` and `sql.Sorted` interfaces.
|
||||
type Product struct {
|
||||
ID int64 `db:"id" json:"id"`
|
||||
CategoryID int64 `db:"category_id" json:"category_id"`
|
||||
Title string `db:"title" json:"title"`
|
||||
ImageURL string `db:"image_url" json:"image_url"`
|
||||
Price float32 `db:"price" json:"price"`
|
||||
Description string `db:"description" json:"description"`
|
||||
CreatedAt *time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt *time.Time `db:"updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
// TableName returns the database table name of a Product.
|
||||
func (p Product) TableName() string {
|
||||
return "products"
|
||||
}
|
||||
|
||||
// PrimaryKey returns the primary key of a Product.
|
||||
func (p *Product) PrimaryKey() string {
|
||||
return "id"
|
||||
}
|
||||
|
||||
// SortBy returns the column name that
|
||||
// should be used as a fallback for sorting a set of Product.
|
||||
func (p *Product) SortBy() string {
|
||||
return "updated_at"
|
||||
}
|
||||
|
||||
// ValidateInsert simple check for empty fields that should be required.
|
||||
func (p *Product) ValidateInsert() bool {
|
||||
return p.CategoryID > 0 && p.Title != "" && p.ImageURL != "" && p.Price > 0 /* decimal* */ && p.Description != ""
|
||||
}
|
||||
|
||||
// Scan binds mysql rows to this Product.
|
||||
func (p *Product) Scan(rows *sql.Rows) error {
|
||||
p.CreatedAt = new(time.Time)
|
||||
p.UpdatedAt = new(time.Time)
|
||||
return rows.Scan(&p.ID, &p.CategoryID, &p.Title, &p.ImageURL, &p.Price, &p.Description, &p.CreatedAt, &p.UpdatedAt)
|
||||
}
|
||||
|
||||
// Products is a list of products. Implements the `Scannable` interface.
|
||||
type Products []*Product
|
||||
|
||||
// Scan binds mysql rows to this Categories.
|
||||
func (ps *Products) Scan(rows *sql.Rows) (err error) {
|
||||
cp := *ps
|
||||
for rows.Next() {
|
||||
p := new(Product)
|
||||
if err = p.Scan(rows); err != nil {
|
||||
return
|
||||
}
|
||||
cp = append(cp, p)
|
||||
}
|
||||
|
||||
if len(cp) == 0 {
|
||||
return sql.ErrNoRows
|
||||
}
|
||||
|
||||
*ps = cp
|
||||
|
||||
return rows.Err()
|
||||
}
|
||||
|
||||
/*
|
||||
// The requests.
|
||||
type (
|
||||
CreateProductRequest struct { // all required.
|
||||
CategoryID int64 `json:"categoryID"`
|
||||
Title string `json:"title"`
|
||||
ImageURL string `json:"imageURL"`
|
||||
Price float32 `json:"price"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
UpdateProductRequest CreateProductRequest // at least 1 required.
|
||||
|
||||
GetProductRequest struct {
|
||||
ID int64 `json:"id"` // required.
|
||||
}
|
||||
|
||||
DeleteProductRequest GetProductRequest
|
||||
|
||||
GetProductsRequest struct {
|
||||
// [page, offset...]
|
||||
}
|
||||
)
|
||||
*/
|
9
_examples/database/mysql/go.mod
Normal file
|
@ -0,0 +1,9 @@
|
|||
module myapp
|
||||
|
||||
go 1.14
|
||||
|
||||
require (
|
||||
github.com/go-sql-driver/mysql v1.5.0
|
||||
github.com/kataras/iris/v12 v12.2.0
|
||||
github.com/mailgun/groupcache/v2 v2.1.0
|
||||
)
|
44
_examples/database/mysql/main.go
Normal file
|
@ -0,0 +1,44 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"myapp/api"
|
||||
"myapp/sql"
|
||||
|
||||
"github.com/kataras/iris/v12"
|
||||
)
|
||||
|
||||
func main() {
|
||||
dsn := fmt.Sprintf("%s:%s@tcp(%s:3306)/%s?parseTime=true&charset=utf8mb4&collation=utf8mb4_unicode_ci",
|
||||
getenv("MYSQL_USER", "user_myapp"),
|
||||
getenv("MYSQL_PASSWORD", "dbpassword"),
|
||||
getenv("MYSQL_HOST", "localhost"),
|
||||
getenv("MYSQL_DATABASE", "myapp"),
|
||||
)
|
||||
|
||||
db, err := sql.ConnectMySQL(dsn)
|
||||
if err != nil {
|
||||
log.Fatalf("error connecting to the MySQL database: %v", err)
|
||||
}
|
||||
|
||||
secret := getenv("JWT_SECRET", "EbnJO3bwmX")
|
||||
|
||||
app := iris.New()
|
||||
subRouter := api.Router(db, secret)
|
||||
app.PartyFunc("/", subRouter)
|
||||
|
||||
addr := fmt.Sprintf(":%s", getenv("PORT", "8080"))
|
||||
app.Listen(addr)
|
||||
}
|
||||
|
||||
func getenv(key string, def string) string {
|
||||
v := os.Getenv(key)
|
||||
if v == "" {
|
||||
return def
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"title": "computer-internet",
|
||||
"position": 2,
|
||||
"image_url": "https://bp.pstatic.gr/public/dist/images/1mOPxYtw1k.webp"
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
[{
|
||||
"title": "product-1",
|
||||
"image_url": "https://images.product1.png",
|
||||
"price": 42.42,
|
||||
"description": "a description for product-1"
|
||||
}, {
|
||||
"title": "product-2",
|
||||
"image_url": "https://images.product2.png",
|
||||
"price": 32.1,
|
||||
"description": "a description for product-2"
|
||||
}, {
|
||||
"title": "product-3",
|
||||
"image_url": "https://images.product3.png",
|
||||
"price": 52321321.32,
|
||||
"description": "a description for product-3"
|
||||
}, {
|
||||
"title": "product-4",
|
||||
"image_url": "https://images.product4.png",
|
||||
"price": 77.4221,
|
||||
"description": "a description for product-4"
|
||||
}, {
|
||||
"title": "product-5",
|
||||
"image_url": "https://images.product5.png",
|
||||
"price": 55.1,
|
||||
"description": "a description for product-5"
|
||||
}, {
|
||||
"title": "product-6",
|
||||
"image_url": "https://images.product6.png",
|
||||
"price": 53.32,
|
||||
"description": "a description for product-6"
|
||||
}]
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"id": 2,
|
||||
"position": 1,
|
||||
"title": "computers",
|
||||
"image_url": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/d7/Desktop_computer_clipart_-_Yellow_theme.svg/1200px-Desktop_computer_clipart_-_Yellow_theme.svg.png"
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"title": "computers-technology"
|
||||
}
|
484
_examples/database/mysql/migration/api_postman.json
Normal file
|
@ -0,0 +1,484 @@
|
|||
{
|
||||
"info": {
|
||||
"_postman_id": "d3a2fdf6-9ebd-4e85-827d-385592a71fd6",
|
||||
"name": "myapp (api-test)",
|
||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
||||
},
|
||||
"item": [
|
||||
{
|
||||
"name": "Category",
|
||||
"item": [
|
||||
{
|
||||
"name": "Create",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1ODk2MzkzNjd9.cYohwgUpe-Z7ac0LPpz4Adi5QXJmtwD1ZRpXrMUMPN0",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\r\n \"title\": \"computer-internet\",\r\n \"position\": 1,\r\n \"image_url\": \"https://bp.pstatic.gr/public/dist/images/1mOPxYtw1k.webp\"\r\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"raw": "http://localhost:8080/category",
|
||||
"protocol": "http",
|
||||
"host": [
|
||||
"localhost"
|
||||
],
|
||||
"port": "8080",
|
||||
"path": [
|
||||
"category"
|
||||
]
|
||||
},
|
||||
"description": "Create a Category"
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Get By ID",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "http://localhost:8080/category/1",
|
||||
"protocol": "http",
|
||||
"host": [
|
||||
"localhost"
|
||||
],
|
||||
"port": "8080",
|
||||
"path": [
|
||||
"category",
|
||||
"1"
|
||||
]
|
||||
},
|
||||
"description": "Get By ID"
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "List",
|
||||
"protocolProfileBehavior": {
|
||||
"disableBodyPruning": true
|
||||
},
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": ""
|
||||
},
|
||||
"url": {
|
||||
"raw": "http://localhost:8080/category?offset=0&limit=30&order=asc",
|
||||
"protocol": "http",
|
||||
"host": [
|
||||
"localhost"
|
||||
],
|
||||
"port": "8080",
|
||||
"path": [
|
||||
"category"
|
||||
],
|
||||
"query": [
|
||||
{
|
||||
"key": "offset",
|
||||
"value": "0"
|
||||
},
|
||||
{
|
||||
"key": "limit",
|
||||
"value": "30"
|
||||
},
|
||||
{
|
||||
"key": "order",
|
||||
"value": "asc"
|
||||
}
|
||||
]
|
||||
},
|
||||
"description": "Get many with limit offset"
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Update (Full)",
|
||||
"request": {
|
||||
"method": "PUT",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1ODk1ODU1NjN9.PtfDS1niGoZ7pV6kplI-_q1fVKLnknQ3IwcrLZhoVCU",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\r\n\t\"id\": 1,\r\n\t\"position\": 3,\r\n \"title\": \"computers\",\r\n \"image_url\":\"https://upload.wikimedia.org/wikipedia/commons/thumb/d/d7/Desktop_computer_clipart_-_Yellow_theme.svg/1200px-Desktop_computer_clipart_-_Yellow_theme.svg.png\"\r\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "http://localhost:8080/category",
|
||||
"protocol": "http",
|
||||
"host": [
|
||||
"localhost"
|
||||
],
|
||||
"port": "8080",
|
||||
"path": [
|
||||
"category"
|
||||
],
|
||||
"query": [
|
||||
{
|
||||
"key": "",
|
||||
"value": null,
|
||||
"disabled": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"description": "Update a Category (full update)"
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Delete By ID",
|
||||
"request": {
|
||||
"method": "DELETE",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1ODk1ODU1NjN9.PtfDS1niGoZ7pV6kplI-_q1fVKLnknQ3IwcrLZhoVCU",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "http://localhost:8080/category/1",
|
||||
"protocol": "http",
|
||||
"host": [
|
||||
"localhost"
|
||||
],
|
||||
"port": "8080",
|
||||
"path": [
|
||||
"category",
|
||||
"1"
|
||||
]
|
||||
},
|
||||
"description": "Delete a Category"
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Update (Partial)",
|
||||
"request": {
|
||||
"method": "PATCH",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1ODk1ODU1NjN9.PtfDS1niGoZ7pV6kplI-_q1fVKLnknQ3IwcrLZhoVCU",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\r\n \"title\": \"computers-technology\"\r\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "http://localhost:8080/category/1",
|
||||
"protocol": "http",
|
||||
"host": [
|
||||
"localhost"
|
||||
],
|
||||
"port": "8080",
|
||||
"path": [
|
||||
"category",
|
||||
"3"
|
||||
]
|
||||
},
|
||||
"description": "Update a Category partially, e.g. title only"
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "List Products",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "http://localhost:8080/category/1/products?offset=0&limit=30&by=price&order=asc",
|
||||
"protocol": "http",
|
||||
"host": [
|
||||
"localhost"
|
||||
],
|
||||
"port": "8080",
|
||||
"path": [
|
||||
"category",
|
||||
"3",
|
||||
"products"
|
||||
],
|
||||
"query": [
|
||||
{
|
||||
"key": "offset",
|
||||
"value": "0"
|
||||
},
|
||||
{
|
||||
"key": "limit",
|
||||
"value": "30"
|
||||
},
|
||||
{
|
||||
"key": "by",
|
||||
"value": "price"
|
||||
},
|
||||
{
|
||||
"key": "order",
|
||||
"value": "asc"
|
||||
}
|
||||
]
|
||||
},
|
||||
"description": "Get products from cheap to expensive"
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Insert Products",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1ODk1ODU1NjN9.PtfDS1niGoZ7pV6kplI-_q1fVKLnknQ3IwcrLZhoVCU",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "[{\r\n \"title\": \"product-1\",\r\n \"image_url\": \"https://images.product1.png\",\r\n \"price\": 42.42,\r\n \"description\": \"a description for product-1\"\r\n}, {\r\n \"title\": \"product-2\",\r\n \"image_url\": \"https://images.product2.png\",\r\n \"price\": 32.1,\r\n \"description\": \"a description for product-2\"\r\n}, {\r\n \"title\": \"product-3\",\r\n \"image_url\": \"https://images.product3.png\",\r\n \"price\": 52321321.32,\r\n \"description\": \"a description for product-3\"\r\n}, {\r\n \"title\": \"product-4\",\r\n \"image_url\": \"https://images.product4.png\",\r\n \"price\": 77.4221,\r\n \"description\": \"a description for product-4\"\r\n}, {\r\n \"title\": \"product-5\",\r\n \"image_url\": \"https://images.product5.png\",\r\n \"price\": 55.1,\r\n \"description\": \"a description for product-5\"\r\n}, {\r\n \"title\": \"product-6\",\r\n \"image_url\": \"https://images.product6.png\",\r\n \"price\": 53.32,\r\n \"description\": \"a description for product-6\"\r\n}]"
|
||||
},
|
||||
"url": {
|
||||
"raw": "http://localhost:8080/category/1/products",
|
||||
"protocol": "http",
|
||||
"host": [
|
||||
"localhost"
|
||||
],
|
||||
"port": "8080",
|
||||
"path": [
|
||||
"category",
|
||||
"3",
|
||||
"products"
|
||||
]
|
||||
},
|
||||
"description": "Batch Insert Products to a Category"
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
],
|
||||
"protocolProfileBehavior": {}
|
||||
},
|
||||
{
|
||||
"name": "Product",
|
||||
"item": [
|
||||
{
|
||||
"name": "List",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "http://localhost:8080/product?offset=0&limit=30&by=price&order=asc",
|
||||
"protocol": "http",
|
||||
"host": [
|
||||
"localhost"
|
||||
],
|
||||
"port": "8080",
|
||||
"path": [
|
||||
"product"
|
||||
],
|
||||
"query": [
|
||||
{
|
||||
"key": "offset",
|
||||
"value": "0"
|
||||
},
|
||||
{
|
||||
"key": "limit",
|
||||
"value": "30"
|
||||
},
|
||||
{
|
||||
"key": "by",
|
||||
"value": "price"
|
||||
},
|
||||
{
|
||||
"key": "order",
|
||||
"value": "asc"
|
||||
}
|
||||
]
|
||||
},
|
||||
"description": "List products"
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Get By ID",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "http://localhost:8080/product/1",
|
||||
"protocol": "http",
|
||||
"host": [
|
||||
"localhost"
|
||||
],
|
||||
"port": "8080",
|
||||
"path": [
|
||||
"product",
|
||||
"1"
|
||||
]
|
||||
},
|
||||
"description": "Get a Product"
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Delete By ID",
|
||||
"request": {
|
||||
"method": "DELETE",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1ODk1ODU1NjN9.PtfDS1niGoZ7pV6kplI-_q1fVKLnknQ3IwcrLZhoVCU",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "http://localhost:8080/product/3",
|
||||
"protocol": "http",
|
||||
"host": [
|
||||
"localhost"
|
||||
],
|
||||
"port": "8080",
|
||||
"path": [
|
||||
"product",
|
||||
"3"
|
||||
]
|
||||
},
|
||||
"description": "Delete a Product"
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Create",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1ODk1ODU1NjN9.PtfDS1niGoZ7pV6kplI-_q1fVKLnknQ3IwcrLZhoVCU",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\r\n \"title\": \"product-1\",\r\n \"category_id\": 1,\r\n \"image_url\": \"https://images.product1.png\",\r\n \"price\": 42.42,\r\n \"description\": \"a description for product-1\"\r\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "http://localhost:8080/product",
|
||||
"protocol": "http",
|
||||
"host": [
|
||||
"localhost"
|
||||
],
|
||||
"port": "8080",
|
||||
"path": [
|
||||
"product"
|
||||
]
|
||||
},
|
||||
"description": "Create a Product (and assign a category)"
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Update (Full)",
|
||||
"request": {
|
||||
"method": "PUT",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1ODk1ODU1NjN9.PtfDS1niGoZ7pV6kplI-_q1fVKLnknQ3IwcrLZhoVCU",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\r\n\t\"id\":19,\r\n \"title\": \"product-9-new\",\r\n \"category_id\": 1,\r\n \"image_url\": \"https://images.product19.png\",\r\n \"price\": 20,\r\n \"description\": \"a description for product-9-new\"\r\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "http://localhost:8080/product",
|
||||
"protocol": "http",
|
||||
"host": [
|
||||
"localhost"
|
||||
],
|
||||
"port": "8080",
|
||||
"path": [
|
||||
"product"
|
||||
]
|
||||
},
|
||||
"description": "Update a Product (full-update)"
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Update (Partial)",
|
||||
"request": {
|
||||
"method": "PATCH",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1ODk1ODU1NjN9.PtfDS1niGoZ7pV6kplI-_q1fVKLnknQ3IwcrLZhoVCU",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\r\n \"title\": \"product-9-new-title\"\r\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "http://localhost:8080/product/9",
|
||||
"protocol": "http",
|
||||
"host": [
|
||||
"localhost"
|
||||
],
|
||||
"port": "8080",
|
||||
"path": [
|
||||
"product",
|
||||
"9"
|
||||
]
|
||||
},
|
||||
"description": "Update a Product (partially)"
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
],
|
||||
"description": "Product Client API",
|
||||
"protocolProfileBehavior": {}
|
||||
},
|
||||
{
|
||||
"name": "Get Token",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "http://localhost:8080/token",
|
||||
"protocol": "http",
|
||||
"host": [
|
||||
"localhost"
|
||||
],
|
||||
"port": "8080",
|
||||
"path": [
|
||||
"token"
|
||||
]
|
||||
},
|
||||
"description": "Get Token to access \"write\" (create, update and delete) endpoints"
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
],
|
||||
"protocolProfileBehavior": {}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"title": "product-1",
|
||||
"category_id": 3,
|
||||
"image_url": "https://images.product1.png",
|
||||
"price": 42.42,
|
||||
"description": "a description for product-1"
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"title": "product-19-new-title"
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"id":19,
|
||||
"title": "product-19",
|
||||
"category_id": 3,
|
||||
"image_url": "https://images.product19.png",
|
||||
"price": 20,
|
||||
"description": "a description for product-19"
|
||||
}
|
33
_examples/database/mysql/migration/db.sql
Normal file
|
@ -0,0 +1,33 @@
|
|||
CREATE DATABASE IF NOT EXISTS myapp DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
USE myapp;
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
SET FOREIGN_KEY_CHECKS = 0;
|
||||
|
||||
DROP TABLE IF EXISTS categories;
|
||||
CREATE TABLE categories (
|
||||
id int(11) NOT NULL AUTO_INCREMENT,
|
||||
title varchar(255) NOT NULL,
|
||||
position int(11) NOT NULL,
|
||||
image_url varchar(255) NOT NULL,
|
||||
created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
DROP TABLE IF EXISTS products;
|
||||
CREATE TABLE products (
|
||||
id int(11) NOT NULL AUTO_INCREMENT,
|
||||
category_id int,
|
||||
title varchar(255) NOT NULL,
|
||||
image_url varchar(255) NOT NULL,
|
||||
price decimal(10,2) NOT NULL,
|
||||
description text NOT NULL,
|
||||
created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
FOREIGN KEY (category_id) REFERENCES categories(id)
|
||||
);
|
||||
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
74
_examples/database/mysql/service/category_service.go
Normal file
|
@ -0,0 +1,74 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"myapp/entity"
|
||||
"myapp/sql"
|
||||
)
|
||||
|
||||
// CategoryService represents the category entity service.
|
||||
// Note that the given entity (request) should be already validated
|
||||
// before service's calls.
|
||||
type CategoryService struct {
|
||||
*sql.Service
|
||||
}
|
||||
|
||||
// NewCategoryService returns a new category service to communicate with the database.
|
||||
func NewCategoryService(db sql.Database) *CategoryService {
|
||||
return &CategoryService{Service: sql.NewService(db, new(entity.Category))}
|
||||
}
|
||||
|
||||
// Insert stores a category to the database and returns its ID.
|
||||
func (s *CategoryService) Insert(ctx context.Context, e entity.Category) (int64, error) {
|
||||
if e.Title == "" || e.ImageURL == "" {
|
||||
return 0, sql.ErrUnprocessable
|
||||
}
|
||||
|
||||
q := fmt.Sprintf(`INSERT INTO %s (title, position, image_url)
|
||||
VALUES (?,?,?);`, e.TableName())
|
||||
|
||||
res, err := s.DB().Exec(ctx, q, e.Title, e.Position, e.ImageURL)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return res.LastInsertId()
|
||||
}
|
||||
|
||||
// Update updates a category based on its `ID`.
|
||||
func (s *CategoryService) Update(ctx context.Context, e entity.Category) (int, error) {
|
||||
if e.ID == 0 || e.Title == "" || e.ImageURL == "" {
|
||||
return 0, sql.ErrUnprocessable
|
||||
}
|
||||
|
||||
q := fmt.Sprintf(`UPDATE %s
|
||||
SET
|
||||
title = ?,
|
||||
position = ?,
|
||||
image_url = ?
|
||||
WHERE %s = ?;`, e.TableName(), e.PrimaryKey())
|
||||
|
||||
res, err := s.DB().Exec(ctx, q, e.Title, e.Position, e.ImageURL, e.ID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
n := sql.GetAffectedRows(res)
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// The updatable fields, separately from that we create for any possible future necessities.
|
||||
var categoryUpdateSchema = map[string]reflect.Kind{
|
||||
"title": reflect.String,
|
||||
"image_url": reflect.String,
|
||||
"position": reflect.Int,
|
||||
}
|
||||
|
||||
// PartialUpdate accepts a key-value map to
|
||||
// update the record based on the given "id".
|
||||
func (s *CategoryService) PartialUpdate(ctx context.Context, id int64, attrs map[string]interface{}) (int, error) {
|
||||
return s.Service.PartialUpdate(ctx, id, categoryUpdateSchema, attrs)
|
||||
}
|
42
_examples/database/mysql/service/category_service_test.go
Normal file
|
@ -0,0 +1,42 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"myapp/entity"
|
||||
"myapp/sql"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
)
|
||||
|
||||
func TestCategoryServiceInsert(t *testing.T) {
|
||||
conn, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
db := &sql.MySQL{Conn: conn}
|
||||
service := NewCategoryService(db)
|
||||
newCategory := entity.Category{
|
||||
Title: "computer-internet",
|
||||
Position: 2,
|
||||
ImageURL: "https://animage",
|
||||
}
|
||||
mock.ExpectExec("INSERT INTO categories (title, position, image_url) VALUES (?,?,?);").
|
||||
WithArgs(newCategory.Title, newCategory.Position, newCategory.ImageURL).WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
|
||||
id, err := service.Insert(context.TODO(), newCategory)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if id != 1 {
|
||||
t.Fatalf("expected ID to be 1 as this is the first entry")
|
||||
}
|
||||
|
||||
if err = mock.ExpectationsWereMet(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
110
_examples/database/mysql/service/product_service.go
Normal file
|
@ -0,0 +1,110 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"myapp/entity"
|
||||
"myapp/sql"
|
||||
)
|
||||
|
||||
// ProductService represents the product entity service.
|
||||
// Note that the given entity (request) should be already validated
|
||||
// before service's calls.
|
||||
type ProductService struct {
|
||||
*sql.Service
|
||||
rec sql.Record
|
||||
}
|
||||
|
||||
// NewProductService returns a new product service to communicate with the database.
|
||||
func NewProductService(db sql.Database) *ProductService {
|
||||
return &ProductService{Service: sql.NewService(db, new(entity.Product))}
|
||||
}
|
||||
|
||||
// Insert stores a product to the database and returns its ID.
|
||||
func (s *ProductService) Insert(ctx context.Context, e entity.Product) (int64, error) {
|
||||
if !e.ValidateInsert() {
|
||||
return 0, sql.ErrUnprocessable
|
||||
}
|
||||
|
||||
q := fmt.Sprintf(`INSERT INTO %s (category_id, title, image_url, price, description)
|
||||
VALUES (?,?,?,?,?);`, e.TableName())
|
||||
|
||||
res, err := s.DB().Exec(ctx, q, e.CategoryID, e.Title, e.ImageURL, e.Price, e.Description)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return res.LastInsertId()
|
||||
}
|
||||
|
||||
// BatchInsert inserts one or more products at once and returns the total length created.
|
||||
func (s *ProductService) BatchInsert(ctx context.Context, products []entity.Product) (int, error) {
|
||||
if len(products) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
var (
|
||||
valuesLines []string
|
||||
args []interface{}
|
||||
)
|
||||
|
||||
for _, p := range products {
|
||||
if !p.ValidateInsert() {
|
||||
// all products should be "valid", we don't skip, we cancel.
|
||||
return 0, sql.ErrUnprocessable
|
||||
}
|
||||
|
||||
valuesLines = append(valuesLines, "(?,?,?,?,?)")
|
||||
args = append(args, []interface{}{p.CategoryID, p.Title, p.ImageURL, p.Price, p.Description}...)
|
||||
}
|
||||
|
||||
q := fmt.Sprintf("INSERT INTO %s (category_id, title, image_url, price, description) VALUES %s;",
|
||||
s.RecordInfo().TableName(),
|
||||
strings.Join(valuesLines, ", "))
|
||||
|
||||
res, err := s.DB().Exec(ctx, q, args...)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
n := sql.GetAffectedRows(res)
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// Update updates a product based on its `ID` from the database
|
||||
// and returns the affected numbrer (0 when nothing changed otherwise 1).
|
||||
func (s *ProductService) Update(ctx context.Context, e entity.Product) (int, error) {
|
||||
q := fmt.Sprintf(`UPDATE %s
|
||||
SET
|
||||
category_id = ?,
|
||||
title = ?,
|
||||
image_url = ?,
|
||||
price = ?,
|
||||
description = ?
|
||||
WHERE %s = ?;`, e.TableName(), e.PrimaryKey())
|
||||
|
||||
res, err := s.DB().Exec(ctx, q, e.CategoryID, e.Title, e.ImageURL, e.Price, e.Description, e.ID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
n := sql.GetAffectedRows(res)
|
||||
return n, nil
|
||||
}
|
||||
|
||||
var productUpdateSchema = map[string]reflect.Kind{
|
||||
"category_id": reflect.Int,
|
||||
"title": reflect.String,
|
||||
"image_url": reflect.String,
|
||||
"price": reflect.Float32,
|
||||
"description": reflect.String,
|
||||
}
|
||||
|
||||
// PartialUpdate accepts a key-value map to
|
||||
// update the record based on the given "id".
|
||||
func (s *ProductService) PartialUpdate(ctx context.Context, id int64, attrs map[string]interface{}) (int, error) {
|
||||
return s.Service.PartialUpdate(ctx, id, productUpdateSchema, attrs)
|
||||
}
|
123
_examples/database/mysql/sql/mysql.go
Normal file
|
@ -0,0 +1,123 @@
|
|||
package sql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql" // lint: mysql driver.
|
||||
)
|
||||
|
||||
// MySQL holds the underline connection of a MySQL (or MariaDB) database.
|
||||
// See the `ConnectMySQL` package-level function.
|
||||
type MySQL struct {
|
||||
Conn *sql.DB
|
||||
}
|
||||
|
||||
var _ Database = (*MySQL)(nil)
|
||||
|
||||
var (
|
||||
// DefaultCharset default charset parameter for new databases.
|
||||
DefaultCharset = "utf8mb4"
|
||||
// DefaultCollation default collation parameter for new databases.
|
||||
DefaultCollation = "utf8mb4_unicode_ci"
|
||||
)
|
||||
|
||||
// ConnectMySQL returns a new ready to use MySQL Database instance.
|
||||
// Accepts a single argument of "dsn", i.e:
|
||||
// username:password@tcp(localhost:3306)/myapp?parseTime=true&charset=utf8mb4&collation=utf8mb4_unicode_ci
|
||||
func ConnectMySQL(dsn string) (*MySQL, error) {
|
||||
conn, err := sql.Open("mysql", dsn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = conn.Ping()
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &MySQL{
|
||||
Conn: conn,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CreateDatabase executes the CREATE DATABASE query.
|
||||
func (db *MySQL) CreateDatabase(database string) error {
|
||||
q := fmt.Sprintf("CREATE DATABASE %s DEFAULT CHARSET = %s COLLATE = %s;", database, DefaultCharset, DefaultCollation)
|
||||
_, err := db.Conn.Exec(q)
|
||||
return err
|
||||
}
|
||||
|
||||
// Drop executes the DROP DATABASE query.
|
||||
func (db *MySQL) Drop(database string) error {
|
||||
q := fmt.Sprintf("DROP DATABASE %s;", database)
|
||||
_, err := db.Conn.Exec(q)
|
||||
return err
|
||||
}
|
||||
|
||||
// Select performs the SELECT query for this database (dsn database name is required).
|
||||
func (db *MySQL) Select(ctx context.Context, dest interface{}, query string, args ...interface{}) error {
|
||||
rows, err := db.Conn.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
if scannable, ok := dest.(Scannable); ok {
|
||||
return scannable.Scan(rows)
|
||||
}
|
||||
|
||||
if !rows.Next() {
|
||||
return ErrNoRows
|
||||
}
|
||||
return rows.Scan(dest)
|
||||
|
||||
/* Uncomment this and pass a slice if u want to see reflection powers <3
|
||||
v, ok := dest.(reflect.Value)
|
||||
if !ok {
|
||||
v = reflect.Indirect(reflect.ValueOf(dest))
|
||||
}
|
||||
|
||||
sliceTyp := v.Type()
|
||||
|
||||
if sliceTyp.Kind() != reflect.Slice {
|
||||
sliceTyp = reflect.SliceOf(sliceTyp)
|
||||
}
|
||||
|
||||
sliceElementTyp := deref(sliceTyp.Elem())
|
||||
for rows.Next() {
|
||||
obj := reflect.New(sliceElementTyp)
|
||||
obj.Interface().(Scannable).Scan(rows)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
v.Set(reflect.Append(v, reflect.Indirect(obj)))
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
// Get same as `Select` but it moves the cursor to the first result.
|
||||
func (db *MySQL) Get(ctx context.Context, dest interface{}, query string, args ...interface{}) error {
|
||||
rows, err := db.Conn.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
if !rows.Next() {
|
||||
return ErrNoRows
|
||||
}
|
||||
|
||||
if scannable, ok := dest.(Scannable); ok {
|
||||
return scannable.Scan(rows)
|
||||
}
|
||||
|
||||
return rows.Scan(dest)
|
||||
}
|
||||
|
||||
// Exec executes a query. It does not return any rows.
|
||||
// Use the first output parameter to count the affected rows on UPDATE, INSERT, or DELETE.
|
||||
func (db *MySQL) Exec(ctx context.Context, query string, args ...interface{}) (sql.Result, error) {
|
||||
return db.Conn.ExecContext(ctx, query, args...)
|
||||
}
|
243
_examples/database/mysql/sql/service.go
Normal file
|
@ -0,0 +1,243 @@
|
|||
package sql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Service holder for common queries.
|
||||
// Note: each entity service keeps its own base Service instance.
|
||||
type Service struct {
|
||||
db Database
|
||||
rec Record // see `Count`, `List` and `DeleteByID` methods.
|
||||
}
|
||||
|
||||
// NewService returns a new (SQL) base service for common operations.
|
||||
func NewService(db Database, of Record) *Service {
|
||||
return &Service{db: db, rec: of}
|
||||
}
|
||||
|
||||
// DB exposes the database instance.
|
||||
func (s *Service) DB() Database {
|
||||
return s.db
|
||||
}
|
||||
|
||||
// RecordInfo returns the record info provided through `NewService`.
|
||||
func (s *Service) RecordInfo() Record {
|
||||
return s.rec
|
||||
}
|
||||
|
||||
// ErrNoRows is returned when GET doesn't return a row.
|
||||
// A shortcut of sql.ErrNoRows.
|
||||
var ErrNoRows = sql.ErrNoRows
|
||||
|
||||
// GetByID binds a single record from the databases to the "dest".
|
||||
func (s *Service) GetByID(ctx context.Context, dest interface{}, id int64) error {
|
||||
q := fmt.Sprintf("SELECT * FROM %s WHERE %s = ? LIMIT 1", s.rec.TableName(), s.rec.PrimaryKey())
|
||||
err := s.db.Get(ctx, dest, q, id)
|
||||
return err
|
||||
// if err != nil {
|
||||
// if err == sql.ErrNoRows {
|
||||
// return false, nil
|
||||
// }
|
||||
|
||||
// return false, err
|
||||
// }
|
||||
|
||||
// return true, nil
|
||||
}
|
||||
|
||||
// Count returns the total records count in the table.
|
||||
func (s *Service) Count(ctx context.Context) (total int64, err error) {
|
||||
q := fmt.Sprintf("SELECT COUNT(DISTINCT %s) FROM %s", s.rec.PrimaryKey(), s.rec.TableName())
|
||||
if err = s.db.Select(ctx, &total, q); err == sql.ErrNoRows {
|
||||
err = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ListOptions holds the options to be passed on the `Service.List` method.
|
||||
type ListOptions struct {
|
||||
Table string // the table name.
|
||||
Offset uint64 // inclusive.
|
||||
Limit uint64
|
||||
OrderByColumn string
|
||||
Order string // "ASC" or "DESC" (could be a bool type instead).
|
||||
WhereColumn string
|
||||
WhereValue interface{}
|
||||
}
|
||||
|
||||
// Where accepts a column name and column value to set
|
||||
// on the WHERE clause of the result query.
|
||||
// It returns a new `ListOptions` value.
|
||||
// Note that this is a basic implementation which just takes care our current needs.
|
||||
func (opt ListOptions) Where(colName string, colValue interface{}) ListOptions {
|
||||
opt.WhereColumn = colName
|
||||
opt.WhereValue = colValue
|
||||
return opt
|
||||
}
|
||||
|
||||
// BuildQuery returns the query and the arguments that
|
||||
// should be form a SELECT command.
|
||||
func (opt ListOptions) BuildQuery() (q string, args []interface{}) {
|
||||
q = fmt.Sprintf("SELECT * FROM %s", opt.Table)
|
||||
|
||||
if opt.WhereColumn != "" && opt.WhereValue != nil {
|
||||
q += fmt.Sprintf(" WHERE %s = ?", opt.WhereColumn)
|
||||
args = append(args, opt.WhereValue)
|
||||
}
|
||||
|
||||
if opt.OrderByColumn != "" {
|
||||
q += fmt.Sprintf(" ORDER BY %s %s", opt.OrderByColumn, ParseOrder(opt.Order))
|
||||
}
|
||||
|
||||
if opt.Limit > 0 {
|
||||
q += fmt.Sprintf(" LIMIT %d", opt.Limit) // offset below.
|
||||
}
|
||||
|
||||
if opt.Offset > 0 {
|
||||
q += fmt.Sprintf(" OFFSET %d", opt.Offset)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// const defaultLimit = 30 // default limit if not set.
|
||||
|
||||
// ParseListOptions returns a `ListOptions` from a map[string][]string.
|
||||
func ParseListOptions(q url.Values) ListOptions {
|
||||
offset, _ := strconv.ParseUint(q.Get("offset"), 10, 64)
|
||||
limit, _ := strconv.ParseUint(q.Get("limit"), 10, 64)
|
||||
order := q.Get("order") // empty, asc(...) or desc(...).
|
||||
orderBy := q.Get("by") // e.g. price
|
||||
|
||||
return ListOptions{Offset: offset, Limit: limit, Order: order, OrderByColumn: orderBy}
|
||||
}
|
||||
|
||||
// List binds one or more records from the database to the "dest".
|
||||
// If the record supports ordering then it will sort by the `Sorted.OrderBy` column name(s).
|
||||
// Use the "order" input parameter to set a descending order ("DESC").
|
||||
func (s *Service) List(ctx context.Context, dest interface{}, opts ListOptions) error {
|
||||
// Set table and order by column from record info for `List` by options
|
||||
// so it can be more flexible to perform read-only calls of other table's too.
|
||||
if opts.Table == "" {
|
||||
// If missing then try to set it by record info.
|
||||
opts.Table = s.rec.TableName()
|
||||
}
|
||||
if opts.OrderByColumn == "" {
|
||||
if b, ok := s.rec.(Sorted); ok {
|
||||
opts.OrderByColumn = b.SortBy()
|
||||
}
|
||||
}
|
||||
|
||||
q, args := opts.BuildQuery()
|
||||
return s.db.Select(ctx, dest, q, args...)
|
||||
}
|
||||
|
||||
// DeleteByID removes a single record of "dest" from the database.
|
||||
func (s *Service) DeleteByID(ctx context.Context, id int64) (int, error) {
|
||||
q := fmt.Sprintf("DELETE FROM %s WHERE %s = ? LIMIT 1", s.rec.TableName(), s.rec.PrimaryKey())
|
||||
res, err := s.db.Exec(ctx, q, id)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return GetAffectedRows(res), nil
|
||||
}
|
||||
|
||||
// ErrUnprocessable indicates error caused by invalid entity (entity's key-values).
|
||||
// The syntax of the request entity is correct, but it was unable to process the contained instructions
|
||||
// e.g. empty or unsupported value.
|
||||
//
|
||||
// See `../service/XService.Insert` and `../service/XService.Update`
|
||||
// and `PartialUpdate`.
|
||||
var ErrUnprocessable = errors.New("invalid entity")
|
||||
|
||||
// PartialUpdate accepts a columns schema and a key-value map to
|
||||
// update the record based on the given "id".
|
||||
// Note: Trivial string, int and boolean type validations are performed here.
|
||||
func (s *Service) PartialUpdate(ctx context.Context, id int64, schema map[string]reflect.Kind, attrs map[string]interface{}) (int, error) {
|
||||
if len(schema) == 0 || len(attrs) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
var (
|
||||
keyLines []string
|
||||
values []interface{}
|
||||
)
|
||||
|
||||
for key, kind := range schema {
|
||||
v, ok := attrs[key]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
switch v.(type) {
|
||||
case string:
|
||||
if kind != reflect.String {
|
||||
return 0, ErrUnprocessable
|
||||
}
|
||||
case int:
|
||||
if kind != reflect.Int {
|
||||
return 0, ErrUnprocessable
|
||||
}
|
||||
case bool:
|
||||
if kind != reflect.Bool {
|
||||
return 0, ErrUnprocessable
|
||||
}
|
||||
}
|
||||
|
||||
keyLines = append(keyLines, fmt.Sprintf("%s = ?", key))
|
||||
values = append(values, v)
|
||||
}
|
||||
|
||||
if len(values) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
q := fmt.Sprintf("UPDATE %s SET %s WHERE %s = ?;",
|
||||
s.rec.TableName(), strings.Join(keyLines, ", "), s.rec.PrimaryKey())
|
||||
|
||||
res, err := s.DB().Exec(ctx, q, append(values, id)...)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
n := GetAffectedRows(res)
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// GetAffectedRows returns the number of affected rows after
|
||||
// a DELETE or UPDATE operation.
|
||||
func GetAffectedRows(result sql.Result) int {
|
||||
if result == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
n, _ := result.RowsAffected()
|
||||
return int(n)
|
||||
}
|
||||
|
||||
const (
|
||||
ascending = "ASC"
|
||||
descending = "DESC"
|
||||
)
|
||||
|
||||
// ParseOrder accept an order string and returns a valid mysql ORDER clause.
|
||||
// Defaults to "ASC". Two possible outputs: "ASC" and "DESC".
|
||||
func ParseOrder(order string) string {
|
||||
order = strings.TrimSpace(order)
|
||||
if len(order) >= 4 {
|
||||
if strings.HasPrefix(strings.ToUpper(order), descending) {
|
||||
return descending
|
||||
}
|
||||
}
|
||||
|
||||
return ascending
|
||||
}
|
40
_examples/database/mysql/sql/sql.go
Normal file
|
@ -0,0 +1,40 @@
|
|||
package sql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
// Database is an interface which a database(sql) should implement.
|
||||
type Database interface {
|
||||
Get(ctx context.Context, dest interface{}, q string, args ...interface{}) error
|
||||
Select(ctx context.Context, dest interface{}, q string, args ...interface{}) error
|
||||
Exec(ctx context.Context, q string, args ...interface{}) (sql.Result, error)
|
||||
}
|
||||
|
||||
// Record should represent a database record.
|
||||
// It holds the table name and the primary key.
|
||||
// Entities should implement that
|
||||
// in order to use the BaseService's methods.
|
||||
type Record interface {
|
||||
TableName() string // the table name which record belongs to.
|
||||
PrimaryKey() string // the primary key of the record.
|
||||
}
|
||||
|
||||
// Sorted should represent a set of database records
|
||||
// that should be rendered with order.
|
||||
//
|
||||
// It does NOT support the form of
|
||||
// column1 ASC,
|
||||
// column2 DESC
|
||||
// The OrderBy method should return text in form of:
|
||||
// column1
|
||||
// or column1, column2
|
||||
type Sorted interface {
|
||||
SortBy() string // column names separated by comma.
|
||||
}
|
||||
|
||||
// Scannable for go structs to bind their fields.
|
||||
type Scannable interface {
|
||||
Scan(*sql.Rows) error
|
||||
}
|
66
_examples/dependency-injection/jwt/contrib/main.go
Normal file
|
@ -0,0 +1,66 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/kataras/iris/v12"
|
||||
|
||||
"github.com/iris-contrib/middleware/jwt"
|
||||
)
|
||||
|
||||
var secret = []byte("My Secret Key")
|
||||
|
||||
func main() {
|
||||
app := iris.New()
|
||||
app.ConfigureContainer(register)
|
||||
|
||||
app.Listen(":8080")
|
||||
}
|
||||
|
||||
func register(api *iris.APIContainer) {
|
||||
j := jwt.New(jwt.Config{
|
||||
// Extract by "token" url parameter.
|
||||
Extractor: jwt.FromFirst(jwt.FromParameter("token"), jwt.FromAuthHeader),
|
||||
ValidationKeyGetter: func(token *jwt.Token) (interface{}, error) {
|
||||
return secret, nil
|
||||
},
|
||||
SigningMethod: jwt.SigningMethodHS256,
|
||||
})
|
||||
|
||||
api.Get("/authenticate", writeToken)
|
||||
// This works as usually:
|
||||
api.Get("/restricted", j.Serve, restrictedPage)
|
||||
|
||||
// You can also bind the *jwt.Token (see `verifiedWithBindedTokenPage`)
|
||||
// by registering a *jwt.Token dependency.
|
||||
//
|
||||
// api.RegisterDependency(func(ctx iris.Context) *jwt.Token {
|
||||
// if err := j.CheckJWT(ctx); err != nil {
|
||||
// ctx.StopWithStatus(iris.StatusUnauthorized)
|
||||
// return nil
|
||||
// }
|
||||
//
|
||||
// token := j.Get(ctx)
|
||||
// return token
|
||||
// })
|
||||
// ^ You can do the same with MVC too, as the container is shared and works
|
||||
// the same way in both functions-as-handlers and structs-as-controllers.
|
||||
//
|
||||
// api.Get("/", restrictedPageWithBindedTokenPage)
|
||||
}
|
||||
|
||||
func writeToken() string {
|
||||
token := jwt.NewTokenWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"foo": "bar",
|
||||
})
|
||||
|
||||
tokenString, _ := token.SignedString(secret)
|
||||
return tokenString
|
||||
}
|
||||
|
||||
func restrictedPage() string {
|
||||
return "This page can only be seen by verified clients"
|
||||
}
|
||||
|
||||
func restrictedPageWithBindedTokenPage(token *jwt.Token) string {
|
||||
// Token[foo] value: bar
|
||||
return "Token[foo] value: " + token.Claims.(jwt.MapClaims)["foo"].(string)
|
||||
}
|
|
@ -1,13 +1,12 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/kataras/iris/v12"
|
||||
|
||||
"github.com/iris-contrib/middleware/jwt"
|
||||
"github.com/kataras/iris/v12/middleware/jwt"
|
||||
)
|
||||
|
||||
var secret = []byte("My Secret Key")
|
||||
|
||||
func main() {
|
||||
app := iris.New()
|
||||
app.ConfigureContainer(register)
|
||||
|
@ -16,51 +15,36 @@ func main() {
|
|||
}
|
||||
|
||||
func register(api *iris.APIContainer) {
|
||||
j := jwt.New(jwt.Config{
|
||||
// Extract by "token" url parameter.
|
||||
Extractor: jwt.FromFirst(jwt.FromParameter("token"), jwt.FromAuthHeader),
|
||||
ValidationKeyGetter: func(token *jwt.Token) (interface{}, error) {
|
||||
return secret, nil
|
||||
},
|
||||
SigningMethod: jwt.SigningMethodHS256,
|
||||
j := jwt.HMAC(15*time.Minute, "secret", "secretforencrypt")
|
||||
|
||||
api.RegisterDependency(func(ctx iris.Context) (claims userClaims) {
|
||||
if err := j.VerifyToken(ctx, &claims); err != nil {
|
||||
ctx.StopWithError(iris.StatusUnauthorized, err)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
})
|
||||
|
||||
api.Get("/token", writeToken)
|
||||
// This works as usually:
|
||||
api.Get("/", j.Serve, verifiedPage)
|
||||
|
||||
// You can also bind the *jwt.Token (see `verifiedWithBindedTokenPage`)
|
||||
// by registering a *jwt.Token dependency.
|
||||
//
|
||||
// api.RegisterDependency(func(ctx iris.Context) *jwt.Token {
|
||||
// if err := j.CheckJWT(ctx); err != nil {
|
||||
// ctx.StopWithStatus(iris.StatusUnauthorized)
|
||||
// return nil
|
||||
// }
|
||||
//
|
||||
// token := j.Get(ctx)
|
||||
// return token
|
||||
// })
|
||||
// ^ You can do the same with MVC too, as the container is shared and works
|
||||
// the same way in both functions-as-handlers and structs-as-controllers.
|
||||
//
|
||||
// api.Get("/", verifiedWithBindedTokenPage)
|
||||
api.Get("/authenticate", writeToken(j))
|
||||
api.Get("/restricted", restrictedPage)
|
||||
}
|
||||
|
||||
func writeToken() string {
|
||||
token := jwt.NewTokenWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"foo": "bar",
|
||||
type userClaims struct {
|
||||
jwt.Claims
|
||||
Username string
|
||||
}
|
||||
|
||||
func writeToken(j *jwt.JWT) iris.Handler {
|
||||
return func(ctx iris.Context) {
|
||||
j.WriteToken(ctx, userClaims{
|
||||
Claims: j.Expiry(jwt.Claims{Issuer: "an-issuer"}),
|
||||
Username: "kataras",
|
||||
})
|
||||
|
||||
tokenString, _ := token.SignedString(secret)
|
||||
return tokenString
|
||||
}
|
||||
}
|
||||
|
||||
func verifiedPage() string {
|
||||
return "This page can only be seen by verified clients"
|
||||
}
|
||||
|
||||
func verifiedWithBindedTokenPage(token *jwt.Token) string {
|
||||
// Token[foo] value: bar
|
||||
return "Token[foo] value: " + token.Claims.(jwt.MapClaims)["foo"].(string)
|
||||
func restrictedPage(claims userClaims) string {
|
||||
// userClaims.Username: kataras
|
||||
return "userClaims.Username: " + claims.Username
|
||||
}
|
||||
|
|
|
@ -302,7 +302,7 @@ If you have done it successfully. Now go and upload some images and reload the u
|
|||
- http://www.dropzonejs.com/#server-side-implementation
|
||||
- https://www.startutorial.com/articles/view/how-to-build-a-file-upload-form-using-dropzonejs-and-php
|
||||
- https://docs.iris-go.com
|
||||
- https://github.com/kataras/iris/tree/master/_examples/tutorial/dropzonejs
|
||||
- https://github.com/kataras/iris/tree/master/_examples/dropzonejs
|
||||
|
||||
## The end
|
||||
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |