init clean-arch

This commit is contained in:
fengzhida 2023-05-05 20:27:33 +08:00
commit f07fd1ab7d
31 changed files with 1824 additions and 0 deletions

7
.dockerignore Normal file
View File

@ -0,0 +1,7 @@
.git
.vscode
assets
.dockerignore
.gitignore
Dockerfile
README.md

21
.gitignore vendored Normal file
View File

@ -0,0 +1,21 @@
# IDE
.vscode
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
# Output from 'e2e-testing.sh'
cookie.txt

8
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="Go" enabled="true" />
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

8
.idea/modules.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/docker-mariadb-clean-arch.iml" filepath="$PROJECT_DIR$/.idea/docker-mariadb-clean-arch.iml" />
</modules>
</component>
</project>

30
Dockerfile Normal file
View File

@ -0,0 +1,30 @@
# Get Go image from DockerHub.
FROM golang:1.16.6 AS api
# Set working directory.
WORKDIR /compiler
# Copy dependency locks so we can cache.
COPY go.mod go.sum .
# Get all of our dependencies.
RUN go mod download
# Copy all of our remaining application.
COPY . .
# Build our application.
RUN CGO_ENABLED=0 GOOS=linux go build -o docker-mariadb-clean-arch ./cmd/docker-mariadb-clean-arch/main.go
# Use 'scratch' image for super-mini build.
FROM scratch AS prod
# Set working directory for this stage.
WORKDIR /production
# Copy our compiled executable from the last stage.
COPY --from=api /compiler/docker-mariadb-clean-arch .
# Run application and expose port 8080.
EXPOSE 8080
CMD ["./docker-mariadb-clean-arch"]

12
Makefile Normal file
View File

@ -0,0 +1,12 @@
.PHONY: start
start:
docker-compose up -d --build
.PHONY: stop
stop:
docker-compose rm -v --force --stop
docker image rm docker-mariadb-clean-arch:latest
.PHONY: test
test:
sh ./scripts/e2e-testing.sh

193
README.md Normal file
View File

