feat: nearly finished
This commit is contained in:
parent
2e7e1c8749
commit
96931551aa
|
@ -13,3 +13,9 @@
|
||||||
|
|
||||||
# Dependency directories (remove the comment below to include it)
|
# Dependency directories (remove the comment below to include it)
|
||||||
vendor/
|
vendor/
|
||||||
|
|
||||||
|
# Environment variable
|
||||||
|
.env
|
||||||
|
|
||||||
|
# Heroku bin directory
|
||||||
|
bin
|
|
@ -6,20 +6,52 @@ Still work in progress
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Install modules
|
# Install modules
|
||||||
$ go install
|
$ go mod download
|
||||||
# or
|
# or
|
||||||
$ go mod vendor
|
$ go mod vendor
|
||||||
|
|
||||||
# run the local server
|
# run the local server
|
||||||
$ go run ./
|
$ go run main.go
|
||||||
|
|
||||||
# build everything
|
# build everything
|
||||||
$ go build ./
|
$ go build main.go
|
||||||
```
|
```
|
||||||
|
|
||||||
## Modules
|
## Used packages
|
||||||
|
|
||||||
- https://github.com/Masterminds/squirrel
|
| Name | Version | Type |
|
||||||
- https://github.com/jackc/pgx
|
| --- | --- | --- |
|
||||||
- https://github.com/go-redis/redis/v8
|
| gofiber/fiber | v2.14.0 | Framework |
|
||||||
- https://github.com/unrolled/secure
|
| jackc/pgx | v4.11.0 | Database |
|
||||||
|
| go-redis/redis | v8.11.0 | Cache |
|
||||||
|
| joho/godotenv | v1.3.0 | Config |
|
||||||
|
| getsentry/sentry-go | v0.11.0 | Logging |
|
||||||
|
| aldy505/phc-crypto | v1.1.0 | Utils |
|
||||||
|
| Masterminds/squirrel | v1.5.0 | Utils |
|
||||||
|
| aldy505/bob | v0.0.1 | Utils |
|
||||||
|
|
||||||
|
## Directory structure
|
||||||
|
|
||||||
|
```
|
||||||
|
└-- /app
|
||||||
|
└---- /v1
|
||||||
|
└---- /handler
|
||||||
|
└---- /middleware folder for add middleware
|
||||||
|
└---- /models
|
||||||
|
└---- /platform
|
||||||
|
└--------- /cache folder with in-memory cache setup functions
|
||||||
|
└--------- /database folder with database setup functions
|
||||||
|
└---- /routes folder for describe routes
|
||||||
|
└---- /utils folder with utility functions
|
||||||
|
```
|
||||||
|
## `.env` configuration
|
||||||
|
|
||||||
|
```ini
|
||||||
|
ENV=development
|
||||||
|
PORT=5000
|
||||||
|
|
||||||
|
DATABASE_URL=postgres://postgres:password@localhost:5432/jokesbapak2
|
||||||
|
REDIS_URL=redis://@localhost:6379
|
||||||
|
|
||||||
|
SENTRY_DSN=
|
||||||
|
```
|
|
@ -0,0 +1,18 @@
|
||||||
|
package v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/aldy505/jokes-bapak2-api/api/app/v1/routes"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func New() *fiber.App {
|
||||||
|
app := fiber.New(fiber.Config{
|
||||||
|
DisableKeepalive: true,
|
||||||
|
CaseSensitive: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
routes.Health(app)
|
||||||
|
routes.Joke(app)
|
||||||
|
|
||||||
|
return app
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
package handler
|
||||||
|
|
||||||
|
import "github.com/gofiber/fiber/v2"
|
||||||
|
|
||||||
|
func Health(c *fiber.Ctx) error {
|
||||||
|
return c.SendStatus(fiber.StatusNoContent)
|
||||||
|
}
|
|
@ -8,7 +8,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func AddNewJoke(c *fiber.Ctx) error {
|
func AddNewJoke(c *fiber.Ctx) error {
|
||||||
var body models.JokePost
|
var body models.RequestJokePost
|
||||||
err := c.BodyParser(&body)
|
err := c.BodyParser(&body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -24,7 +24,7 @@ func AddNewJoke(c *fiber.Ctx) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.Status(fiber.StatusCreated).JSON(fiber.Map{
|
return c.Status(fiber.StatusCreated).JSON(models.ResponseJoke{
|
||||||
"link": body.Link,
|
Link: body.Link,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +1,43 @@
|
||||||
package handler
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/Masterminds/squirrel"
|
||||||
|
"github.com/aldy505/jokes-bapak2-api/api/app/v1/models"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func DeleteJoke(c *fiber.Ctx) error {
|
||||||
|
id := c.Params("id")
|
||||||
|
|
||||||
|
// Check if the joke exists
|
||||||
|
sql, args, err := psql.Select("id").From("jokesbapak2").Where(squirrel.Eq{"id": id}).ToSql()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var jokeID string
|
||||||
|
err = db.QueryRow(context.Background(), sql, args...).Scan(&jokeID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if jokeID == id {
|
||||||
|
sql, args, err = psql.Delete("jokesbapak2").Where(squirrel.Eq{"id": id}).ToSql()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = db.Query(context.Background(), sql, args...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.Status(fiber.StatusOK).JSON(models.ResponseJoke{
|
||||||
|
Message: "specified joke id has been deleted",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return c.Status(fiber.StatusNotAcceptable).JSON(models.ResponseError{
|
||||||
|
Error: "specified joke id does not exists",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -2,7 +2,6 @@ package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"strconv"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Masterminds/squirrel"
|
"github.com/Masterminds/squirrel"
|
||||||
|
@ -38,20 +37,20 @@ func TodayJoke(c *fiber.Ctx) error {
|
||||||
|
|
||||||
if eq {
|
if eq {
|
||||||
c.Attachment(joke.link)
|
c.Attachment(joke.link)
|
||||||
return c.SendStatus(200)
|
return c.SendStatus(fiber.StatusOK)
|
||||||
} else {
|
} else {
|
||||||
var link string
|
var link string
|
||||||
err := db.QueryRow(context.Background(), "SELECT link FROM jokesbapak2 WHERE random() < 0.01 LIMIT 1").Scan(&link)
|
err := db.QueryRow(context.Background(), "SELECT link FROM jokesbapak2 WHERE random() < 0.01 LIMIT 1").Scan(&link)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
now := strconv.Itoa(int(time.Now().Unix()))
|
now := time.Now().UTC().Format(time.RFC3339)
|
||||||
err = redis.MSet(context.Background(), "today:link", link, "today:date", now).Err()
|
err = redis.MSet(context.Background(), "today:link", link, "today:date", now).Err()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
c.Attachment(link)
|
c.Attachment(link)
|
||||||
return c.SendStatus(200)
|
return c.SendStatus(fiber.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -66,7 +65,7 @@ func SingleJoke(c *fiber.Ctx) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
c.Attachment(link)
|
c.Attachment(link)
|
||||||
return c.SendStatus(200)
|
return c.SendStatus(fiber.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
func JokeByID(c *fiber.Ctx) error {
|
func JokeByID(c *fiber.Ctx) error {
|
||||||
|
@ -79,8 +78,8 @@ func JokeByID(c *fiber.Ctx) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if link == "" {
|
if link == "" {
|
||||||
return c.Status(404).Send([]byte("Requested ID was not found."))
|
return c.Status(fiber.StatusNotFound).Send([]byte("Requested ID was not found."))
|
||||||
}
|
}
|
||||||
c.Attachment(link)
|
c.Attachment(link)
|
||||||
return c.SendStatus(200)
|
return c.SendStatus(fiber.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +1,51 @@
|
||||||
package handler
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/Masterminds/squirrel"
|
||||||
|
"github.com/aldy505/jokes-bapak2-api/api/app/v1/models"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func UpdateJoke(c *fiber.Ctx) error {
|
||||||
|
id := c.Params("id")
|
||||||
|
// Check if the joke exists
|
||||||
|
sql, args, err := psql.Select("id").From("jokesbapak2").Where(squirrel.Eq{"id": id}).ToSql()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var jokeID string
|
||||||
|
err = db.QueryRow(context.Background(), sql, args...).Scan(&jokeID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if jokeID == id {
|
||||||
|
body := new(models.RequestJokePost)
|
||||||
|
err = c.BodyParser(&body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
sql, args, err = psql.Update("jokesbapak2").Set("link", body.Link).Set("key", body.Key).ToSql()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := db.Query(context.Background(), sql, args...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).JSON(models.ResponseJoke{
|
||||||
|
Message: "specified joke id has been updated",
|
||||||
|
Link: body.Link,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusNotAcceptable).JSON(models.ResponseError{
|
||||||
|
Error: "specified joke id does not exists",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ var db = database.New()
|
||||||
|
|
||||||
func RequireAuth() fiber.Handler {
|
func RequireAuth() fiber.Handler {
|
||||||
return func(c *fiber.Ctx) error {
|
return func(c *fiber.Ctx) error {
|
||||||
var auth models.Auth
|
var auth models.RequestAuth
|
||||||
err := c.BodyParser(&auth)
|
err := c.BodyParser(&auth)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -33,8 +33,8 @@ func RequireAuth() fiber.Handler {
|
||||||
}
|
}
|
||||||
|
|
||||||
if token == "" {
|
if token == "" {
|
||||||
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{
|
return c.Status(fiber.StatusForbidden).JSON(models.ResponseError{
|
||||||
"error": "Invalid token",
|
Error: "Invalid token",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,8 +61,9 @@ func RequireAuth() fiber.Handler {
|
||||||
if verify {
|
if verify {
|
||||||
c.Next()
|
c.Next()
|
||||||
}
|
}
|
||||||
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{
|
|
||||||
"error": "Invalid key",
|
return c.Status(fiber.StatusForbidden).JSON(models.ResponseError{
|
||||||
|
Error: "Invalid key",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
package models
|
|
||||||
|
|
||||||
type Auth struct {
|
|
||||||
Key string `json:"key"`
|
|
||||||
Token string `json:"token"`
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
package models
|
|
||||||
|
|
||||||
type JokePost struct {
|
|
||||||
Key string `json:"string"`
|
|
||||||
Link string `json:"link"`
|
|
||||||
}
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
type RequestJokePost struct {
|
||||||
|
Key string `json:"string"`
|
||||||
|
Link string `json:"link"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RequestAuth struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
Token string `json:"token"`
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
type ResponseError struct {
|
||||||
|
Error string `json:"error"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResponseJoke struct {
|
||||||
|
Link string `json:"link"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/go-redis/redis/v8"
|
"github.com/go-redis/redis/v8"
|
||||||
|
_ "github.com/joho/godotenv/autoload"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Connect to the database
|
// Connect to the database
|
||||||
|
|
|
@ -5,105 +5,55 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
sq "github.com/Masterminds/squirrel"
|
|
||||||
"github.com/aldy505/bob"
|
"github.com/aldy505/bob"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Set up the table connection, create table if not exists
|
// Set up the table connection, create table if not exists
|
||||||
func Setup() error {
|
func Setup() error {
|
||||||
psql := sq.StatementBuilder.PlaceholderFormat(sq.Dollar)
|
|
||||||
db := New()
|
db := New()
|
||||||
|
|
||||||
// Jokesbapak2 table & data
|
// Jokesbapak2 table & data
|
||||||
// Check if table exists
|
sql, _, err := bob.CreateTableIfNotExists("jokesbapak2").
|
||||||
sql, args, err := bob.HasTable("jokesbapak2").PlaceholderFormat(bob.Dollar).ToSQL()
|
Columns("id", "link", "key").
|
||||||
|
Types("SERIAL", "TEXT", "VARCHAR(255)").
|
||||||
|
Primary("id").ToSql()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Fatalln("10 - failed on table creation: ", err)
|
||||||
log.Fatalln("failed on checking database table:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var hasTableJokes bool
|
|
||||||
err = db.QueryRow(context.Background(), sql, args...).Scan(&hasTableJokes)
|
|
||||||
if err != nil {
|
|
||||||
if err.Error() == "no rows in result set" {
|
|
||||||
hasTableJokes = false
|
|
||||||
} else {
|
|
||||||
log.Fatalln("failed on checking database table:", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !hasTableJokes {
|
|
||||||
sql, _, err = bob.CreateTable("jokesbapak2").
|
|
||||||
Columns("id", "link").
|
|
||||||
Types("SERIAL", "VARCHAR(255)").
|
|
||||||
Primary("id").ToSQL()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln("failed on table creation:", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
splitSql := strings.Split(sql, ";")
|
splitSql := strings.Split(sql, ";")
|
||||||
for i := range splitSql {
|
for i := range splitSql {
|
||||||
_, err = db.Query(context.Background(), splitSql[i])
|
_, err = db.Query(context.Background(), splitSql[i])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(sql)
|
log.Fatalln("11 - failed on table creation: ", err)
|
||||||
log.Fatalln("Failed on table creation: ", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
insertQuery, args, err := psql.Insert("jokesbapak2").
|
|
||||||
Columns("link").
|
|
||||||
Values("https://i.ibb.co/19pntdQ/Ea-p8-BWU8-AAtbjp.jpg").
|
|
||||||
ToSql()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln("Failed on query creation: ", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err = db.Query(context.Background(), insertQuery, args...)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln("Failed on table insertion: ", err)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Authorization
|
// Authorization
|
||||||
// Check if table exists
|
sql, _, err = bob.CreateTableIfNotExists("authorization").
|
||||||
sql, args, err = bob.HasTable("authorization").PlaceholderFormat(bob.Dollar).ToSQL()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln("failed on checking database table:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var hasTableAuth bool
|
|
||||||
err = db.QueryRow(context.Background(), sql, args...).Scan(&hasTableAuth)
|
|
||||||
if err != nil {
|
|
||||||
if err.Error() == "no rows in result set" {
|
|
||||||
hasTableAuth = false
|
|
||||||
} else {
|
|
||||||
log.Fatalln("failed on checking database table:", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !hasTableAuth {
|
|
||||||
sql, _, err = bob.CreateTable("authorization").
|
|
||||||
Columns("id", "token", "key").
|
Columns("id", "token", "key").
|
||||||
Types("SERIAL", "VARCHAR(255)", "VARCHAR(255)").
|
Types("SERIAL", "TEXT", "VARCHAR(255)").
|
||||||
Primary("id").
|
Primary("id").
|
||||||
Unique("token").
|
Unique("token").
|
||||||
ToSQL()
|
ToSql()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln("Failed on table creation: ", err)
|
log.Fatalln("14 - failed on table creation: ", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
splitSql := strings.Split(sql, ";")
|
splitSql = strings.Split(sql, ";")
|
||||||
for i := range splitSql {
|
for i := range splitSql {
|
||||||
_, err = db.Query(context.Background(), splitSql[i])
|
_, err = db.Query(context.Background(), splitSql[i])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln("Failed on table creation: ", err)
|
log.Fatalln("15 - failed on table creation: ", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
_, err = db.Query(context.Background(), "ALTER TABLE jokesbapak2 ADD CONSTRAINT fk_jokesbapak2_key FOREIGN KEY (key) REFERENCES authorization (id)")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("16 - failed on foreign key iteration: ", err)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/jackc/pgx/v4/pgxpool"
|
"github.com/jackc/pgx/v4/pgxpool"
|
||||||
|
_ "github.com/joho/godotenv/autoload"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Connect to the database
|
// Connect to the database
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/aldy505/jokes-bapak2-api/api/app/v1/handler"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Health(app *fiber.App) *fiber.App {
|
||||||
|
// Health check
|
||||||
|
app.Get("/", handler.Health)
|
||||||
|
return app
|
||||||
|
}
|
|
@ -12,31 +12,24 @@ import (
|
||||||
var db = database.New()
|
var db = database.New()
|
||||||
var redis = cache.New()
|
var redis = cache.New()
|
||||||
|
|
||||||
func New() *fiber.App {
|
func Joke(app *fiber.App) *fiber.App {
|
||||||
app := fiber.New(fiber.Config{
|
|
||||||
ETag: true,
|
|
||||||
DisableKeepalive: true,
|
|
||||||
CaseSensitive: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
v1 := app.Group("/v1")
|
|
||||||
// Single route
|
// Single route
|
||||||
v1.Get("/", handler.SingleJoke)
|
app.Get("/", handler.SingleJoke)
|
||||||
|
|
||||||
// Today's joke
|
// Today's joke
|
||||||
v1.Get("/today", handler.TodayJoke)
|
app.Get("/today", handler.TodayJoke)
|
||||||
|
|
||||||
// Joke by ID
|
// Joke by ID
|
||||||
v1.Get("/:id", handler.JokeByID)
|
app.Get("/:id", handler.JokeByID)
|
||||||
|
|
||||||
// Add new joke
|
// Add new joke
|
||||||
v1.Put("/", middleware.RequireAuth(), handler.AddNewJoke)
|
app.Put("/", middleware.RequireAuth(), handler.AddNewJoke)
|
||||||
|
|
||||||
// Update a joke
|
// Update a joke
|
||||||
v1.Patch("/:id")
|
app.Patch("/:id", middleware.RequireAuth(), handler.UpdateJoke)
|
||||||
|
|
||||||
// Delete a joke
|
// Delete a joke
|
||||||
v1.Delete("/:id")
|
app.Delete("/:id", middleware.RequireAuth(), handler.DeleteJoke)
|
||||||
|
|
||||||
return app
|
return app
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,15 +4,13 @@ import "time"
|
||||||
|
|
||||||
// IsToday checks if a date is in fact today or not.
|
// IsToday checks if a date is in fact today or not.
|
||||||
func IsToday(date string) (bool, error) {
|
func IsToday(date string) (bool, error) {
|
||||||
parse, err := time.Parse(time.UnixDate, date)
|
parse, err := time.Parse(time.RFC3339, date)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
now := time.Now()
|
y1, m1, d1 := parse.Date()
|
||||||
|
y2, m2, d2 := time.Now().Date()
|
||||||
|
|
||||||
if parse.Equal(now) {
|
return y1 == y2 && m1 == m2 && d1 == d2, nil
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
return false, nil
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
package utils_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/aldy505/jokes-bapak2-api/api/app/v1/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIsToday(t *testing.T) {
|
||||||
|
t.Run("should be able to tell if it's today", func(t *testing.T) {
|
||||||
|
today, err := utils.IsToday(time.Now().UTC().Format(time.RFC3339))
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err.Error())
|
||||||
|
}
|
||||||
|
if today == false {
|
||||||
|
t.Error("today should be true:", today)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should be able to tell if it's not today", func(t *testing.T) {
|
||||||
|
today, err := utils.IsToday("2021-01-01T11:48:24Z")
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err.Error())
|
||||||
|
}
|
||||||
|
if today == true {
|
||||||
|
t.Error("today should be false:", today)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParseToFormBody converts a body to form data type
|
||||||
|
func ParseToFormBody(body map[string]interface{}) ([]byte, error) {
|
||||||
|
var form string
|
||||||
|
for key, value := range body {
|
||||||
|
form += key + "="
|
||||||
|
switch v := value.(type) {
|
||||||
|
case string:
|
||||||
|
form += v
|
||||||
|
case int:
|
||||||
|
form += strconv.Itoa(v)
|
||||||
|
case bool:
|
||||||
|
form += strconv.FormatBool(v)
|
||||||
|
}
|
||||||
|
form += "&"
|
||||||
|
}
|
||||||
|
return []byte(form), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseToJSONBody converts a body to json data type
|
||||||
|
func ParseToJSONBody(body map[string]interface{}) ([]byte, error) {
|
||||||
|
b, err := json.Marshal(body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return b, nil
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
package utils_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/aldy505/jokes-bapak2-api/api/app/v1/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseToJSONBody(t *testing.T) {
|
||||||
|
t.Run("should be able to parse a json string", func(t *testing.T) {
|
||||||
|
body := map[string]interface{}{
|
||||||
|
"name": "Scott",
|
||||||
|
"age": 32,
|
||||||
|
"fat": true,
|
||||||
|
}
|
||||||
|
parsed, err := utils.ParseToJSONBody(body)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err.Error())
|
||||||
|
}
|
||||||
|
result := "{\"age\":32,\"fat\":true,\"name\":\"Scott\"}"
|
||||||
|
if string(parsed) != result {
|
||||||
|
t.Error("parsed string is not the same as result:", string(parsed))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseToFormBody(t *testing.T) {
|
||||||
|
t.Run("should be able to parse a form body", func(t *testing.T) {
|
||||||
|
body := map[string]interface{}{
|
||||||
|
"name": "Scott",
|
||||||
|
"age": 32,
|
||||||
|
"fat": true,
|
||||||
|
}
|
||||||
|
parsed, err := utils.ParseToFormBody(body)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err.Error())
|
||||||
|
}
|
||||||
|
result := "name=Scott&age=32&fat=true&"
|
||||||
|
if string(parsed) != result {
|
||||||
|
t.Error("parsed string is not the same as result:", string(parsed))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ContentType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
JSON ContentType = iota
|
||||||
|
Form
|
||||||
|
)
|
||||||
|
|
||||||
|
type RequestConfig struct {
|
||||||
|
URL string
|
||||||
|
Method string
|
||||||
|
Headers map[string]interface{}
|
||||||
|
Body map[string]interface{}
|
||||||
|
ContentType ContentType
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request is a simple wrapper around http.NewRequest
|
||||||
|
func Request(config RequestConfig) (response *http.Response, err error) {
|
||||||
|
client := &http.Client{}
|
||||||
|
|
||||||
|
var body []byte
|
||||||
|
if config.ContentType == JSON {
|
||||||
|
parsed, err := ParseToJSONBody(config.Body)
|
||||||
|
if err != nil {
|
||||||
|
return &http.Response{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
body = parsed
|
||||||
|
} else if config.ContentType == Form {
|
||||||
|
parsed, err := ParseToFormBody(config.Body)
|
||||||
|
if err != nil {
|
||||||
|
return &http.Response{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
body = parsed
|
||||||
|
}
|
||||||
|
|
||||||
|
request, err := http.NewRequest(config.Method, config.URL, bytes.NewReader(body))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err = client.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
package utils_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/aldy505/jokes-bapak2-api/api/app/v1/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRequest(t *testing.T) {
|
||||||
|
t.Run("should be able to do a get request", func(t *testing.T) {
|
||||||
|
res, err := utils.Request(utils.RequestConfig{
|
||||||
|
URL: "https://jsonplaceholder.typicode.com/todos/1",
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Headers: map[string]interface{}{
|
||||||
|
"User-Agent": "Jokesbapak2 Test API",
|
||||||
|
"Accept": "application/json",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err.Error())
|
||||||
|
}
|
||||||
|
if res.StatusCode != 200 {
|
||||||
|
t.Error("response does not have 200 status", res.Status)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -4,10 +4,11 @@ go 1.16
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Masterminds/squirrel v1.5.0
|
github.com/Masterminds/squirrel v1.5.0
|
||||||
github.com/aldy505/bob v0.0.0-20210630160113-75547d606a54
|
github.com/aldy505/bob v0.0.1
|
||||||
github.com/aldy505/phc-crypto v1.1.0
|
github.com/aldy505/phc-crypto v1.1.0
|
||||||
github.com/getsentry/sentry-go v0.11.0
|
github.com/getsentry/sentry-go v0.11.0
|
||||||
github.com/go-redis/redis/v8 v8.11.0
|
github.com/go-redis/redis/v8 v8.11.0
|
||||||
github.com/gofiber/fiber/v2 v2.14.0
|
github.com/gofiber/fiber/v2 v2.14.0
|
||||||
github.com/jackc/pgx/v4 v4.11.0
|
github.com/jackc/pgx/v4 v4.11.0
|
||||||
|
github.com/joho/godotenv v1.3.0
|
||||||
)
|
)
|
||||||
|
|
|
@ -17,6 +17,8 @@ github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia
|
||||||
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
||||||
github.com/aldy505/bob v0.0.0-20210630160113-75547d606a54 h1:U9GFaqa00Ft1kqmUxoNz+CugEmXGdX6bz2L+O/dbB/8=
|
github.com/aldy505/bob v0.0.0-20210630160113-75547d606a54 h1:U9GFaqa00Ft1kqmUxoNz+CugEmXGdX6bz2L+O/dbB/8=
|
||||||
github.com/aldy505/bob v0.0.0-20210630160113-75547d606a54/go.mod h1:/8HuD17XXgzuaFw5j4oDyB8O+NlW8mKWd0QCCbeoLVE=
|
github.com/aldy505/bob v0.0.0-20210630160113-75547d606a54/go.mod h1:/8HuD17XXgzuaFw5j4oDyB8O+NlW8mKWd0QCCbeoLVE=
|
||||||
|
github.com/aldy505/bob v0.0.1 h1:L/nvD9+ViLJaWbgbeBes/4xQfz7YtQLJtk8OjSa9L2k=
|
||||||
|
github.com/aldy505/bob v0.0.1/go.mod h1:/8HuD17XXgzuaFw5j4oDyB8O+NlW8mKWd0QCCbeoLVE=
|
||||||
github.com/aldy505/phc-crypto v1.1.0 h1:BagRKCrB7FOYy5vnuXR6xs6ml2gJD/CvSJkX/Ozo63w=
|
github.com/aldy505/phc-crypto v1.1.0 h1:BagRKCrB7FOYy5vnuXR6xs6ml2gJD/CvSJkX/Ozo63w=
|
||||||
github.com/aldy505/phc-crypto v1.1.0/go.mod h1:LJugClOkOWKnpLrWhSaIDRN/5ftvZPD48S5oXsT7iTg=
|
github.com/aldy505/phc-crypto v1.1.0/go.mod h1:LJugClOkOWKnpLrWhSaIDRN/5ftvZPD48S5oXsT7iTg=
|
||||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||||
|
@ -243,6 +245,8 @@ github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dv
|
||||||
github.com/jackc/puddle v1.1.3 h1:JnPg/5Q9xVJGfjsO5CPUOjnJps1JaRUm8I9FXVCFK94=
|
github.com/jackc/puddle v1.1.3 h1:JnPg/5Q9xVJGfjsO5CPUOjnJps1JaRUm8I9FXVCFK94=
|
||||||
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||||
|
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
|
||||||
|
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
||||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||||
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
|
|
50
api/main.go
50
api/main.go
|
@ -3,16 +3,18 @@ package main
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"os/signal"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
v1 "github.com/aldy505/jokes-bapak2-api/api/app/v1"
|
||||||
"github.com/aldy505/jokes-bapak2-api/api/app/v1/platform/database"
|
"github.com/aldy505/jokes-bapak2-api/api/app/v1/platform/database"
|
||||||
"github.com/aldy505/jokes-bapak2-api/api/app/v1/routes"
|
|
||||||
|
|
||||||
"github.com/getsentry/sentry-go"
|
"github.com/getsentry/sentry-go"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/gofiber/fiber/v2/middleware/cors"
|
"github.com/gofiber/fiber/v2/middleware/cors"
|
||||||
"github.com/gofiber/fiber/v2/middleware/etag"
|
"github.com/gofiber/fiber/v2/middleware/etag"
|
||||||
"github.com/gofiber/fiber/v2/middleware/limiter"
|
"github.com/gofiber/fiber/v2/middleware/limiter"
|
||||||
|
_ "github.com/joho/godotenv/autoload"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
@ -45,14 +47,54 @@ func main() {
|
||||||
app.Use(limiter.New())
|
app.Use(limiter.New())
|
||||||
app.Use(etag.New())
|
app.Use(etag.New())
|
||||||
|
|
||||||
app.Mount("/", routes.New())
|
app.Mount("/v1", v1.New())
|
||||||
|
|
||||||
log.Fatal(app.Listen(":" + os.Getenv("PORT")))
|
// Start server (with or without graceful shutdown).
|
||||||
|
if os.Getenv("ENV") == "development" {
|
||||||
|
StartServer(app)
|
||||||
|
} else {
|
||||||
|
StartServerWithGracefulShutdown(app)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func errorHandler(c *fiber.Ctx, err error) error {
|
func errorHandler(c *fiber.Ctx, err error) error {
|
||||||
sentry.CaptureException(err)
|
sentry.CaptureException(err)
|
||||||
return c.Status(500).JSON(fiber.Map{
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||||
"error": "Something went wrong on our end",
|
"error": "Something went wrong on our end",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StartServerWithGracefulShutdown function for starting server with a graceful shutdown.
|
||||||
|
func StartServerWithGracefulShutdown(a *fiber.App) {
|
||||||
|
// Create channel for idle connections.
|
||||||
|
idleConnsClosed := make(chan struct{})
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
sigint := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigint, os.Interrupt) // Catch OS signals.
|
||||||
|
<-sigint
|
||||||
|
|
||||||
|
// Received an interrupt signal, shutdown.
|
||||||
|
if err := a.Shutdown(); err != nil {
|
||||||
|
// Error from closing listeners, or context timeout:
|
||||||
|
log.Printf("Oops... Server is not shutting down! Reason: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
close(idleConnsClosed)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Run server.
|
||||||
|
if err := a.Listen(":" + os.Getenv("PORT")); err != nil {
|
||||||
|
log.Printf("Oops... Server is not running! Reason: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
<-idleConnsClosed
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartServer func for starting a simple server.
|
||||||
|
func StartServer(a *fiber.App) {
|
||||||
|
// Run server.
|
||||||
|
if err := a.Listen(":" + os.Getenv("PORT")); err != nil {
|
||||||
|
log.Printf("Oops... Server is not running! Reason: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue