Browse Source

reorganization of _examples and add some new examples such as iris+groupcache+mysql+docker

Former-commit-id: ed635ee95d
tags/v0.0.1
Gerasimos (Makis) Maropoulos 1 year ago
parent
commit
ed45c77be5
100 changed files with 3288 additions and 800 deletions
  1. +9
    -10
      HISTORY.md
  2. +1
    -1
      HISTORY_ES.md
  3. +94
    -98
      _examples/README.md
  4. +3
    -0
      _examples/apidoc/swagger/README.md
  5. +0
    -0
      _examples/auth/basicauth/main.go
  6. +0
    -0
      _examples/auth/basicauth/main_test.go
  7. +0
    -0
      _examples/auth/goth/main.go
  8. +2
    -2
      _examples/auth/goth/templates/index.html
  9. +10
    -10
      _examples/auth/goth/templates/user.html
  10. +0
    -0
      _examples/auth/hcaptcha/hosts
  11. +0
    -0
      _examples/auth/hcaptcha/main.go
  12. +0
    -0
      _examples/auth/hcaptcha/templates/register_form.html
  13. +0
    -0
      _examples/auth/jwt/README.md
  14. +3
    -3
      _examples/auth/jwt/main.go
  15. +0
    -0
      _examples/auth/jwt/rsa_password_protected.key
  16. +0
    -0
      _examples/auth/permissions/main.go
  17. +0
    -0
      _examples/auth/recaptcha/custom_form/main.go
  18. +0
    -0
      _examples/auth/recaptcha/main.go
  19. +0
    -7
      _examples/authentication/README.md
  20. +0
    -0
      _examples/bootstrap/bootstrap/bootstrapper.go
  21. +0
    -0
      _examples/bootstrap/folder_structure.png
  22. +3
    -3
      _examples/bootstrap/main.go
  23. +3
    -3
      _examples/bootstrap/main_test.go
  24. +1
    -1
      _examples/bootstrap/middleware/identity/identity.go
  25. +0
    -0
      _examples/bootstrap/public/favicon.ico
  26. +0
    -0
      _examples/bootstrap/routes/follower.go
  27. +0
    -0
      _examples/bootstrap/routes/following.go
  28. +0
    -0
      _examples/bootstrap/routes/index.go
  29. +0
    -0
      _examples/bootstrap/routes/like.go
  30. +13
    -0
      _examples/bootstrap/routes/routes.go
  31. +0
    -0
      _examples/bootstrap/views/index.html
  32. +4
    -4
      _examples/bootstrap/views/shared/error.html
  33. +22
    -22
      _examples/bootstrap/views/shared/layout.html
  34. +8
    -8
      _examples/caddy/Caddyfile
  35. +23
    -23
      _examples/caddy/README.md
  36. +0
    -0
      _examples/caddy/server1/main.go
  37. +2
    -2
      _examples/caddy/server1/views/index.html
  38. +10
    -10
      _examples/caddy/server1/views/shared/layout.html
  39. +1
    -1
      _examples/caddy/server2/main.go
  40. +0
    -0
      _examples/database/mongodb/.env
  41. +0
    -0
      _examples/database/mongodb/0_create_movie.png
  42. +0
    -0
      _examples/database/mongodb/1_update_movie.png
  43. +0
    -0
      _examples/database/mongodb/2_get_all_movies.png
  44. +0
    -0
      _examples/database/mongodb/3_get_movie.png
  45. +0
    -0
      _examples/database/mongodb/4_delete_movie.png
  46. +17
    -0
      _examples/database/mongodb/Dockerfile
  47. +9
    -3
      _examples/database/mongodb/README.md
  48. +2
    -2
      _examples/database/mongodb/api/store/movie.go
  49. +18
    -0
      _examples/database/mongodb/docker-compose.yml
  50. +26
    -12
      _examples/database/mongodb/env/env.go
  51. +9
    -0
      _examples/database/mongodb/go.mod
  52. +0
    -0
      _examples/database/mongodb/httputil/error.go
  53. +4
    -4
      _examples/database/mongodb/main.go
  54. +0
    -0
      _examples/database/mongodb/store/movie.go
  55. +17
    -0
      _examples/database/mysql/Dockerfile
  56. +146
    -0
      _examples/database/mysql/README.md
  57. +97
    -0
      _examples/database/mysql/api/api.go
  58. +251
    -0
      _examples/database/mysql/api/category_handler.go
  59. +25
    -0
      _examples/database/mysql/api/helper.go
  60. +60
    -0
      _examples/database/mysql/api/httperror.go
  61. +0
    -0
      _examples/database/mysql/api/middleware/.gitkeep
  62. +173
    -0
      _examples/database/mysql/api/product_handler.go
  63. +120
    -0
      _examples/database/mysql/cache/groupcache.go
  64. +32
    -0
      _examples/database/mysql/docker-compose.yml
  65. +89
    -0
      _examples/database/mysql/entity/category.go
  66. +95
    -0
      _examples/database/mysql/entity/product.go
  67. +9
    -0
      _examples/database/mysql/go.mod
  68. +44
    -0
      _examples/database/mysql/main.go
  69. +5
    -0
      _examples/database/mysql/migration/api_category/create_category.json
  70. +31
    -0
      _examples/database/mysql/migration/api_category/insert_products_category.json
  71. +6
    -0
      _examples/database/mysql/migration/api_category/update_category.json
  72. +3
    -0
      _examples/database/mysql/migration/api_category/update_partial_category.json
  73. +484
    -0
      _examples/database/mysql/migration/api_postman.json
  74. +7
    -0
      _examples/database/mysql/migration/api_product/create_product.json
  75. +3
    -0
      _examples/database/mysql/migration/api_product/update_partial_product.json
  76. +8
    -0
      _examples/database/mysql/migration/api_product/update_product.json
  77. +33
    -0
      _examples/database/mysql/migration/db.sql
  78. +74
    -0
      _examples/database/mysql/service/category_service.go
  79. +42
    -0
      _examples/database/mysql/service/category_service_test.go
  80. +110
    -0
      _examples/database/mysql/service/product_service.go
  81. +123
    -0
      _examples/database/mysql/sql/mysql.go
  82. +243
    -0
      _examples/database/mysql/sql/service.go
  83. +40
    -0
      _examples/database/mysql/sql/sql.go
  84. +0
    -0
      _examples/database/orm/gorm/REAMDE.md
  85. +0
    -0
      _examples/database/orm/gorm/main.go
  86. +0
    -0
      _examples/database/orm/xorm/main.go
  87. +66
    -0
      _examples/dependency-injection/jwt/contrib/main.go
  88. +27
    -43
      _examples/dependency-injection/jwt/main.go
  89. +0
    -0
      _examples/desktop/blink/main.go
  90. +0
    -0
      _examples/desktop/lorca/main.go
  91. +0
    -0
      _examples/desktop/webview/main.go
  92. +167
    -167
      _examples/dropzonejs/README.md
  93. +309
    -309
      _examples/dropzonejs/README_PART2.md
  94. +0
    -0
      _examples/dropzonejs/folder_structure.png
  95. +0
    -0
      _examples/dropzonejs/meta.yml
  96. +0
    -0
      _examples/dropzonejs/no_files.png
  97. +0
    -0
      _examples/dropzonejs/src/main.go
  98. +0
    -0
      _examples/dropzonejs/src/public/css/dropzone.css
  99. +0
    -0
      _examples/dropzonejs/src/public/js/dropzone.js
  100. +52
    -52
      _examples/dropzonejs/src/views/upload.html