@ -0,0 +1,193 @@
# Docker MariaDB Clean Architecture
A slightly complex REST application with Fiber to showcase Clean Architecture with MariaDB as a dependency with Docker.
## Prerequisites
- Docker Compose for running the application.
- Shell that supports `sh`, `make`, and `curl` for end-to-end testing. UNIX systems or WSL should work fine.
- Postman if you want to test this API with GUI.
## Application
This application is a slightly complex example of a REST API that have four major endpoints. A public user can access the `User`, `Auth`, and `Misc` major endpoints, but they cannot access the `City` endpoint (as it is protected). If one wants to access said endpoint, they have to log in first via the `Auth` endpoint, and only after that they can access the `City` endpoint.
This application uses MariaDB as a database (dockerized), and JWT as an authentication mechanism. This application also showcases how to perform 1-to-many relational mapping in Clean Architecture (one user can have multiple cities), and also the implementation of `JOIN` SQL clause in Go in general.
## Clean Architecture
![Clean Architecture](./assets/CleanArchitecture.jpg)
Clean Architecture is a concept introduced by Robert C. Martin or also known as Uncle Bob. Simply put, the purpose of this architecture is to perform complete separation of concerns. Systems made this way can be independent of frameworks, testable (easy to write unit tests), independent of UI, independent of database, and independent of any external agency. When you use this architecture, it is simple to change the UI, the database, or the business logic.
One thing that you should keep in mind when using this architecture is about Dependency Rule. In Clean Architecture, source code dependency can only point inwards. This means that the 'inner circle' of the system cannot know at all about the outside world. For example, in the diagram above, use-cases knows about entities, but entities cannot know about use-cases. Data formats used in outer circle should not be used by an inner circle.
Because of this, when you change something that is located the innermost of the circle (entities for example), usually you have to change the outer circles. However, if you change something that is not the innermost of the circle (controllers for example), you do not need to change the use-cases and the entities (you may have to change the frameworks and drivers as they are dependent on each other).
If you want to learn more about Clean Architecture, please see the articles that I have attached below as references.
## System Architecture
For the sake of clearness, here is the diagram that showcases the system architecture of this API.
![System Architecture](./assets/SystemArchitecture.png)
Please refer to below table for terminologies / filenames for each layers that are used in this application. The project structure is referred from [this project](https://github.com/golang-standards/project-layout). In the `internal` package, there are packages that are grouped according to their functional responsibilities. If you open the package, you will see the files that represents the Clean Architecture layers.
For the dependency graph, it is straightforward: handler/middleware depends on service, service depends on repository, and repository depends on domain and the database (via dependency injection). All of the layers are implemented with the said infrastructure (Fiber, MariaDB, and Authentication Service) in above image.
I have slightly modified the layers in this application to conform to my own taste of Clean Architecture.
| Architecture Layer | Equivalent Layer | Filename |
| :-----------------: | :--------------------: | :------------------------------: |
| External Interfaces | Presenters and Drivers | `middleware.go` and `handler.go` |
| Controllers | Business Logic | `service.go` |
| Use Cases | Repositories | `repository.go` |
| Entities | Entities | `domain.go` |
Basically, a request will have to go through `handler.go` (and `middleware.go`) first. After that, the program will call a repository or a use-case that is requested with `service.go`. That controller (`service.go`) will call `repository.go` that conforms to the `domain.go` in order to fulfill the request that the `service.go` asked for. The result of the request will be returned back to the user by `handler.go`.
In short:
- `handler.go` and `middleware.go` is used to receive and send requests.
- `service.go` is business-logic or controller (some might have different opinions, but this is my subjective opinion).
- `repository.go` is used to interact to the database (use-case).
- `domain.go` is the 'shape' of the data models that the program use.
For the sake of completeness, here are the functional responsibilities of the project structure.
- `internal/auth` is used to manage authentication.
- `internal/city` is used to manage cities. This endpoint **is protected**.
- `internal/infrastructure` is used to manage infrastructure of the application, such as MariaDB and Fiber.
- `internal/misc` is used to manage miscellaneous endpoints.
- `internal/user` is used to manage users. This endpoint is **not protected**.
Please refer to the code itself for further details. I commented everything in the code, so I hope it is clear enough!
## API Endpoints / Features
This API is divided into four 'major endpoints', which are miscellaneous, users, authentication, and cities.
### Miscellaneous
Endpoints classified here are miscellaneous endpoints.
- `GET /api/v1` for health check.
### Users
Endpoints classified here are endpoints to perform operation on 'User' domain.
- `GET /api/v1/users` to get all users.
- `POST /api/v1/users` to create a user.
- `GET /api/v1/users/<userID>` to get a user.
- `PUT /api/v1/users/<userID>` to update a user.
- `DELETE /api/v1/users/<userID>` to delete a user.
### Authentication
Endpoints classified here are endpoints to perform authentication. In my opinion, this is framework-layer / implementation detail, so there is no 'domain' regarding this endpoint and you can use this endpoint as an enhancement to other endpoints. Authentication in this API is done using JSON Web Tokens.
- `POST /api/v1/auth/login` to log in as the user with ID of 1 in the database. Will return JWT and said JWT will be stored in a cookie.
- `POST /api/v1/auth/logout` to log out. This route removes the JWT from the cookie.
- `GET /api/v1/auth/private` to access a private route which displays information about the current (valid) JWT.
### Cities
Endpoints classified here are endpoints to perform operation on `City` domain. **Endpoints here are protected via JWT in the cookie**, so if you are going to use this endpoint, make sure you are logged in first (or at least have a valid JWT).
- `GET /api/v1/cities` to get all cities.
- `POST /api/v1/cities` to create a new city.
- `GET /api/v1/cities/<cityID>` to get a city.
- `PUT /api/v1/cities/<cityID>` to update a city.
- `DELETE /api/v1/cities/<cityID>` to delete a city.
## Installation
In order to run this application, you just need to do the following commands.
- Clone the repository.
```bash
git clone git@github.com:gofiber/recipes.git
```
- Switch to this repository.
```bash
cd recipes/docker-mariadb-clean-arch
```
- Run immediately with Docker. After you run this command, migration script will be automatically run to populate your dockerized MariaDB.
```bash
make start
```
- Test with Postman (set the request URL to `localhost:8080`) or with the created end-to-end testing script. Keep in mind that the end-to-end script is only available for the first run. If you are trying to run it the second time, you might not be able to get all of the perfect results (because of the auto-increment in the MariaDB). Please run `make stop` and `make start` first if you want to run the test suite again.
```bash
make test
```
- Teardown or stop the container. This will also delete the Docker volume created and will also delete the created image.
```bash
make stop
```
You're done!
## FAQ
Some frequently asked questions that I found scattered on the Internet. Keep in mind that the answers are mostly subjective.
**Q: Is this the right way to do Clean Architecture?**
A: Nope. There are many ways to perform clean architecture - this example being one of them. Some projects might be better than this example.
**Q: Why is authentication an implementation detail?**
A: Authentication is an implementation detail because it does not interact with the use-case or the repository / interface layer. Authentication is a bit strange that it can be implemented in any other routes as a middleware. Keep in mind that this is my subjective opinion.
**Q: Is this the recommended way to structure Fiber projects?**
A: Nope. Just like any other Gophers, I recommend you to start your project by using a single `main.go` file. Some projects do not require complicated architectures. After you start seeing the need to branch out, I recommend you to [split your code based on functional responsibilities](https://rakyll.org/style-packages/). If you need an even more strict structure, then you can try to adapt Clean Architecture or any other architectures that you see fit, such as Onion, Hexagonal, etcetera.
**Q: Is this only for Fiber?**
A: Nope. You can simply adjust `handler.go` and `middleware.go` files in order to change the external interfaces / presenters and drivers layer to something else. You can use `net/http`, `gin-gonic`, `echo`, and many more. If you want to change or add your database, you just need to adjust the `repository.go` file accordingly. If you want to change your business logic, simply change the `service.go` file. As long as you the separation of concerns is done well, you should have no need to change a lot of things.
**Q: Is this production-ready?**
A: I try to make this as production-ready as possible 😉
## Improvements
Several further improvements that could be implemented in this project:
- Add more tests and mocks, especially unit tests (Clean Architecture is the best for performing unit tests).
- Add more API endpoints.
- Add a caching mechanism to the repository layer, such as Redis.
- Add transaction support.
- Maybe try to integrate S3 backend to the repository layer (MinIO is a good choice).
- Maybe add a `domain` folder in the `internal` package where we can leave the entities there?
## Discussion
Feel free to create an issue in this repository (or maybe ask in Fiber's Discord Server) in order to discuss this together!
## References
Thanks to articles and their writers that I have read and found inspiration in!
- [Clean Architecture by Angad Sharma](https://medium.com/gdg-vit/clean-architecture-the-right-way-d83b81ecac6)
- [Clean Architecture by Uncle Bob](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)
- [Clean Architecture with Go by Elton Minetto](https://dev.to/eminetto/clean-architecture-using-golang-5791)
- [Clean Architecture with Go Part 2 by Elton Minetto](https://dev.to/eminetto/clean-architecture-2-years-later-4een)
- [Creating Clean Architecture using Go by @namkount](https://hackernoon.com/creating-clean-architecture-using-golang-9h5i3wgr)
- [Dive to Clean Architecture with Go by Kenta Takeuchi](https://dev.to/bmf_san/dive-to-clean-architecture-with-golang-cd4)
- [Go and Clean Architecture by Reshef Sharvit](https://itnext.io/golang-and-clean-architecture-19ae9aae5683)
- [Go Microservices with Clean Architecture by Jin Feng](https://medium.com/@jfeng45/go-microservice-with-clean-architecture-application-design-68f48802c8f)
- [Go Project Layout Repository](https://github.com/golang-standards/project-layout)
- [Trying Clean Architecture on Go by Imam Tumorang](https://hackernoon.com/golang-clean-archithecture-efd6d7c43047)

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@ -0,0 +1,7 @@
package main
import "bill-go-fiber/internal/infrastructure"
func main() {
infrastructure.Run()
}

36
docker-compose.yml Normal file
View File

@ -0,0 +1,36 @@
version: "3.9"
services:
fiber-application:
restart: always
image: docker-mariadb-clean-arch:latest
build:
context: .
dockerfile: Dockerfile
target: prod
ports:
- 8080:8080
environment:
- API_USERID=1
- API_USERNAME=fiber
- API_PASSWORD=fiber
- JWT_SECRET=fiber
networks:
- application
depends_on:
- mariadb
command: ./docker-mariadb-clean-arch
mariadb:
image: mariadb:10.6.3
restart: always
volumes:
- ./scripts/migrations.sql:/docker-entrypoint-initdb.d/1.sql
expose:
- 3306
environment:
- MARIADB_ALLOW_EMPTY_ROOT_PASSWORD=true
networks:
- application
networks:
application:

29
go.mod Normal file
View File

@ -0,0 +1,29 @@
module bill-go-fiber
go 1.20
require (
github.com/go-sql-driver/mysql v1.7.1
github.com/gofiber/fiber/v2 v2.44.0
github.com/gofiber/jwt/v2 v2.2.7
github.com/golang-jwt/jwt v3.2.2+incompatible
)
require (
github.com/andybalholm/brotli v1.0.5 // indirect
github.com/golang-jwt/jwt/v4 v4.0.0 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/klauspost/compress v1.16.3 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.18 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/philhofer/fwd v1.1.2 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94 // indirect
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect
github.com/tinylib/msgp v1.1.8 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.45.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
golang.org/x/sys v0.7.0 // indirect
)

108
go.sum Normal file
View File

@ -0,0 +1,108 @@
github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/gofiber/fiber/v2 v2.17.0/go.mod h1:iftruuHGkRYGEXVISmdD7HTYWyfS2Bh+Dkfq4n/1Owg=
github.com/gofiber/fiber/v2 v2.44.0 h1:Z90bEvPcJM5GFJnu1py0E1ojoerkyew3iiNJ78MQCM8=
github.com/gofiber/fiber/v2 v2.44.0/go.mod h1:VTMtb/au8g01iqvHyaCzftuM/xmZgKOZCtFzz6CdV9w=
github.com/gofiber/jwt/v2 v2.2.7 h1:MgXZV+ak+FiRVepD3btHBxWcyxlFzTDGXJv78dU1sIE=
github.com/gofiber/jwt/v2 v2.2.7/go.mod h1:yaOHLccYXJidk1HX/EiIdIL+Z1xmY2wnIv6hgViw384=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-jwt/jwt/v4 v4.0.0 h1:RAqyYixv1p7uEnocuy8P1nru5wprCh/MH2BIlW5z5/o=
github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/klauspost/compress v1.12.2/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
github.com/klauspost/compress v1.16.3 h1:XuJt9zzcnaz6a16/OU53ZjWp/v7/42WcR5t2a0PcNQY=
github.com/klauspost/compress v1.16.3/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw=
github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94 h1:rmMl4fXJhKMNWl+K+r/fq4FbbKI+Ia2m9hYBLm2h4G4=
github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94/go.mod h1:90zrgN3D/WJsDd1iXHT96alCoN2KJo6/4x1DZC3wZs8=
github.com/savsgio/gotils v0.0.0-20220530130905-52f3993e8d6d/go.mod h1:Gy+0tqhJvgGlqnTF8CVGP0AaGRjwBtXs/a5PA0Y3+A4=
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk=
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g=
github.com/tinylib/msgp v1.1.6/go.mod h1:75BAfg2hauQhs3qedfdDZmWAPcFMAvJE5b9rGOMufyw=
github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0=
github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.26.0/go.mod h1:cmWIqlu99AO/RKcp1HWaViTqc57FswJOfYYdPJBl8BA=
github.com/valyala/fasthttp v1.45.0 h1:zPkkzpIn8tdHZUrVa6PzYd0i5verqiPSkgTd3bSUcpA=
github.com/valyala/fasthttp v1.45.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA=
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI=
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.4.0 h1:7mTAgkunk3fr4GAloyyCasadO6h9zSsQZbwvcaIciV4=
golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

136
internal/auth/handler.go Normal file
View File

@ -0,0 +1,136 @@
package auth
import (
"os"
"time"
"github.com/gofiber/fiber/v2"
"github.com/golang-jwt/jwt"
)
// Create an authentication handler. Leave this empty, as we have no domains nor use-cases.
// In my opinion, authentication is an implementation detail (framework layer).
type AuthHandler struct{}
// Creates a new authentication handler.
func NewAuthHandler(authRoute fiber.Router) {
handler := &AuthHandler{}
// Declare routing for specific routes.
authRoute.Post("/login", handler.signInUser)
authRoute.Post("/logout", handler.signOutUser)
authRoute.Get("/private", JWTMiddleware(), handler.privateRoute)
}
// Signs in a user and gives them a JWT.
func (h *AuthHandler) signInUser(c *fiber.Ctx) error {
// Create a struct so the request body can be mapped here.
type loginRequest struct {
Username string `json:"username"`
Password string `json:"password"`
}
// Create a struct for our custom JWT payload.
type jwtClaims struct {
UserID string `json:"uid"`
User string `json:"user"`
jwt.StandardClaims
}
// Get request body.
request := &loginRequest{}
if err := c.BodyParser(request); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(&fiber.Map{
"status": "fail",
"message": err.Error(),
})
}
// If both username and password are incorrect, do not allow access.
if request.Username != os.Getenv("API_USERNAME") || request.Password != os.Getenv("API_PASSWORD") {
return c.Status(fiber.StatusUnauthorized).JSON(&fiber.Map{
"status": "fail",
"message": "Wrong username or password!",
})
}
// Send back JWT as a cookie.
token := jwt.NewWithClaims(jwt.SigningMethodHS256, &jwtClaims{
os.Getenv("API_USERID"),
os.Getenv("API_USERNAME"),
jwt.StandardClaims{
Audience: "bill-go-fiber-users",
ExpiresAt: time.Now().Add(time.Hour * 24).Unix(),
Issuer: "bill-go-fiber",
},
})
signedToken, err := token.SignedString([]byte(os.Getenv("JWT_SECRET")))
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(&fiber.Map{
"status": "fail",
"message": err.Error(),
})
}
c.Cookie(&fiber.Cookie{
Name: "jwt",
Value: signedToken,
Path: "/",
Expires: time.Now().Add(time.Hour * 24),
Secure: false,
HTTPOnly: true,
})
// Send also response.
return c.Status(fiber.StatusOK).JSON(&fiber.Map{
"status": "success",
"token": signedToken,
})
}
// Logs out user and removes their JWT.
func (h *AuthHandler) signOutUser(c *fiber.Ctx) error {
c.Cookie(&fiber.Cookie{
Name: "jwt",
Value: "loggedOut",
Path: "/",
Expires: time.Now().Add(time.Second * 10),
Secure: false,
HTTPOnly: true,
})
return c.Status(fiber.StatusOK).JSON(&fiber.Map{
"status": "success",
"message": "Logged out successfully!",
})
}
// A single private route that only logged in users can access.
func (h *AuthHandler) privateRoute(c *fiber.Ctx) error {
// Give form to our output response.
type jwtResponse struct {
UserID interface{} `json:"uid"`
User interface{} `json:"user"`
Iss interface{} `json:"iss"`
Aud interface{} `json:"aud"`
Exp interface{} `json:"exp"`
}
// Prepare our variables to be displayed.
jwtData := c.Locals("user").(*jwt.Token)
claims := jwtData.Claims.(jwt.MapClaims)
// Shape output response.
jwtResp := &jwtResponse{
UserID: claims["uid"],
User: claims["user"],
Iss: claims["iss"],
Aud: claims["aud"],
Exp: claims["exp"],
}
return c.Status(fiber.StatusOK).JSON(&fiber.Map{
"status": "success",
"message": "Welcome to the private route!",
"jwtData": jwtResp,
})
}

View File

@ -0,0 +1,54 @@
package auth
import (
"os"
"strconv"
"github.com/gofiber/fiber/v2"
jwtware "github.com/gofiber/jwt/v2"
"github.com/golang-jwt/jwt"
)
// JWT error message.
func jwtError(c *fiber.Ctx, err error) error {
if err.Error() == "Missing or malformed JWT" {
return c.Status(fiber.StatusBadRequest).JSON(&fiber.Map{
"status": "error",
"message": "Missing or malformed JWT!",
})
}
return c.Status(fiber.StatusUnauthorized).JSON(&fiber.Map{
"status": "error",
"message": "Invalid or expired JWT!",
})
}
// Guards a specific endpoint in the API.
func JWTMiddleware() fiber.Handler {
return jwtware.New(jwtware.Config{
ErrorHandler: jwtError,
SigningKey: []byte(os.Getenv("JWT_SECRET")),
SigningMethod: "HS256",
TokenLookup: "cookie:jwt",
})
}
// Gets user data (their ID) from the JWT middleware. Should be executed after calling 'JWTMiddleware()'.
func GetDataFromJWT(c *fiber.Ctx) error {
// Get userID from the previous route.
jwtData := c.Locals("user").(*jwt.Token)
claims := jwtData.Claims.(jwt.MapClaims)
parsedUserID := claims["uid"].(string)
userID, err := strconv.Atoi(parsedUserID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(&fiber.Map{
"status": "fail",
"message": err.Error(),
})
}
// Go to next.
c.Locals("currentUser", userID)
return c.Next()
}

47
internal/city/domain.go Normal file
View File

@ -0,0 +1,47 @@
package city
import (
"bill-go-fiber/internal/user"
"context"
)
// Represents 'cities' object.
type City struct {
ID int `json:"id"`
Name string `json:"name"`
Created int64 `json:"created"`
Modified int64 `json:"modified"`
User int `json:"user"`
}
// Represents our 1-to-many relationship, User to Cities.
// In other words, a single user can have many cities.
// This is used for the presentation layer only.
type CityAndUser struct {
ID int `json:"id"`
Name string `json:"name"`
Created int64 `json:"created"`
Modified int64 `json:"modified"`
User user.User `json:"user"`
}
// Our repository will implement these methods.
type CityRepository interface {
GetCities(ctx context.Context) (*[]CityAndUser, error)
GetCity(ctx context.Context, cityID int) (*CityAndUser, error)
CreateCity(ctx context.Context, city *City) error
UpdateCity(ctx context.Context, cityID int, city *City) error
DeleteCity(ctx context.Context, cityID int) error
}
// Our use-case or service will implement these methods.
// Method names does not matter - I intentionally changed the names here so they are different from the repository.
// Usually, 'services' should be as close as possible to a business use-case / problem.
// Some methods will also take 'userID' as an argument - because we need it to fill the 'User' value in the struct.
type CityService interface {
FetchCities(ctx context.Context) (*[]CityAndUser, error)
FetchCity(ctx context.Context, cityID int) (*CityAndUser, error)
BuildCity(ctx context.Context, city *City, userID int) error
ModifyCity(ctx context.Context, cityID int, city *City, userID int) error
DestroyCity(ctx context.Context, cityID int) error
}

178
internal/city/handler.go Normal file
View File

@ -0,0 +1,178 @@
package city
import (
"bill-go-fiber/internal/auth"
"context"
"github.com/gofiber/fiber/v2"
)
// We will inject our dependency - the service - here.
type CityHandler struct {
cityService CityService
}
// Creates a new handler.
func NewCityHandler(cityRoute fiber.Router, cs CityService) {
// Create a handler based on our created service / use-case.
handler := &CityHandler{
cityService: cs,
}
// We will restrict this route with our JWT middleware.
// You can inject other middlewares if you see fit here.
cityRoute.Use(auth.JWTMiddleware(), auth.GetDataFromJWT)
// Routing for general routes.
cityRoute.Get("", handler.getCities)
cityRoute.Post("", handler.createCity)
// Routing for specific routes.
cityRoute.Get("/:cityID", handler.getCity)
cityRoute.Put("/:cityID", handler.checkIfCityExistsMiddleware, handler.updateCity)
cityRoute.Delete("/:cityID", handler.checkIfCityExistsMiddleware, handler.deleteCity)
}
// Handler to get all cities.
func (h *CityHandler) getCities(c *fiber.Ctx) error {
// Create cancellable context.
customContext, cancel := context.WithCancel(context.Background())
defer cancel()
// Get all cities.
cities, err := h.cityService.FetchCities(customContext)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(&fiber.Map{
"status": "fail",
"message": err.Error(),
})
}
// Return results.
return c.Status(fiber.StatusOK).JSON(&fiber.Map{
"status": "success",
"data": cities,
})
}
// Get one city.
func (h *CityHandler) getCity(c *fiber.Ctx) error {
// Create cancellable context.
customContext, cancel := context.WithCancel(context.Background())
defer cancel()
// Fetch parameter.
targetedCityID, err := c.ParamsInt("cityID")
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(&fiber.Map{
"status": "fail",
"message": "Please specify a valid city ID!",
})
}
// Get one city.
city, err := h.cityService.FetchCity(customContext, targetedCityID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(&fiber.Map{
"status": "fail",
"message": err.Error(),
})
}
// Return results.
return c.Status(fiber.StatusOK).JSON(&fiber.Map{
"status": "success",
"data": city,
})
}
// Creates a single city.
func (h *CityHandler) createCity(c *fiber.Ctx) error {
// Initialize variables.
city := &City{}
currentUserID := c.Locals("currentUser").(int)
// Create cancellable context.
customContext, cancel := context.WithCancel(context.Background())
defer cancel()
// Parse request body.
if err := c.BodyParser(city); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(&fiber.Map{
"status": "fail",
"message": err.Error(),
})
}
// Create one city.
err := h.cityService.BuildCity(customContext, city, currentUserID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(&fiber.Map{
"status": "fail",
"message": err.Error(),
})
}
// Return result.
return c.Status(fiber.StatusCreated).JSON(&fiber.Map{
"status": "success",
"message": "City has been created successfully!",
})
}
// Updates a single city.
func (h *CityHandler) updateCity(c *fiber.Ctx) error {
// Initialize variables.
city := &City{}
currentUserID := c.Locals("currentUser").(int)
targetedCityID := c.Locals("cityID").(int)
// Create cancellable context.
customContext, cancel := context.WithCancel(context.Background())
defer cancel()
// Parse request body.
if err := c.BodyParser(city); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(&fiber.Map{
"status": "fail",
"message": err.Error(),
})
}
// Update one city.
err := h.cityService.ModifyCity(customContext, targetedCityID, city, currentUserID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(&fiber.Map{
"status": "fail",
"message": err.Error(),
})
}
// Return result.
return c.Status(fiber.StatusOK).JSON(&fiber.Map{
"status": "success",
"message": "City has been updated successfully!",
})
}
// Deletes a single city.
func (h *CityHandler) deleteCity(c *fiber.Ctx) error {
// Initialize previous city ID.
targetedCityID := c.Locals("cityID").(int)
// Create cancellable context.
customContext, cancel := context.WithCancel(context.Background())
defer cancel()
// Delete one city.
err := h.cityService.DestroyCity(customContext, targetedCityID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(&fiber.Map{
"status": "fail",
"message": err.Error(),
})
}
// Return 204 No Content.
return c.SendStatus(fiber.StatusNoContent)
}

View File

@ -0,0 +1,42 @@
package city
import (
"context"
"github.com/gofiber/fiber/v2"
)
// If city does not exist, do not allow one to access the API.
func (h *CityHandler) checkIfCityExistsMiddleware(c *fiber.Ctx) error {
// Create a new customized context.
customContext, cancel := context.WithCancel(context.Background())
defer cancel()
// Fetch parameter.
targetedCityID, err := c.ParamsInt("cityID")
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(&fiber.Map{
"status": "fail",
"message": "Please specify a valid city ID!",
})
}
// Check if city exists.
searchedCity, err := h.cityService.FetchCity(customContext, targetedCityID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(&fiber.Map{
"status": "fail",
"message": err.Error(),
})
}
if searchedCity == nil {
return c.Status(fiber.StatusBadRequest).JSON(&fiber.Map{
"status": "fail",
"message": "There is no city with this ID!",
})
}
// Store in locals for further processing in the real handler.
c.Locals("cityID", targetedCityID)
return c.Next()
}

158
internal/city/repository.go Normal file
View File

@ -0,0 +1,158 @@
package city
import (
"context"
"database/sql"
)
// Queries that we will use.
const (
QUERY_GET_CITIES = "SELECT c.id, c.name, c.created, c.modified, u.id, u.name, u.address, u.created, u.modified FROM cities AS c JOIN users AS u ON (c.user = u.id)"
QUERY_GET_CITY = "SELECT c.id, c.name, c.created, c.modified, u.id, u.name, u.address, u.created, u.modified FROM cities AS c JOIN users AS u ON (c.user = u.id) WHERE c.id = ?"
QUERY_CREATE_CITY = "INSERT INTO cities (name, created, modified, user) VALUES (?, ?, ?, ?)"
QUERY_UPDATE_CITY = "UPDATE cities SET name = ?, modified = ?, user = ? WHERE id = ?"
QUERY_DELETE_CITY = "DELETE FROM cities WHERE id = ?"
)
// Represents that we will use MariaDB in order to implement the methods.
type mariaDBRepository struct {
mariadb *sql.DB
}
// Create a new repository with MariaDB as the driver.
func NewCityRepository(mariaDBConnection *sql.DB) CityRepository {
return &mariaDBRepository{
mariadb: mariaDBConnection,
}
}
// Gets all cities in the database.
func (r *mariaDBRepository) GetCities(ctx context.Context) (*[]CityAndUser, error) {
// Initialize variables.
var cities []CityAndUser
// Fetches all cities.
res, err := r.mariadb.QueryContext(ctx, QUERY_GET_CITIES)
if err != nil {
return nil, err
}
defer res.Close()
// Scan all of the cities from the results.
for res.Next() {
city := &CityAndUser{}
err = res.Scan(
&city.ID,
&city.Name,
&city.Created,
&city.Modified,
&city.User.ID,
&city.User.Name,
&city.User.Address,
&city.User.Created,
&city.User.Modified,
)
if err != nil && err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
cities = append(cities, *city)
}
return &cities, nil
}
// Gets a single city in the database.
func (r *mariaDBRepository) GetCity(ctx context.Context, cityID int) (*CityAndUser, error) {
// Initialize variable.
city := &CityAndUser{}
// Prepare SQL to get one city.
stmt, err := r.mariadb.PrepareContext(ctx, QUERY_GET_CITY)
if err != nil {
return nil, err
}
defer stmt.Close()
// Get one city and insert it to the 'city' struct.
// If it's empty, return null.
err = stmt.QueryRowContext(ctx, cityID).Scan(
&city.ID,
&city.Name,
&city.Created,
&city.Modified,
&city.User.ID,
&city.User.Name,
&city.User.Address,
&city.User.Created,
&city.User.Modified,
)
if err != nil && err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
// Return result.
return city, nil
}
// Creates a single city in the database.
func (r *mariaDBRepository) CreateCity(ctx context.Context, city *City) error {
// Prepare context to be used.
stmt, err := r.mariadb.PrepareContext(ctx, QUERY_CREATE_CITY)
if err != nil {
return err
}
defer stmt.Close()
// Insert one city.
_, err = stmt.ExecContext(ctx, city.Name, city.Created, city.Modified, city.User)
if err != nil {
return err
}
// Return empty.
return nil
}
// Updates a single city in the database.
func (r *mariaDBRepository) UpdateCity(ctx context.Context, cityID int, city *City) error {
// Prepare context to be used.
stmt, err := r.mariadb.PrepareContext(ctx, QUERY_UPDATE_CITY)
if err != nil {
return err
}
defer stmt.Close()
// Update one city.
_, err = stmt.ExecContext(ctx, city.Name, city.Modified, city.User, cityID)
if err != nil {
return err
}
// Return empty.
return nil
}
// Deletes a single city in the database.
func (r *mariaDBRepository) DeleteCity(ctx context.Context, cityID int) error {
// Prepare context to be used.
stmt, err := r.mariadb.PrepareContext(ctx, QUERY_DELETE_CITY)
if err != nil {
return err
}
defer stmt.Close()
// Delete one city.
_, err = stmt.ExecContext(ctx, cityID)
if err != nil {
return err
}
// Return empty.
return nil
}

50
internal/city/service.go Normal file
View File

@ -0,0 +1,50 @@
package city
import (
"context"
"time"
)
// Implementation of the repository in this service.
type cityService struct {
cityReposiory CityRepository
}
// Create a new 'service' or 'use-case' for 'User' entity.
func NewCityService(r CityRepository) CityService {
return &cityService{
cityReposiory: r,
}
}
// Implementation of 'FetchCities'.
func (s *cityService) FetchCities(ctx context.Context) (*[]CityAndUser, error) {
return s.cityReposiory.GetCities(ctx)
}
// Implementation of 'FetchCity'.
func (s *cityService) FetchCity(ctx context.Context, cityID int) (*CityAndUser, error) {
return s.cityReposiory.GetCity(ctx, cityID)
}
// Implementation of 'BuildCity'.
// Our business logic is to set our default variables here.
func (s *cityService) BuildCity(ctx context.Context, city *City, userID int) error {
city.Created = time.Now().Unix()
city.Modified = time.Now().Unix()
city.User = userID
return s.cityReposiory.CreateCity(ctx, city)
}
// Implementation of 'ModifyCity'.
// Same as above, our business logic is to set our default variables.
func (s *cityService) ModifyCity(ctx context.Context, cityID int, city *City, userID int) error {
city.Modified = time.Now().Unix()
city.User = userID
return s.cityReposiory.UpdateCity(ctx, cityID, city)
}
// Implementation of 'DestroyCity'.
func (s *cityService) DestroyCity(ctx context.Context, cityID int) error {
return s.cityReposiory.DeleteCity(ctx, cityID)
}

View File

@ -0,0 +1,81 @@
package infrastructure
import (
"bill-go-fiber/internal/auth"
"bill-go-fiber/internal/city"
"bill-go-fiber/internal/misc"
"bill-go-fiber/internal/user"
"fmt"
"log"
_ "github.com/go-sql-driver/mysql"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/compress"
"github.com/gofiber/fiber/v2/middleware/cors"
"github.com/gofiber/fiber/v2/middleware/etag"
"github.com/gofiber/fiber/v2/middleware/favicon"
"github.com/gofiber/fiber/v2/middleware/limiter"
"github.com/gofiber/fiber/v2/middleware/logger"
"github.com/gofiber/fiber/v2/middleware/recover"
"github.com/gofiber/fiber/v2/middleware/requestid"
)
// Run our Fiber webserver.
func Run() {
// Try to connect to our database as the initial part.
mariadb, err := ConnectToMariaDB()
if err != nil {
log.Fatal("Database connection error: $s", err)
}
// Creates a new Fiber instance.
app := fiber.New(fiber.Config{
AppName: "Docker MariaDB Clean Arch",
ServerHeader: "Fiber",
})
// Use global middlewares.
app.Use(cors.New())
app.Use(compress.New())
app.Use(etag.New())
app.Use(favicon.New())
app.Use(limiter.New(limiter.Config{
Max: 100,
LimitReached: func(c *fiber.Ctx) error {
return c.Status(fiber.StatusTooManyRequests).JSON(&fiber.Map{
"status": "fail",
"message": "You have requested too many in a single time-frame! Please wait another minute!",
})
},
}))
app.Use(logger.New())
app.Use(recover.New())
app.Use(requestid.New())
// Create repositories.
cityRepository := city.NewCityRepository(mariadb)
userRepository := user.NewUserRepository(mariadb)
// Create all of our services.
cityService := city.NewCityService(cityRepository)
userService := user.NewUserService(userRepository)
// Prepare our endpoints for the API.
misc.NewMiscHandler(app.Group("/api/v1"))
auth.NewAuthHandler(app.Group("/api/v1/auth"))
city.NewCityHandler(app.Group("/api/v1/cities"), cityService)
user.NewUserHandler(app.Group("/api/v1/users"), userService)
// Prepare an endpoint for 'Not Found'.
app.All("*", func(c *fiber.Ctx) error {
errorMessage := fmt.Sprintf("Route '%s' does not exist in this API!", c.OriginalURL())
return c.Status(fiber.StatusNotFound).JSON(&fiber.Map{
"status": "fail",
"message": errorMessage,
})
})
// Listen to port 8080.
log.Fatal(app.Listen(":8080"))
}

View File

@ -0,0 +1,23 @@
package infrastructure
import (
"database/sql"
"time"
)
// This function is used to connect to MariaDB.
func ConnectToMariaDB() (*sql.DB, error) {
// Connect to MariaDB.
db, err := sql.Open("mysql", "root:@tcp(mariadb:3306)/fiber_dmca")
if err != nil {
return nil, err
}
// Set up important parts as was told by the documentation.
db.SetConnMaxLifetime(time.Minute * 3)
db.SetMaxOpenConns(10)
db.SetMaxIdleConns(10)
// Return our database instance.
return db, nil
}

22
internal/misc/handler.go Normal file
View File

@ -0,0 +1,22 @@
package misc
import "github.com/gofiber/fiber/v2"
// Create a handler. Leave this empty, as we have no domains nor use-cases.
type MiscHandler struct{}
// Represents a new handler.
func NewMiscHandler(miscRoute fiber.Router) {
handler := &MiscHandler{}
// Declare routing.
miscRoute.Get("", handler.healthCheck)
}
// Check for the health of the API.
func (h *MiscHandler) healthCheck(c *fiber.Ctx) error {
return c.Status(fiber.StatusOK).JSON(&fiber.Map{
"status": "success",
"message": "Hello World!",
})
}

30
internal/user/domain.go Normal file
View File

@ -0,0 +1,30 @@
package user
import "context"
// Represents the 'User' object.
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Address string `json:"address"`
Created int64 `json:"created"`
Modified int64 `json:"modified"`
}
// Our repository will implement these methods.
type UserRepository interface {
GetUsers(ctx context.Context) (*[]User, error)
GetUser(ctx context.Context, userID int) (*User, error)
CreateUser(ctx context.Context, user *User) error
UpdateUser(ctx context.Context, userID int, user *User) error
DeleteUser(ctx context.Context, userID int) error
}
// Our use-case or service will implement these methods.
type UserService interface {
GetUsers(ctx context.Context) (*[]User, error)
GetUser(ctx context.Context, userID int) (*User, error)
CreateUser(ctx context.Context, user *User) error
UpdateUser(ctx context.Context, userID int, user *User) error
DeleteUser(ctx context.Context, userID int) error
}

171
internal/user/handler.go Normal file
View File

@ -0,0 +1,171 @@
package user
import (
"context"
"github.com/gofiber/fiber/v2"
)
// Represents our handler with our use-case / service.
type UserHandler struct {
userService UserService
}
// Creates a new handler.
func NewUserHandler(userRoute fiber.Router, us UserService) {
// Create a handler based on our created service / use-case.
handler := &UserHandler{
userService: us,
}
// Declare routing endpoints for general routes.
userRoute.Get("", handler.getUsers)
userRoute.Post("", handler.createUser)
// Declare routing endpoints for specific routes.
userRoute.Get("/:userID", handler.getUser)
userRoute.Put("/:userID", handler.checkIfUserExistsMiddleware, handler.updateUser)
userRoute.Delete("/:userID", handler.checkIfUserExistsMiddleware, handler.deleteUser)
}
// Gets all users.
func (h *UserHandler) getUsers(c *fiber.Ctx) error {
// Create cancellable context.
customContext, cancel := context.WithCancel(context.Background())
defer cancel()
// Get all users.
users, err := h.userService.GetUsers(customContext)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(&fiber.Map{
"status": "fail",
"message": err.Error(),
})
}
// Return results.
return c.Status(fiber.StatusOK).JSON(&fiber.Map{
"status": "success",
"data": users,
})
}
// Gets a single user.
func (h *UserHandler) getUser(c *fiber.Ctx) error {
// Create cancellable context.
customContext, cancel := context.WithCancel(context.Background())
defer cancel()
// Fetch parameter.
targetedUserID, err := c.ParamsInt("userID")
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(&fiber.Map{
"status": "fail",
"message": "Please specify a valid user ID!",
})
}
// Get one user.
user, err := h.userService.GetUser(customContext, targetedUserID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(&fiber.Map{
"status": "fail",
"message": err.Error(),
})
}
// Return results.
return c.Status(fiber.StatusOK).JSON(&fiber.Map{
"status": "success",
"data": user,
})
}
// Creates a single user.
func (h *UserHandler) createUser(c *fiber.Ctx) error {
// Initialize variables.
user := &User{}
// Create cancellable context.
customContext, cancel := context.WithCancel(context.Background())
defer cancel()
// Parse request body.
if err := c.BodyParser(user); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(&fiber.Map{
"status": "fail",
"message": err.Error(),
})
}
// Create one user.
err := h.userService.CreateUser(customContext, user)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(&fiber.Map{
"status": "fail",
"message": err.Error(),
})
}
// Return result.
return c.Status(fiber.StatusCreated).JSON(&fiber.Map{
"status": "success",
"message": "User has been created successfully!",
})
}
// Updates a single user.
func (h *UserHandler) updateUser(c *fiber.Ctx) error {
// Initialize variables.
user := &User{}
targetedUserID := c.Locals("userID").(int)
// Create cancellable context.
customContext, cancel := context.WithCancel(context.Background())
defer cancel()
// Parse request body.
if err := c.BodyParser(user); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(&fiber.Map{
"status": "fail",
"message": err.Error(),
})
}
// Update one user.
err := h.userService.UpdateUser(customContext, targetedUserID, user)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(&fiber.Map{
"status": "fail",
"message": err.Error(),
})
}
// Return result.
return c.Status(fiber.StatusOK).JSON(&fiber.Map{
"status": "success",
"message": "User has been updated successfully!",
})
}
// Deletes a single user.
func (h *UserHandler) deleteUser(c *fiber.Ctx) error {
// Initialize previous user ID.
targetedUserID := c.Locals("userID").(int)
// Create cancellable context.
customContext, cancel := context.WithCancel(context.Background())
defer cancel()
// Delete one user.
err := h.userService.DeleteUser(customContext, targetedUserID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(&fiber.Map{
"status": "fail",
"message": err.Error(),
})
}
// Return 204 No Content.
return c.SendStatus(fiber.StatusNoContent)
}

View File

@ -0,0 +1,42 @@
package user
import (
"context"
"github.com/gofiber/fiber/v2"
)
// If user does not exist, do not allow one to access the API.
func (h *UserHandler) checkIfUserExistsMiddleware(c *fiber.Ctx) error {
// Create a new customized context.
customContext, cancel := context.WithCancel(context.Background())
defer cancel()
// Fetch parameter.
targetedUserID, err := c.ParamsInt("userID")
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(&fiber.Map{
"status": "fail",
"message": "Please specify a valid user ID!",
})
}
// Check if user exists.
searchedUser, err := h.userService.GetUser(customContext, targetedUserID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(&fiber.Map{
"status": "fail",
"message": err.Error(),
})
}
if searchedUser == nil {
return c.Status(fiber.StatusBadRequest).JSON(&fiber.Map{
"status": "fail",
"message": "There is no user with this ID!",
})
}
// Store in locals for further processing in the real handler.
c.Locals("userID", targetedUserID)
return c.Next()
}

140
internal/user/repository.go Normal file
View File

@ -0,0 +1,140 @@
package user
import (
"context"
"database/sql"
)
// Queries that we will use.
const (
QUERY_GET_USERS = "SELECT * FROM users"
QUERY_GET_USER = "SELECT * FROM users WHERE id = ?"
QUERY_CREATE_USER = "INSERT INTO users (name, address, created, modified) VALUES (?, ?, ?, ?)"
QUERY_UPDATE_USER = "UPDATE users SET name = ?, address = ?, modified = ? WHERE id = ?"
QUERY_DELETE_USER = "DELETE FROM users WHERE id = ?"
)
// Represents that we will use MariaDB in order to implement the methods.
type mariaDBRepository struct {
mariadb *sql.DB
}
// Create a new repository with MariaDB as the driver.
func NewUserRepository(mariaDBConnection *sql.DB) UserRepository {
return &mariaDBRepository{
mariadb: mariaDBConnection,
}
}
// Gets all users in the database.
func (r *mariaDBRepository) GetUsers(ctx context.Context) (*[]User, error) {
// Initialize variables.
var users []User
// Get all users.
res, err := r.mariadb.QueryContext(ctx, QUERY_GET_USERS)
if err != nil {
return nil, err
}
defer res.Close()
// Scan all of the results to the 'users' array.
// If it's empty, return null.
for res.Next() {
user := &User{}
err = res.Scan(&user.ID, &user.Name, &user.Address, &user.Created, &user.Modified)
if err != nil && err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
users = append(users, *user)
}
// Return all of our users.
return &users, nil
}
// Gets a single user in the database.
func (r *mariaDBRepository) GetUser(ctx context.Context, userID int) (*User, error) {
// Initialize variable.
user := &User{}
// Prepare SQL to get one user.
stmt, err := r.mariadb.PrepareContext(ctx, QUERY_GET_USER)
if err != nil {
return nil, err
}
defer stmt.Close()
// Get one user and insert it to the 'user' struct.
// If it's empty, return null.
err = stmt.QueryRowContext(ctx, userID).Scan(&user.ID, &user.Name, &user.Address, &user.Created, &user.Modified)
if err != nil && err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
// Return result.
return user, nil
}
// Creates a single user in the database.
func (r *mariaDBRepository) CreateUser(ctx context.Context, user *User) error {
// Prepare context to be used.
stmt, err := r.mariadb.PrepareContext(ctx, QUERY_CREATE_USER)
if err != nil {
return err
}
defer stmt.Close()
// Insert one user.
_, err = stmt.ExecContext(ctx, user.Name, user.Address, user.Created, user.Modified)
if err != nil {
return err
}
// Return empty.
return nil
}
// Updates a single user in the database.
func (r *mariaDBRepository) UpdateUser(ctx context.Context, userID int, user *User) error {
// Prepare context to be used.
stmt, err := r.mariadb.PrepareContext(ctx, QUERY_UPDATE_USER)
if err != nil {
return err
}
defer stmt.Close()
// Update one user.
_, err = stmt.ExecContext(ctx, user.Name, user.Address, user.Modified, userID)
if err != nil {
return err
}
// Return empty.
return nil
}
// Deletes a single user in the database.
func (r *mariaDBRepository) DeleteUser(ctx context.Context, userID int) error {
// Prepare context to be used.
stmt, err := r.mariadb.PrepareContext(ctx, QUERY_DELETE_USER)
if err != nil {
return err
}
defer stmt.Close()
// Delete one user.
_, err = stmt.ExecContext(ctx, userID)
if err != nil {
return err
}
// Return empty.
return nil
}