+ 9
- 10
HISTORY.md View File

@@ -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


+ 1
- 1
HISTORY_ES.md View File

@@ -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


+ 94
- 98
_examples/README.md View File

@@ -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)
@@ -56,19 +55,27 @@
* Custom Context
* [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)
* Subdomains
* [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)
* [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](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)
* [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](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)
* [Webassembly](webassembly/basic/main.go)
* Cache
* [Simple](cache/simple/main.go)
* [Client-Side (304)](cache/client-side/main.go)
* [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](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
- 0
_examples/apidoc/swagger/README.md View File

@@ -0,0 +1,3 @@
# Swagger 2.0

Visit https://github.com/iris-contrib/swagger instead.

_examples/authentication/basicauth/main.go → _examples/auth/basicauth/main.go View File


_examples/authentication/basicauth/main_test.go → _examples/auth/basicauth/main_test.go View File


_examples/authentication/oauth2/main.go → _examples/auth/goth/main.go View File


_examples/authentication/oauth2/templates/index.html → _examples/auth/goth/templates/index.html View File

@@ -1,3 +1,3 @@
{{range $key,$value:=.Providers}}
<p><a href="/auth/{{$value}}">Log in with {{index $.ProvidersMap $value}}</a></p>
{{range $key,$value:=.Providers}}
<p><a href="/auth/{{$value}}">Log in with {{index $.ProvidersMap $value}}</a></p>
{{end}}

_examples/authentication/oauth2/templates/user.html → _examples/auth/goth/templates/user.html View File

@@ -1,11 +1,11 @@
<p><a href="/logout/{{.Provider}}">logout</a></p>
<p>Name: {{.Name}} [{{.LastName}}, {{.FirstName}}]</p>
<p>Email: {{.Email}}</p>
<p>NickName: {{.NickName}}</p>
<p>Location: {{.Location}}</p>
<p>AvatarURL: {{.AvatarURL}} <img src="{{.AvatarURL}}"></p>
<p>Description: {{.Description}}</p>
<p>UserID: {{.UserID}}</p>
<p>AccessToken: {{.AccessToken}}</p>
<p>ExpiresAt: {{.ExpiresAt}}</p>
<p><a href="/logout/{{.Provider}}">logout</a></p>
<p>Name: {{.Name}} [{{.LastName}}, {{.FirstName}}]</p>
<p>Email: {{.Email}}</p>
<p>NickName: {{.NickName}}</p>
<p>Location: {{.Location}}</p>
<p>AvatarURL: {{.AvatarURL}} <img src="{{.AvatarURL}}"></p>
<p>Description: {{.Description}}</p>
<p>UserID: {{.UserID}}</p>
<p>AccessToken: {{.AccessToken}}</p>
<p>ExpiresAt: {{.ExpiresAt}}</p>
<p>RefreshToken: {{.RefreshToken}}</p>

_examples/miscellaneous/hcaptcha/hosts → _examples/auth/hcaptcha/hosts View File


_examples/miscellaneous/hcaptcha/main.go → _examples/auth/hcaptcha/main.go View File


_examples/miscellaneous/hcaptcha/templates/register_form.html → _examples/auth/hcaptcha/templates/register_form.html View File


_examples/miscellaneous/jwt/README.md → _examples/auth/jwt/README.md View File


_examples/miscellaneous/jwt/main.go → _examples/auth/jwt/main.go View File

@@ -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)
}