52
internal/user/service.go Normal file
View File

@ -0,0 +1,52 @@
package user
import (
"context"
"time"
)
// Implementation of the repository in this service.
type userService struct {
userRepository UserRepository
}
// Create a new 'service' or 'use-case' for 'User' entity.
func NewUserService(r UserRepository) UserService {
return &userService{
userRepository: r,
}
}
// Implementation of 'GetUsers'.
func (s *userService) GetUsers(ctx context.Context) (*[]User, error) {
return s.userRepository.GetUsers(ctx)
}
// Implementation of 'GetUser'.
func (s *userService) GetUser(ctx context.Context, userID int) (*User, error) {
return s.userRepository.GetUser(ctx, userID)
}
// Implementation of 'CreateUser'.
func (s *userService) CreateUser(ctx context.Context, user *User) error {
// Set default value of 'Created' and 'Modified'.
user.Created = time.Now().Unix()
user.Modified = time.Now().Unix()
// Pass to the repository layer.
return s.userRepository.CreateUser(ctx, user)
}
// Implementation of 'UpdateUser'.
func (s *userService) UpdateUser(ctx context.Context, userID int, user *User) error {
// Set value for 'Modified' attribute.
user.Modified = time.Now().Unix()
// Pass to the repository layer.
return s.userRepository.UpdateUser(ctx, userID, user)
}
// Implementation of 'DeleteUser'.
func (s *userService) DeleteUser(ctx context.Context, userID int) error {
return s.userRepository.DeleteUser(ctx, userID)
}