_examples/miscellaneous/jwt/rsa_password_protected.key → _examples/auth/jwt/rsa_password_protected.key View File


_examples/permissions/main.go → _examples/auth/permissions/main.go View File


_examples/miscellaneous/recaptcha/custom_form/main.go → _examples/auth/recaptcha/custom_form/main.go View File


_examples/miscellaneous/recaptcha/main.go → _examples/auth/recaptcha/main.go View File


+ 0
- 7
_examples/authentication/README.md View File

@@ -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)

_examples/structuring/bootstrap/bootstrap/bootstrapper.go → _examples/bootstrap/bootstrap/bootstrapper.go View File


_examples/structuring/bootstrap/folder_structure.png → _examples/bootstrap/folder_structure.png View File


_examples/structuring/bootstrap/main.go → _examples/bootstrap/main.go View File

@@ -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 {

_examples/structuring/bootstrap/main_test.go → _examples/bootstrap/main_test.go View File

@@ -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)

_examples/structuring/bootstrap/middleware/identity/identity.go → _examples/bootstrap/middleware/identity/identity.go View File

@@ -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

_examples/structuring/bootstrap/public/favicon.ico → _examples/bootstrap/public/favicon.ico View File


_examples/structuring/bootstrap/routes/follower.go → _examples/bootstrap/routes/follower.go View File


_examples/structuring/bootstrap/routes/following.go → _examples/bootstrap/routes/following.go View File


_examples/structuring/bootstrap/routes/index.go → _examples/bootstrap/routes/index.go View File


_examples/structuring/bootstrap/routes/like.go → _examples/bootstrap/routes/like.go View File


+ 13
- 0
_examples/bootstrap/routes/routes.go View 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)
}

_examples/structuring/bootstrap/views/index.html → _examples/bootstrap/views/index.html View File


_examples/structuring/bootstrap/views/shared/error.html → _examples/bootstrap/views/shared/error.html View File

@@ -1,5 +1,5 @@
<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>
<h3>{{.Err.status}}</h3>
<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>
<h3>{{.Err.status}}</h3>
<h4>{{.Err.message}}</h4>

_examples/structuring/bootstrap/views/shared/layout.html → _examples/bootstrap/views/shared/layout.html View File

@@ -1,23 +1,23 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico" />
<title>{{.Title}} - {{.AppName}}</title>
</head>
<body>
<div>
<!-- Render the current template here -->
{{ yield }}
<hr />
<footer>
<p>&copy; 2017 - {{.AppOwner}}</p>
</footer>
</div>
</body>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico" />
<title>{{.Title}} - {{.AppName}}</title>
</head>
<body>
<div>
<!-- Render the current template here -->
{{ yield }}
<hr />
<footer>
<p>&copy; 2017 - {{.AppOwner}}</p>
</footer>
</div>
</body>
</html>

_examples/tutorial/caddy/Caddyfile → _examples/caddy/Caddyfile View File

@@ -1,9 +1,9 @@
example.com {
header / Server "Iris"
proxy / example.com:9091 # localhost:9091
}
api.example.com {
header / Server "Iris"
proxy / api.example.com:9092 # localhost:9092
example.com {
header / Server "Iris"
proxy / example.com:9091 # localhost:9091
}
api.example.com {
header / Server "Iris"
proxy / api.example.com:9092 # localhost:9092
}

_examples/tutorial/caddy/README.md → _examples/caddy/README.md View File

@@ -1,24 +1,24 @@
# Caddy loves Iris
The `Caddyfile` shows how you can use caddy to listen on ports 80 & 443 and sit in front of iris webserver(s) that serving on a different port (9091 and 9092 in this case; see Caddyfile).
## Running our two web servers
1. Go to `$GOPATH/src/github.com/kataras/iris/_examples/tutorial/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`
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
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`
## Notes
Iris has the `app.Run(iris.AutoTLS(":443", "example.com", "mail@example.com"))` which does
# Caddy loves Iris
The `Caddyfile` shows how you can use caddy to listen on ports 80 & 443 and sit in front of iris webserver(s) that serving on a different port (9091 and 9092 in this case; see Caddyfile).
## Running our two web servers
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/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/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`
## Notes
Iris has the `app.Run(iris.AutoTLS(":443", "example.com", "mail@example.com"))` which does
the exactly same thing but caddy is a great tool that helps you when you run multiple web servers from one host machine, i.e iris, apache, tomcat.

_examples/tutorial/caddy/server1/main.go → _examples/caddy/server1/main.go View File


_examples/tutorial/caddy/server1/views/index.html → _examples/caddy/server1/views/index.html View File

@@ -1,3 +1,3 @@
<div>
{{.Message}}
<div>
{{.Message}}
</div>

_examples/tutorial/caddy/server1/views/shared/layout.html → _examples/caddy/server1/views/shared/layout.html View File

@@ -1,11 +1,11 @@
<html>
<head>
<title>{{.Layout.Title}}</title>
</head>
<body>
{{ yield }}
</body>
<html>
<head>
<title>{{.Layout.Title}}</title>
</head>
<body>
{{ yield }}
</body>
</html>

_examples/tutorial/caddy/server2/main.go → _examples/caddy/server2/main.go View File

@@ -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}

_examples/tutorial/mongodb/.env → _examples/database/mongodb/.env View File


_examples/tutorial/mongodb/0_create_movie.png → _examples/database/mongodb/0_create_movie.png View File


_examples/tutorial/mongodb/1_update_movie.png → _examples/database/mongodb/1_update_movie.png View File


_examples/tutorial/mongodb/2_get_all_movies.png → _examples/database/mongodb/2_get_all_movies.png View File


_examples/tutorial/mongodb/3_get_movie.png → _examples/database/mongodb/3_get_movie.png View File


_examples/tutorial/mongodb/4_delete_movie.png → _examples/database/mongodb/4_delete_movie.png View File


+ 17
- 0
_examples/database/mongodb/Dockerfile View 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"]

_examples/tutorial/mongodb/README.md → _examples/database/mongodb/README.md View File

@@ -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
```

_examples/tutorial/mongodb/api/store/movie.go → _examples/database/mongodb/api/store/movie.go View File

@@ -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
- 0
_examples/database/mongodb/docker-compose.yml View 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

_examples/tutorial/mongodb/env/env.go → _examples/database/mongodb/env/env.go View File

@@ -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,29 +37,32 @@ 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 err := godotenv.Load(envFiles...); err != nil {
panic(fmt.Sprintf("error loading environment variables from [%s]: %v", envFileName, err))
}
if fileExists(envFile) {
log.Printf("Loading environment variables from file: %s\n", envFile)

envMap, _ := godotenv.Read(envFiles...)
for k, v := range envMap {
log.Printf("◽ %s=%s\n", k, v)
if err := godotenv.Load(envFile); err != nil {
panic(fmt.Sprintf("error loading environment variables from [%s]: %v", envFile, err))
}
}
}

// 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
- 0
_examples/database/mongodb/go.mod View 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
)

_examples/tutorial/mongodb/httputil/error.go → _examples/database/mongodb/httputil/error.go View File


_examples/tutorial/mongodb/main.go → _examples/database/mongodb/main.go View File

@@ -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)
}

_examples/tutorial/mongodb/store/movie.go → _examples/database/mongodb/store/movie.go View File


+ 17
- 0
_examples/database/mysql/Dockerfile View 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
- 0
_examples/database/mysql/README.md View 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
- 0
_examples/database/mysql/api/api.go View 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
- 0
_examples/database/mysql/api/category_handler.go View 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
- 0
_examples/database/mysql/api/helper.go View 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
- 0
_examples/database/mysql/api/httperror.go View 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 == "")
}

_examples/http_request/upload-file/uploads/.gitkeep → _examples/database/mysql/api/middleware/.gitkeep View File


+ 173
- 0
_examples/database/mysql/api/product_handler.go View 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
- 0
_examples/database/mysql/cache/groupcache.go View 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
- 0
_examples/database/mysql/docker-compose.yml View 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
- 0
_examples/database/mysql/entity/category.go View 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
- 0
_examples/database/mysql/entity/product.go View 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 {