84
scripts/e2e-testing.sh Normal file
View File

@ -0,0 +1,84 @@
#!/bin/bash
# Declare variables.
API_URL=http://localhost:8080
# Introduction to the script.
echo "Welcome to 'docker-mariadb-clean-arch' application!"
echo "Before running the end-to-end tests, please ensure that you have run 'make start'!"; echo
# Testing '/api/v1'.
echo
echo "Running end-to-end testing..."
echo "Testing GET route '/api/v1'..."
curl $API_URL/api/v1; echo
# Testing '/api/v1/users'.
echo
echo "Testing GET route '/api/v1/users'..."
curl $API_URL/api/v1/users; echo
echo
echo "Testing POST route '/api/v1/users'..."
curl -X POST -H 'Content-Type: application/json' -d '{"name":"Lucy Heartfilia","address":"Shinhotaka, Japan"}' $API_URL/api/v1/users; echo
# Testing '/api/v1/users/:userID'.
echo
echo "Using 'userID' with value of 11 (the one created beforehand)."
echo "Testing GET route '/api/v1/users/:userID'..."
curl $API_URL/api/v1/users/11; echo
echo
echo "Testing PUT route '/api/v1/users/:userID'..."
curl -X PUT -H 'Content-Type: application/json' -d '{"name":"Mirajane Strauss","address":"Osaka, Japan"}' $API_URL/api/v1/users/11; echo
echo
echo "Testing GET route after PUT '/api/v1/users/:userID'..."
curl $API_URL/api/v1/users/11; echo
echo
echo "Testing DELETE route '/api/v1/users/:userID'..."
curl -X DELETE $API_URL/api/v1/users/11; echo
echo
echo "Testing GET route after DELETE '/api/v1/users/:userID'..."
curl $API_URL/api/v1/users/11; echo
# Testing '/api/v1/auth/login'.
echo
echo "Testing POST route '/api/v1/auth/login'..."
curl -X POST -H 'Content-Type: application/json' -d '{"username":"fiber","password":"fiber"}' -c cookie.txt $API_URL/api/v1/auth/login; echo
# Testing '/api/v1/auth/private'.
echo
echo "Testing GET route '/api/v1/auth/private'..."
curl -b cookie.txt $API_URL/api/v1/auth/private; echo
# Testing '/api/v1/cities'.
echo
echo "Testing GET route '/api/v1/cities'..."
curl -b cookie.txt $API_URL/api/v1/cities; echo
echo
echo "Testing POST route '/api/v1/cities'..."
curl -b cookie.txt -X POST -H 'Content-Type: application/json' -d '{"name":"Kyoto"}' $API_URL/api/v1/cities; echo
# Testing '/api/v1/cities/:cityID'.
echo
echo "Using 'cityID' with value of 6 (the one created beforehand)."
echo "Testing GET route '/api/v1/cities/:cityID'..."
curl -b cookie.txt $API_URL/api/v1/cities/6; echo
echo
echo "Testing PUT route '/api/v1/cities/:cityID'..."
curl -b cookie.txt -X PUT -H 'Content-Type: application/json' -d '{"name":"Osaka"}' $API_URL/api/v1/cities/6; echo
echo
echo "Testing GET route after PUT '/api/v1/cities/:cityID'..."
curl -b cookie.txt $API_URL/api/v1/cities/6; echo
echo
echo "Testing DELETE route '/api/v1/cities/:cityID'..."
curl -b cookie.txt -X DELETE $API_URL/api/v1/cities/6; echo
echo
echo "Testing GET route after DELETE '/api/v1/cities/:cityID'..."
curl -b cookie.txt $API_URL/api/v1/cities/6; echo
# Testing '/api/v1/auth/logout'.
echo
echo "Testing POST route '/api/v1/auth/logout'..."
curl -X POST $API_URL/api/v1/auth/logout; echo
# Finish end-to-end testing.
rm cookie.txt
echo "Finished testing the application!"

46
scripts/migrations.sql Normal file
View File

@ -0,0 +1,46 @@
-- In this script, 'dmca' stands for 'Docker MariaDB Clean Arch'.
DROP DATABASE IF EXISTS fiber_dmca;
CREATE DATABASE IF NOT EXISTS fiber_dmca;
USE fiber_dmca;
-- Create a sample table.
CREATE TABLE users (
id INT NOT NULL AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
address VARCHAR(255) NOT NULL,
created VARCHAR(255) NOT NULL,
modified VARCHAR(255) NOT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB CHARACTER SET utf8;
-- Populate table with 10 users.
INSERT INTO users VALUES
(1, 'Sayu Ogiwara', 'Hokkaido, Japan', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()),
(2, 'Chizuru Ichinose', 'Tokyo, Japan', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()),
(3, 'Asagi Aiba', 'Kyoto, Japan', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()),
(4, 'Rin Tohsaka', 'Kobe, Japan', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()),
(5, 'Mai Sakurajima', 'Fujisawa, Japan', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()),
(6, 'Aki Adagaki', 'Fukuoka, Japan', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()),
(7, 'Asuna Yuuki', 'Shinagawa, Japan', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()),
(8, 'Ruka Sarashina', 'Gotenba, Japan', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()),
(9, 'Miyuki Shiba', 'Nagano, Japan', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()),
(10, 'Fumino Furuhashi', 'Niigata, Japan', UNIX_TIMESTAMP(), UNIX_TIMESTAMP());
-- Create another sample table.
CREATE TABLE cities (
id INT NOT NULL AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
created VARCHAR(255) NOT NULL,
modified VARCHAR(255) NOT NULL,
user INT NOT NULL,
PRIMARY KEY (id),
FOREIGN KEY (user) REFERENCES users (id)
) ENGINE=InnoDB CHARACTER SET utf8;
-- Create five data samples.
INSERT INTO cities VALUES
(1, 'Hokkaido', UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), 1),
(2, 'Tokyo', UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), 1),
(3, 'Kyoto', UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), 1),
(4, 'Osaka', UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), 2),
(5, 'Fukuoka', UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), 3);