diff --git a/.github/workflows/api.yml b/.github/workflows/api.yml
index fec57c1..a5a3ae9 100644
--- a/.github/workflows/api.yml
+++ b/.github/workflows/api.yml
@@ -8,7 +8,8 @@ jobs:
api-build:
name: Build
runs-on: ubuntu-latest
- container: golang:1.16.6
+ container: golang:1.17
+ timeout-minutes: 15
services:
postgres:
image: postgres:13-alpine
@@ -54,7 +55,7 @@ jobs:
run: go build main.go
- name: Run test & coverage
- run: go test -v -race -coverprofile=coverage.out -covermode=atomic ./...
+ run: go test -v -coverprofile=coverage.out -covermode=atomic ./...
env:
ENV: development
PORT: 5000
diff --git a/.github/workflows/client.yml b/.github/workflows/client.yml
index 1377cfb..2f001ce 100644
--- a/.github/workflows/client.yml
+++ b/.github/workflows/client.yml
@@ -8,7 +8,7 @@ jobs:
client-build:
name: Build
runs-on: ubuntu-latest
-
+ timeout-minutes: 15
defaults:
run:
working-directory: ./client
diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml
index a72aa0b..36e9566 100644
--- a/.github/workflows/pr.yml
+++ b/.github/workflows/pr.yml
@@ -8,7 +8,7 @@ jobs:
client-build:
name: Client
runs-on: ubuntu-latest
-
+ timeout-minutes: 15
defaults:
run:
working-directory: ./client
@@ -62,7 +62,8 @@ jobs:
api-build:
name: API
runs-on: ubuntu-latest
- container: golang:1.16.6
+ container: golang:1.17
+ timeout-minutes: 15
services:
postgres:
image: postgres:13-alpine
@@ -108,7 +109,7 @@ jobs:
run: go build main.go
- name: Run test & coverage
- run: go test -v -race -coverprofile=coverage.out -covermode=atomic ./...
+ run: go test -v -coverprofile=coverage.out -covermode=atomic ./...
env:
ENV: development
PORT: 5000
diff --git a/README.md b/README.md
index 0219129..04783e9 100644
--- a/README.md
+++ b/README.md
@@ -24,10 +24,10 @@ This is some kind of [icanhazdadjokes](https://icanhazdadjoke.com/) but it's Ind
You can consume this API via a website (linked in the front facing web) with a few endpoints:
- * `/v1/` - Random jokes bapak2
- * `/v1/id/{number}` - Jokes bapak2 based on ID
- * `/v1/today` - Jokes bapak2 of the day
- * `/v1/total` - Total available jokes bapak2
+ * `/` - Random jokes bapak2
+ * `/id/{number}` - Jokes bapak2 based on ID
+ * `/today` - Jokes bapak2 of the day
+ * `/total` - Total available jokes bapak2
Currently I'm (still) searching for an alternative for AWS S3 that I can use for free.
@@ -54,25 +54,26 @@ See [CONTRIBUTING](./CONTRIBUTING.md) or README files on each project directory
* [Ronny Gunawan](https://github.com/ronnygunawan) for the caching concept & ideas
* [artileda](https://github.com/artileda) for the jokes submission
* [elianiva](https://github.com/elianiva) for solving my SvelteKit problems
+* [kokizzu](https://github.com/kokizzu) for the dependency injection concept & ideas
## License
Jokes Bapak2 API is licensed under [GNU GENERAL PUBLIC LICENSE v3 license](./LICENSE)
```
- Jokes Bapak2 API is a free-to-use image API of Indonesian dad jokes.
- Copyright (C) 2021-present Jokes Bapak2 Contributors
+Jokes Bapak2 API is a free-to-use image API of Indonesian dad jokes.
+Copyright (C) 2021-present Jokes Bapak2 Contributors
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
- You should have received a copy of the GNU General Public License
- along with this program. If not, see .
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
```
\ No newline at end of file
diff --git a/api/Dockerfile b/api/Dockerfile
index 6599306..b0c07de 100644
--- a/api/Dockerfile
+++ b/api/Dockerfile
@@ -1,4 +1,4 @@
-FROM golang:1.16.6-buster
+FROM golang:1.17-buster
WORKDIR /app
diff --git a/api/app/v1/app.go b/api/app/v1/app.go
deleted file mode 100644
index bccbe98..0000000
--- a/api/app/v1/app.go
+++ /dev/null
@@ -1,73 +0,0 @@
-package v1
-
-import (
- "jokes-bapak2-api/app/v1/core"
- "jokes-bapak2-api/app/v1/platform/cache"
- "jokes-bapak2-api/app/v1/platform/database"
- "jokes-bapak2-api/app/v1/routes"
- "log"
- "os"
- "time"
-
- "github.com/getsentry/sentry-go"
- "github.com/gofiber/fiber/v2"
- "github.com/gofiber/fiber/v2/middleware/cors"
- "github.com/gofiber/fiber/v2/middleware/etag"
-)
-
-var memory = cache.InMemory()
-var db = database.New()
-
-func New() *fiber.App {
- app := fiber.New(fiber.Config{
- DisableKeepalive: true,
- CaseSensitive: true,
- ErrorHandler: errorHandler,
- })
-
- err := sentry.Init(sentry.ClientOptions{
- Dsn: os.Getenv("SENTRY_DSN"),
- Environment: os.Getenv("ENV"),
- AttachStacktrace: true,
- // Enable printing of SDK debug messages.
- // Useful when getting started or trying to figure something out.
- Debug: true,
- })
- if err != nil {
- log.Fatal(err)
- }
-
- defer sentry.Flush(2 * time.Second)
-
- err = database.Setup()
- if err != nil {
- sentry.CaptureException(err)
- log.Fatal(err)
- }
-
- err = core.SetAllJSONJoke(db, memory)
- if err != nil {
- log.Fatalln(err)
- }
- err = core.SetTotalJoke(db, memory)
- if err != nil {
- log.Fatalln(err)
- }
-
- app.Use(cors.New())
- app.Use(etag.New())
-
- routes.Health(app)
- routes.Joke(app)
- routes.Submit(app)
-
- return app
-}
-
-func errorHandler(c *fiber.Ctx, err error) error {
- log.Println(err)
- sentry.CaptureException(err)
- return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
- "error": "Something went wrong on our end",
- })
-}
diff --git a/api/app/v1/core/joke_setter.go b/api/app/v1/core/joke_setter.go
deleted file mode 100644
index 2412822..0000000
--- a/api/app/v1/core/joke_setter.go
+++ /dev/null
@@ -1,55 +0,0 @@
-package core
-
-import (
- "jokes-bapak2-api/app/v1/models"
-
- "github.com/allegro/bigcache/v3"
- "github.com/jackc/pgx/v4/pgxpool"
- "github.com/pquerna/ffjson/ffjson"
-)
-
-// SetAllJSONJoke fetches jokes data from GetAllJSONJokes then set it to memory cache.
-func SetAllJSONJoke(db *pgxpool.Pool, memory *bigcache.BigCache) error {
- jokes, err := GetAllJSONJokes(db)
- if err != nil {
- return err
- }
- err = memory.Set("jokes", jokes)
- if err != nil {
- return err
- }
- return nil
-}
-
-func SetTotalJoke(db *pgxpool.Pool, memory *bigcache.BigCache) error {
- check, err := CheckJokesCache(memory)
- if err != nil {
- return err
- }
-
- if !check {
- err = SetAllJSONJoke(db, memory)
- if err != nil {
- return err
- }
- }
-
- jokes, err := memory.Get("jokes")
- if err != nil {
- return err
- }
-
- var data []models.Joke
- err = ffjson.Unmarshal(jokes, &data)
- if err != nil {
- return err
- }
-
- var total = []byte{byte(len(data))}
- err = memory.Set("total", total)
- if err != nil {
- return err
- }
-
- return nil
-}
diff --git a/api/app/v1/handler/builder.go b/api/app/v1/handler/builder.go
deleted file mode 100644
index 6121335..0000000
--- a/api/app/v1/handler/builder.go
+++ /dev/null
@@ -1,16 +0,0 @@
-package handler
-
-import (
- "jokes-bapak2-api/app/v1/platform/cache"
- "jokes-bapak2-api/app/v1/platform/database"
- "time"
-
- "github.com/Masterminds/squirrel"
- "github.com/gojek/heimdall/v7/httpclient"
-)
-
-var Psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
-var Db = database.New()
-var Redis = cache.New()
-var Memory = cache.InMemory()
-var Client = httpclient.NewClient(httpclient.WithHTTPTimeout(10 * time.Second))
diff --git a/api/app/v1/handler/health/health.go b/api/app/v1/handler/health/health.go
deleted file mode 100644
index 128cda8..0000000
--- a/api/app/v1/handler/health/health.go
+++ /dev/null
@@ -1,31 +0,0 @@
-package health
-
-import (
- "context"
- "jokes-bapak2-api/app/v1/handler"
- "jokes-bapak2-api/app/v1/models"
-
- "github.com/gofiber/fiber/v2"
-)
-
-func Health(c *fiber.Ctx) error {
- // Ping REDIS database
- err := handler.Redis.Ping(context.Background()).Err()
- if err != nil {
- return c.
- Status(fiber.StatusServiceUnavailable).
- JSON(models.Error{
- Error: "REDIS: " + err.Error(),
- })
- }
-
- _, err = handler.Db.Query(context.Background(), "SELECT \"id\" FROM \"jokesbapak2\" LIMIT 1")
- if err != nil {
- return c.
- Status(fiber.StatusServiceUnavailable).
- JSON(models.Error{
- Error: "POSTGRESQL: " + err.Error(),
- })
- }
- return c.SendStatus(fiber.StatusOK)
-}
diff --git a/api/app/v1/handler/health/health_test.go b/api/app/v1/handler/health/health_test.go
deleted file mode 100644
index cf55b70..0000000
--- a/api/app/v1/handler/health/health_test.go
+++ /dev/null
@@ -1,72 +0,0 @@
-package health_test
-
-import (
- "context"
- "io/ioutil"
- v1 "jokes-bapak2-api/app/v1"
- "jokes-bapak2-api/app/v1/platform/database"
- "net/http"
- "testing"
- "time"
-
- "github.com/gofiber/fiber/v2"
- "github.com/jackc/pgx/v4/pgxpool"
- _ "github.com/joho/godotenv/autoload"
- "github.com/stretchr/testify/assert"
-)
-
-var db *pgxpool.Pool = database.New()
-var jokesData = []interface{}{1, "https://via.placeholder.com/300/06f/fff.png", 1, 2, "https://via.placeholder.com/300/07f/fff.png", 1, 3, "https://via.placeholder.com/300/08f/fff.png", 1}
-var app *fiber.App = v1.New()
-
-func cleanup() {
- j, err := db.Query(context.Background(), "DROP TABLE \"jokesbapak2\"")
- if err != nil {
- panic(err)
- }
- a, err := db.Query(context.Background(), "DROP TABLE \"administrators\"")
- if err != nil {
- panic(err)
- }
-
- defer j.Close()
- defer a.Close()
-}
-
-func setup() error {
- err := database.Setup()
- if err != nil {
- return err
- }
- a, err := db.Query(context.Background(), "INSERT INTO \"administrators\" (id, key, token, last_used) VALUES ($1, $2, $3, $4);", 1, "very secure", "not the real one", time.Now().Format(time.RFC3339))
- if err != nil {
- return err
- }
- j, err := db.Query(context.Background(), "INSERT INTO \"jokesbapak2\" (id, link, creator) VALUES ($1, $2, $3), ($4, $5, $6), ($7, $8, $9);", jokesData...)
- if err != nil {
- return err
- }
-
- defer a.Close()
- defer j.Close()
-
- return nil
-}
-
-func TestHealth(t *testing.T) {
- err := setup()
- if err != nil {
- t.Fatal(err)
- }
-
- defer cleanup()
-
- req, _ := http.NewRequest("GET", "/health", nil)
- res, err := app.Test(req, -1)
-
- assert.Equalf(t, false, err != nil, "health")
- assert.Equalf(t, 200, res.StatusCode, "health")
- assert.NotEqualf(t, 0, res.ContentLength, "health")
- _, err = ioutil.ReadAll(res.Body)
- assert.Nilf(t, err, "health")
-}
diff --git a/api/app/v1/handler/joke/joke_add.go b/api/app/v1/handler/joke/joke_add.go
deleted file mode 100644
index 30c0989..0000000
--- a/api/app/v1/handler/joke/joke_add.go
+++ /dev/null
@@ -1,65 +0,0 @@
-package joke
-
-import (
- "context"
-
- "jokes-bapak2-api/app/v1/core"
- "jokes-bapak2-api/app/v1/handler"
- "jokes-bapak2-api/app/v1/models"
-
- "github.com/gofiber/fiber/v2"
-)
-
-func AddNewJoke(c *fiber.Ctx) error {
- var body models.Joke
- err := c.BodyParser(&body)
- if err != nil {
- return err
- }
-
- // Check link validity
- valid, err := core.CheckImageValidity(handler.Client, body.Link)
- if err != nil {
- return err
- }
-
- if !valid {
- return c.
- Status(fiber.StatusBadRequest).
- JSON(models.Error{
- Error: "URL provided is not a valid image",
- })
- }
-
- sql, args, err := handler.Psql.
- Insert("jokesbapak2").
- Columns("link", "creator").
- Values(body.Link, c.Locals("userID")).
- ToSql()
- if err != nil {
- return err
- }
-
- // TODO: Implement solution if the link provided already exists.
- r, err := handler.Db.Query(context.Background(), sql, args...)
- if err != nil {
- return err
- }
-
- defer r.Close()
-
- err = core.SetAllJSONJoke(handler.Db, handler.Memory)
- if err != nil {
- return err
- }
- err = core.SetTotalJoke(handler.Db, handler.Memory)
- if err != nil {
- return err
- }
-
- return c.
- Status(fiber.StatusCreated).
- JSON(models.ResponseJoke{
- Link: body.Link,
- })
-}
diff --git a/api/app/v1/handler/joke/joke_add_test.go b/api/app/v1/handler/joke/joke_add_test.go
deleted file mode 100644
index ada5cd0..0000000
--- a/api/app/v1/handler/joke/joke_add_test.go
+++ /dev/null
@@ -1,57 +0,0 @@
-package joke_test
-
-import (
- "io/ioutil"
- "net/http"
- "strings"
- "testing"
-
- "github.com/stretchr/testify/assert"
-)
-
-func TestAddNewJoke_201(t *testing.T) {
- // TODO: Remove this line below, make this test works
- t.SkipNow()
- err := setup()
- if err != nil {
- t.Fatal(err)
- }
-
- defer cleanup()
-
- reqBody := strings.NewReader("{\"link\":\"https://via.placeholder.com/300/07f/ff0000.png\",\"key\":\"test\",\"token\":\"password\"}")
- req, _ := http.NewRequest("PUT", "/", reqBody)
- req.Header.Set("content-type", "application/json")
- req.Header.Add("accept", "application/json")
- res, err := app.Test(req, -1)
-
- assert.Equalf(t, false, err != nil, "joke add")
- assert.Equalf(t, 201, res.StatusCode, "joke add")
- assert.NotEqualf(t, 0, res.ContentLength, "joke add")
- body, err := ioutil.ReadAll(res.Body)
- assert.Nilf(t, err, "joke add")
- assert.Equalf(t, "{\"link\":\"https://via.placeholder.com/300/07f/ff0000.png\"}", string(body), "joke add")
-}
-
-func TestAddNewJoke_NotValidImage(t *testing.T) {
- // TODO: Remove this line below, make this test works
- t.SkipNow()
- err := setup()
- if err != nil {
- t.Fatal(err)
- }
-
- defer cleanup()
-
- reqBody := strings.NewReader("{\"link\":\"https://google.com/\",\"key\":\"test\",\"token\":\"password\"}")
- req, _ := http.NewRequest("PUT", "/", reqBody)
- req.Header.Set("content-type", "application/json")
- req.Header.Add("accept", "application/json")
- res, err := app.Test(req, -1)
-
- assert.Equalf(t, false, err != nil, "joke add")
- assert.Equalf(t, 400, res.StatusCode, "joke add")
- body, err := ioutil.ReadAll(res.Body)
- assert.Nilf(t, err, "joke add")
- assert.Equalf(t, "{\"error\":\"URL provided is not a valid image\"}", string(body), "joke add")
-}
diff --git a/api/app/v1/handler/joke/joke_delete.go b/api/app/v1/handler/joke/joke_delete.go
deleted file mode 100644
index d3b7c69..0000000
--- a/api/app/v1/handler/joke/joke_delete.go
+++ /dev/null
@@ -1,73 +0,0 @@
-package joke
-
-import (
- "context"
- "strconv"
-
- "jokes-bapak2-api/app/v1/core"
- "jokes-bapak2-api/app/v1/handler"
- "jokes-bapak2-api/app/v1/models"
-
- "github.com/Masterminds/squirrel"
- "github.com/gofiber/fiber/v2"
-)
-
-func DeleteJoke(c *fiber.Ctx) error {
- id, err := strconv.Atoi(c.Params("id"))
- if err != nil {
- return err
- }
-
- // Check if the joke exists
- sql, args, err := handler.Psql.
- Select("id").
- From("jokesbapak2").
- Where(squirrel.Eq{"id": id}).
- ToSql()
- if err != nil {
- return err
- }
-
- var jokeID int
- err = handler.Db.QueryRow(context.Background(), sql, args...).Scan(&jokeID)
- if err != nil {
- return err
- }
-
- if jokeID == id {
- sql, args, err = handler.Psql.
- Delete("jokesbapak2").
- Where(squirrel.Eq{"id": id}).
- ToSql()
- if err != nil {
- return err
- }
-
- r, err := handler.Db.Query(context.Background(), sql, args...)
- if err != nil {
- return err
- }
-
- defer r.Close()
-
- err = core.SetAllJSONJoke(handler.Db, handler.Memory)
- if err != nil {
- return err
- }
- err = core.SetTotalJoke(handler.Db, handler.Memory)
- 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.Error{
- Error: "specified joke id does not exists",
- })
-}
diff --git a/api/app/v1/handler/joke/joke_delete_test.go b/api/app/v1/handler/joke/joke_delete_test.go
deleted file mode 100644
index 3a9531f..0000000
--- a/api/app/v1/handler/joke/joke_delete_test.go
+++ /dev/null
@@ -1,64 +0,0 @@
-package joke_test
-
-import (
- "context"
- "io/ioutil"
- "net/http"
- "strings"
- "testing"
-
- "github.com/stretchr/testify/assert"
-)
-
-func TestDeleteJoke_200(t *testing.T) {
- // TODO: Remove this line below, make this test works
- t.SkipNow()
- err := setup()
- if err != nil {
- t.Fatal(err)
- }
-
- j, err := db.Query(context.Background(), "INSERT INTO \"jokesbapak2\" (id, link, creator) VALUES ($1, $2, $3);", 100, "https://via.placeholder.com/300/01f/fff.png", 1)
- if err != nil {
- t.Fatal(err)
- }
-
- defer j.Close()
- defer cleanup()
-
- reqBody := strings.NewReader("{\"key\":\"very secure\",\"token\":\"password\"}")
- req, _ := http.NewRequest("DELETE", "/id/100", reqBody)
- req.Header.Set("Content-Type", "application/json")
- req.Header.Set("Accept", "application/json")
- res, err := app.Test(req, -1)
-
- assert.Equalf(t, false, err != nil, "joke delete")
- assert.Equalf(t, 200, res.StatusCode, "joke delete")
- assert.NotEqualf(t, 0, res.ContentLength, "joke delete")
- body, err := ioutil.ReadAll(res.Body)
- assert.Nilf(t, err, "joke delete")
- assert.Equalf(t, "{\"message\":\"specified joke id has been deleted\"}", string(body), "joke delete")
-}
-func TestDeleteJoke_NotExists(t *testing.T) {
- // TODO: Remove this line below, make this test works
- t.SkipNow()
- err := setup()
- if err != nil {
- t.Fatal(err)
- }
-
- defer cleanup()
-
- reqBody := strings.NewReader("{\"key\":\"very secure\",\"token\":\"password\"}")
- req, _ := http.NewRequest("DELETE", "/id/100", reqBody)
- req.Header.Set("Content-Type", "application/json")
- req.Header.Set("Accept", "application/json")
- res, err := app.Test(req, -1)
-
- assert.Equalf(t, false, err != nil, "joke delete")
- assert.Equalf(t, 406, res.StatusCode, "joke delete")
- assert.NotEqualf(t, 0, res.ContentLength, "joke delete")
- body, err := ioutil.ReadAll(res.Body)
- assert.Nilf(t, err, "joke delete")
- assert.Equalf(t, "{\"message\":\"specified joke id does not exists\"}", string(body), "joke delete")
-}
diff --git a/api/app/v1/handler/joke/joke_get.go b/api/app/v1/handler/joke/joke_get.go
deleted file mode 100644
index cab33a8..0000000
--- a/api/app/v1/handler/joke/joke_get.go
+++ /dev/null
@@ -1,153 +0,0 @@
-package joke
-
-import (
- "context"
- "io/ioutil"
- "strconv"
- "time"
-
- "jokes-bapak2-api/app/v1/core"
- "jokes-bapak2-api/app/v1/handler"
- "jokes-bapak2-api/app/v1/models"
- "jokes-bapak2-api/app/v1/utils"
-
- "github.com/gofiber/fiber/v2"
-)
-
-func TodayJoke(c *fiber.Ctx) error {
- // check from handler.Redis if today's joke already exists
- // send the joke if exists
- // get a new joke if it's not, then send it.
- var joke models.Today
- err := handler.Redis.MGet(context.Background(), "today:link", "today:date", "today:image", "today:contentType").Scan(&joke)
- if err != nil {
- return err
- }
-
- eq, err := utils.IsToday(joke.Date)
- if err != nil {
- return err
- }
-
- if eq {
- c.Set("Content-Type", joke.ContentType)
- return c.Status(fiber.StatusOK).Send([]byte(joke.Image))
- } else {
- var link string
- err := handler.Db.QueryRow(context.Background(), "SELECT link FROM jokesbapak2 ORDER BY random() LIMIT 1").Scan(&link)
- if err != nil {
- return err
- }
-
- response, err := handler.Client.Get(link, nil)
- if err != nil {
- return err
- }
-
- data, err := ioutil.ReadAll(response.Body)
- if err != nil {
- return err
- }
-
- now := time.Now().UTC().Format(time.RFC3339)
- err = handler.Redis.MSet(context.Background(), map[string]interface{}{
- "today:link": link,
- "today:date": now,
- "today:image": string(data),
- "today:contentType": response.Header.Get("content-type"),
- }).Err()
- if err != nil {
- return err
- }
-
- c.Set("Content-Type", response.Header.Get("content-type"))
- return c.Status(fiber.StatusOK).Send(data)
- }
-
-}
-
-func SingleJoke(c *fiber.Ctx) error {
- checkCache, err := core.CheckJokesCache(handler.Memory)
- if err != nil {
- return err
- }
-
- if !checkCache {
- jokes, err := core.GetAllJSONJokes(handler.Db)
- if err != nil {
- return err
- }
- err = handler.Memory.Set("jokes", jokes)
- if err != nil {
- return err
- }
- }
-
- link, err := core.GetRandomJokeFromCache(handler.Memory)
- if err != nil {
- return err
- }
-
- // Get image data
- response, err := handler.Client.Get(link, nil)
- if err != nil {
- return err
- }
-
- data, err := ioutil.ReadAll(response.Body)
- if err != nil {
- return err
- }
-
- c.Set("Content-Type", response.Header.Get("content-type"))
- return c.Status(fiber.StatusOK).Send(data)
-
-}
-
-func JokeByID(c *fiber.Ctx) error {
- checkCache, err := core.CheckJokesCache(handler.Memory)
- if err != nil {
- return err
- }
-
- if !checkCache {
- jokes, err := core.GetAllJSONJokes(handler.Db)
- if err != nil {
- return err
- }
- err = handler.Memory.Set("jokes", jokes)
- if err != nil {
- return err
- }
- }
-
- id, err := strconv.Atoi(c.Params("id"))
- if err != nil {
- return err
- }
-
- link, err := core.GetCachedJokeByID(handler.Memory, id)
- if err != nil {
- return err
- }
-
- if link == "" {
- return c.
- Status(fiber.StatusNotFound).
- Send([]byte("Requested ID was not found."))
- }
-
- // Get image data
- response, err := handler.Client.Get(link, nil)
- if err != nil {
- return err
- }
-
- data, err := ioutil.ReadAll(response.Body)
- if err != nil {
- return err
- }
-
- c.Set("Content-Type", response.Header.Get("content-type"))
- return c.Status(fiber.StatusOK).Send(data)
-}
diff --git a/api/app/v1/handler/joke/joke_get_test.go b/api/app/v1/handler/joke/joke_get_test.go
deleted file mode 100644
index fe136bf..0000000
--- a/api/app/v1/handler/joke/joke_get_test.go
+++ /dev/null
@@ -1,131 +0,0 @@
-package joke_test
-
-import (
- "context"
- "io/ioutil"
- "net/http"
- "testing"
-
- v1 "jokes-bapak2-api/app/v1"
- "jokes-bapak2-api/app/v1/platform/database"
-
- "github.com/gofiber/fiber/v2"
- "github.com/jackc/pgx/v4/pgxpool"
- _ "github.com/joho/godotenv/autoload"
- "github.com/stretchr/testify/assert"
-)
-
-var db *pgxpool.Pool = database.New()
-var jokesData = []interface{}{1, "https://via.placeholder.com/300/06f/fff.png", 1, 2, "https://via.placeholder.com/300/07f/fff.png", 1, 3, "https://via.placeholder.com/300/08f/fff.png", 1}
-var app *fiber.App = v1.New()
-
-func cleanup() {
- j, err := db.Query(context.Background(), "DROP TABLE \"jokesbapak2\"")
- if err != nil {
- panic(err)
- }
- a, err := db.Query(context.Background(), "DROP TABLE \"administrators\"")
- if err != nil {
- panic(err)
- }
-
- defer j.Close()
- defer a.Close()
-}
-
-func setup() error {
- err := database.Setup()
- if err != nil {
- return err
- }
-
- a, err := db.Query(context.Background(), "INSERT INTO \"administrators\" (\"id\", \"key\", \"token\", \"last_used\") VALUES (1, 'test', '$argon2id$v=19$m=65536,t=16,p=4$3a08c79fbf2222467a623df9a9ebf75802c65a4f9be36eb1df2f5d2052d53cb7$ce434bd38f7ba1fc1f2eb773afb8a1f7f2dad49140803ac6cb9d7256ce9826fb3b4afa1e2488da511c852fc6c33a76d5657eba6298a8e49d617b9972645b7106', '');")
- if err != nil {
- return err
- }
-
- defer a.Close()
-
- j, err := db.Query(context.Background(), "INSERT INTO \"jokesbapak2\" (id, link, creator) VALUES ($1, $2, $3), ($4, $5, $6), ($7, $8, $9);", jokesData...)
- if err != nil {
- return err
- }
-
- defer j.Close()
-
- return nil
-}
-
-/// Need to find some workaround for this test
-func TestTodayJoke(t *testing.T) {
- err := setup()
- if err != nil {
- t.Fatal(err)
- }
-
- defer cleanup()
-
- req, _ := http.NewRequest("GET", "/today", nil)
- res, err := app.Test(req, -1)
-
- assert.Equalf(t, false, err != nil, "today joke")
- assert.Equalf(t, 200, res.StatusCode, "today joke")
- assert.NotEqualf(t, 0, res.ContentLength, "today joke")
- _, err = ioutil.ReadAll(res.Body)
- assert.Nilf(t, err, "today joke")
-}
-
-func TestSingleJoke(t *testing.T) {
- err := setup()
- if err != nil {
- t.Fatal(err)
- }
-
- defer cleanup()
-
- req, _ := http.NewRequest("GET", "/", nil)
- res, err := app.Test(req, -1)
-
- assert.Equalf(t, false, err != nil, "single joke")
- assert.Equalf(t, 200, res.StatusCode, "single joke")
- assert.NotEqualf(t, 0, res.ContentLength, "single joke")
- _, err = ioutil.ReadAll(res.Body)
- assert.Nilf(t, err, "single joke")
-}
-
-func TestJokeByID_200(t *testing.T) {
- err := setup()
- if err != nil {
- t.Fatal(err)
- }
-
- defer cleanup()
-
- req, _ := http.NewRequest("GET", "/id/1", nil)
- res, err := app.Test(req, -1)
-
- assert.Equalf(t, false, err != nil, "joke by id")
- assert.Equalf(t, 200, res.StatusCode, "joke by id")
- assert.NotEqualf(t, 0, res.ContentLength, "joke by id")
- _, err = ioutil.ReadAll(res.Body)
- assert.Nilf(t, err, "joke by id")
-}
-
-func TestJokeByID_404(t *testing.T) {
- err := setup()
- if err != nil {
- t.Fatal(err)
- }
-
- defer cleanup()
-
- req, _ := http.NewRequest("GET", "/id/300", nil)
- res, err := app.Test(req, -1)
-
- assert.Equalf(t, false, err != nil, "joke by id")
- assert.Equalf(t, 404, res.StatusCode, "joke by id")
- assert.NotEqualf(t, 0, res.ContentLength, "joke by id")
- body, err := ioutil.ReadAll(res.Body)
- assert.Nilf(t, err, "joke by id")
- assert.Equalf(t, "Requested ID was not found.", string(body), "joke by id")
-}
diff --git a/api/app/v1/handler/joke/joke_total.go b/api/app/v1/handler/joke/joke_total.go
deleted file mode 100644
index 6fc1874..0000000
--- a/api/app/v1/handler/joke/joke_total.go
+++ /dev/null
@@ -1,43 +0,0 @@
-package joke
-
-import (
- "jokes-bapak2-api/app/v1/core"
- "jokes-bapak2-api/app/v1/handler"
- "jokes-bapak2-api/app/v1/models"
- "strconv"
-
- "github.com/gofiber/fiber/v2"
-)
-
-func TotalJokes(c *fiber.Ctx) error {
- checkTotal, err := core.CheckTotalJokesCache(handler.Memory)
- if err != nil {
- return err
- }
-
- if !checkTotal {
- err = core.SetTotalJoke(handler.Db, handler.Memory)
- if err != nil {
- return err
- }
- }
-
- total, err := handler.Memory.Get("total")
-
- if err != nil {
- if err.Error() == "Entry not found" {
- return c.
- Status(fiber.StatusInternalServerError).
- JSON(models.Error{
- Error: "no data found",
- })
- }
- return err
- }
-
- return c.
- Status(fiber.StatusOK).
- JSON(models.ResponseJoke{
- Message: strconv.Itoa(int(total[0])),
- })
-}
diff --git a/api/app/v1/handler/joke/joke_total_test.go b/api/app/v1/handler/joke/joke_total_test.go
deleted file mode 100644
index ea907c9..0000000
--- a/api/app/v1/handler/joke/joke_total_test.go
+++ /dev/null
@@ -1,30 +0,0 @@
-package joke_test
-
-import (
- "io/ioutil"
- "net/http"
- "testing"
-
- "github.com/stretchr/testify/assert"
-)
-
-func TestTotalJokes(t *testing.T) {
- err := setup()
- if err != nil {
- t.Fatal(err)
- }
-
- defer cleanup()
-
- req, _ := http.NewRequest("GET", "/total", nil)
- res, err := app.Test(req, -1)
-
- assert.Equalf(t, false, err != nil, "joke total")
- assert.Equalf(t, 200, res.StatusCode, "joke total")
- assert.NotEqualf(t, 0, res.ContentLength, "joke total")
- body, err := ioutil.ReadAll(res.Body)
- assert.Nilf(t, err, "joke total")
- // FIXME: This should be "message": "3", not one. I don't know what's wrong as it's 1 AM.
- assert.Equalf(t, "{\"message\":\"3\"}", string(body), "joke total")
-
-}
diff --git a/api/app/v1/handler/joke/joke_update.go b/api/app/v1/handler/joke/joke_update.go
deleted file mode 100644
index 7a5ee83..0000000
--- a/api/app/v1/handler/joke/joke_update.go
+++ /dev/null
@@ -1,91 +0,0 @@
-package joke
-
-import (
- "context"
-
- "jokes-bapak2-api/app/v1/core"
- "jokes-bapak2-api/app/v1/handler"
- "jokes-bapak2-api/app/v1/models"
-
- "github.com/Masterminds/squirrel"
- "github.com/gofiber/fiber/v2"
-)
-
-func UpdateJoke(c *fiber.Ctx) error {
- id := c.Params("id")
- // Check if the joke exists
- sql, args, err := handler.Psql.
- Select("id").
- From("jokesbapak2").
- Where(squirrel.Eq{"id": id}).
- ToSql()
- if err != nil {
- return err
- }
-
- var jokeID string
- err = handler.Db.QueryRow(context.Background(), sql, args...).Scan(&jokeID)
- if err != nil && err != models.ErrNoRows {
- return err
- }
-
- if jokeID == id {
- body := new(models.Joke)
- err = c.BodyParser(&body)
- if err != nil {
- return err
- }
-
- // Check link validity
- valid, err := core.CheckImageValidity(handler.Client, body.Link)
- if err != nil {
- return err
- }
-
- if !valid {
- return c.
- Status(fiber.StatusBadRequest).
- JSON(models.Error{
- Error: "URL provided is not a valid image",
- })
- }
-
- sql, args, err = handler.Psql.
- Update("jokesbapak2").
- Set("link", body.Link).
- Set("creator", c.Locals("userID")).
- ToSql()
- if err != nil {
- return err
- }
-
- r, err := handler.Db.Query(context.Background(), sql, args...)
- if err != nil {
- return err
- }
-
- defer r.Close()
-
- err = core.SetAllJSONJoke(handler.Db, handler.Memory)
- if err != nil {
- return err
- }
- err = core.SetTotalJoke(handler.Db, handler.Memory)
- 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.Error{
- Error: "specified joke id does not exists",
- })
-}
diff --git a/api/app/v1/handler/joke/joke_update_test.go b/api/app/v1/handler/joke/joke_update_test.go
deleted file mode 100644
index 34518cf..0000000
--- a/api/app/v1/handler/joke/joke_update_test.go
+++ /dev/null
@@ -1,55 +0,0 @@
-package joke_test
-
-import (
- "io/ioutil"
- "net/http"
- "strings"
- "testing"
-
- "github.com/stretchr/testify/assert"
-)
-
-func TestUpdateJoke_200(t *testing.T) {
- t.SkipNow()
- err := setup()
- if err != nil {
- t.Fatal(err)
- }
- defer cleanup()
-
- reqBody := strings.NewReader("{\"link\":\"https://picsum.photos/id/9/200/300\",\"key\":\"test\",\"token\":\"password\"}")
- req, _ := http.NewRequest("PATCH", "/id/1", reqBody)
- req.Header.Set("Content-Type", "application/json")
- req.Header.Set("Accept", "application/json")
- res, err := app.Test(req, -1)
-
- assert.Equalf(t, false, err != nil, "joke update")
- assert.Equalf(t, 200, res.StatusCode, "joke update")
- assert.NotEqualf(t, 0, res.ContentLength, "joke update")
- body, err := ioutil.ReadAll(res.Body)
- assert.Nilf(t, err, "joke update")
- assert.Equalf(t, "{\"message\":\"specified joke id has been deleted\"}", string(body), "joke update")
-}
-
-func TestUpdateJoke_NotExists(t *testing.T) {
- // TODO: Remove this line below, make this test works
- t.SkipNow()
- err := setup()
- if err != nil {
- t.Fatal(err)
- }
- defer cleanup()
-
- reqBody := strings.NewReader("{\"link\":\"https://picsum.photos/id/9/200/300\",\"key\":\"test\",\"token\":\"password\"}")
- req, _ := http.NewRequest("PATCH", "/id/100", reqBody)
- req.Header.Set("Content-Type", "application/json")
- req.Header.Set("Accept", "application/json")
- res, err := app.Test(req, -1)
-
- assert.Equalf(t, false, err != nil, "joke update")
- assert.Equalf(t, 406, res.StatusCode, "joke update")
- assert.NotEqualf(t, 0, res.ContentLength, "joke update")
- body, err := ioutil.ReadAll(res.Body)
- assert.Nilf(t, err, "joke update")
- assert.Equalf(t, "{\"message\":\"specified joke id does not exists\"}", string(body), "joke update")
-}
diff --git a/api/app/v1/handler/submit/submit_add.go b/api/app/v1/handler/submit/submit_add.go
deleted file mode 100644
index 65be308..0000000
--- a/api/app/v1/handler/submit/submit_add.go
+++ /dev/null
@@ -1,101 +0,0 @@
-package submit
-
-import (
- "context"
- "jokes-bapak2-api/app/v1/core"
- "jokes-bapak2-api/app/v1/handler"
- "jokes-bapak2-api/app/v1/models"
- "strings"
- "time"
-
- "github.com/georgysavva/scany/pgxscan"
- "github.com/gofiber/fiber/v2"
-)
-
-func SubmitJoke(c *fiber.Ctx) error {
- var body models.Submission
- err := c.BodyParser(&body)
- if err != nil {
- return err
- }
-
- // Image and/or Link should not be empty
- if body.Image == "" && body.Link == "" {
- return c.Status(fiber.StatusBadRequest).JSON(models.Error{
- Error: "a link or an image should be supplied in a form of multipart/form-data",
- })
- }
-
- // Author should be supplied
- if body.Author == "" {
- return c.Status(fiber.StatusBadRequest).JSON(models.Error{
- Error: "an author key consisting on the format \"yourname \" must be supplied",
- })
- } else {
- // Validate format
- valid := core.ValidateAuthor(body.Author)
- if !valid {
- return c.Status(fiber.StatusBadRequest).JSON(models.Error{
- Error: "please stick to the format of \"yourname \" and within 200 characters",
- })
- }
- }
-
- var url string
-
- // Check link validity if link was provided
- if body.Link != "" {
- valid, err := core.CheckImageValidity(handler.Client, body.Link)
- if err != nil {
- return err
- }
- if !valid {
- return c.Status(fiber.StatusBadRequest).JSON(models.Error{
- Error: "URL provided is not a valid image",
- })
- }
-
- url = body.Link
- }
-
- // If image was provided
- if body.Image != "" {
- image := strings.NewReader(body.Image)
-
- url, err = core.UploadImage(handler.Client, image)
- if err != nil {
- return err
- }
- }
-
- now := time.Now().UTC().Format(time.RFC3339)
-
- sql, args, err := handler.Psql.
- Insert("submission").
- Columns("link", "created_at", "author").
- Values(url, now, body.Author).
- Suffix("RETURNING id,created_at,link,author,status").
- ToSql()
- if err != nil {
- return err
- }
-
- var submission []models.Submission
- result, err := handler.Db.Query(context.Background(), sql, args...)
- if err != nil {
- return err
- }
- defer result.Close()
-
- err = pgxscan.ScanAll(&submission, result)
- if err != nil {
- return err
- }
-
- return c.
- Status(fiber.StatusCreated).
- JSON(models.ResponseSubmission{
- Message: "Joke submitted. Please wait for a few days for admin to approve your submission.",
- Data: submission[0],
- })
-}
diff --git a/api/app/v1/handler/submit/submit_get.go b/api/app/v1/handler/submit/submit_get.go
deleted file mode 100644
index f3a3fa7..0000000
--- a/api/app/v1/handler/submit/submit_get.go
+++ /dev/null
@@ -1,104 +0,0 @@
-package submit
-
-import (
- "bytes"
- "context"
- "jokes-bapak2-api/app/v1/handler"
- "jokes-bapak2-api/app/v1/models"
- "log"
- "strconv"
-
- "github.com/aldy505/bob"
- "github.com/georgysavva/scany/pgxscan"
- "github.com/gofiber/fiber/v2"
-)
-
-func GetSubmission(c *fiber.Ctx) error {
- query := new(models.SubmissionQuery)
- err := c.QueryParser(query)
- if err != nil {
- return err
- }
-
- var limit int
- var offset int
- var approved bool
-
- if query.Limit != "" {
- limit, err = strconv.Atoi(query.Limit)
- if err != nil {
- return err
- }
- }
- if query.Page != "" {
- page, err := strconv.Atoi(query.Page)
- if err != nil {
- return err
- }
- offset = (page - 1) * 20
- }
-
- if query.Approved != "" {
- approved, err = strconv.ParseBool(query.Approved)
- if err != nil {
- return err
- }
- }
-
- var status int
-
- if approved {
- status = 1
- } else {
- status = 0
- }
-
- var sql string
- var args []interface{}
-
- var sqlQuery *bytes.Buffer = &bytes.Buffer{}
- sqlQuery.WriteString("SELECT * FROM submission WHERE TRUE")
-
- if query.Author != "" {
- sqlQuery.WriteString(" AND author = ?")
- args = append(args, query.Author)
- }
-
- if query.Approved != "" {
- sqlQuery.WriteString(" AND status = ?")
- args = append(args, status)
- }
-
- if limit > 0 {
- sqlQuery.WriteString(" LIMIT " + strconv.Itoa(limit))
- } else {
- sqlQuery.WriteString(" LIMIT 20")
- }
-
- if query.Page != "" {
- sqlQuery.WriteString(" OFFSET " + strconv.Itoa(offset))
- }
-
- sql = bob.ReplacePlaceholder(sqlQuery.String(), bob.Dollar)
-
- var submissions []models.Submission
- results, err := handler.Db.Query(context.Background(), sql, args...)
- if err != nil {
- log.Println(err)
- return err
- }
-
- defer results.Close()
-
- err = pgxscan.ScanAll(&submissions, results)
- if err != nil {
- return err
- }
-
- return c.
- Status(fiber.StatusOK).
- JSON(fiber.Map{
- "count": len(submissions),
- "jokes": submissions,
- })
-}
diff --git a/api/app/v1/handler/submit/submit_get_test.go b/api/app/v1/handler/submit/submit_get_test.go
deleted file mode 100644
index 2d6eb47..0000000
--- a/api/app/v1/handler/submit/submit_get_test.go
+++ /dev/null
@@ -1,79 +0,0 @@
-package submit_test
-
-import (
- "context"
- "io/ioutil"
- v1 "jokes-bapak2-api/app/v1"
- "jokes-bapak2-api/app/v1/platform/database"
- "net/http"
- "testing"
-
- "github.com/gofiber/fiber/v2"
- "github.com/jackc/pgx/v4/pgxpool"
- _ "github.com/joho/godotenv/autoload"
-
- "github.com/stretchr/testify/assert"
-)
-
-var db *pgxpool.Pool = database.New()
-var submissionData = []interface{}{1, "https://via.placeholder.com/300/01f/fff.png", "2021-08-03T18:20:38Z", "Test ", 0, 2, "https://via.placeholder.com/300/02f/fff.png", "2021-08-04T18:20:38Z", "Test ", 1}
-var app *fiber.App = v1.New()
-
-func cleanup() {
- s, err := db.Query(context.Background(), "DROP TABLE \"submission\"")
- if err != nil {
- panic(err)
- }
- defer s.Close()
-}
-
-func setup() error {
- err := database.Setup()
- if err != nil {
- return err
- }
-
- s, err := db.Query(context.Background(), "INSERT INTO \"submission\" (id, link, created_at, author, status) VALUES ($1, $2, $3, $4, $5), ($6, $7, $8, $9, $10);", submissionData...)
- if err != nil {
- return err
- }
-
- defer s.Close()
-
- return nil
-}
-func TestGetSubmission_200(t *testing.T) {
- err := setup()
- if err != nil {
- t.Fatal(err)
- }
-
- defer cleanup()
-
- req, _ := http.NewRequest("GET", "/submit", nil)
- res, err := app.Test(req, -1)
-
- assert.Equalf(t, false, err != nil, "get submission")
- assert.Equalf(t, 200, res.StatusCode, "get submission")
- body, err := ioutil.ReadAll(res.Body)
- assert.Nilf(t, err, "get submission")
- assert.Equalf(t, "{\"count\":2,\"jokes\":[{\"id\":1,\"link\":\"https://via.placeholder.com/300/01f/fff.png\",\"created_at\":\"2021-08-03T18:20:38Z\",\"author\":\"Test \\u003ctest@example.com\\u003e\",\"status\":0},{\"id\":2,\"link\":\"https://via.placeholder.com/300/02f/fff.png\",\"created_at\":\"2021-08-04T18:20:38Z\",\"author\":\"Test \\u003ctest@example.com\\u003e\",\"status\":1}]}", string(body), "get submission")
-}
-
-func TestGetSubmission_Params(t *testing.T) {
- err := setup()
- if err != nil {
- t.Fatal(err)
- }
-
- defer cleanup()
-
- req, _ := http.NewRequest("GET", "/submit?page=1&limit=5&approved=true", nil)
- res, err := app.Test(req, -1)
-
- assert.Equalf(t, false, err != nil, "get submission")
- assert.Equalf(t, 200, res.StatusCode, "get submission")
- body, err := ioutil.ReadAll(res.Body)
- assert.Nilf(t, err, "get submission")
- assert.Equalf(t, "{\"count\":1,\"jokes\":[{\"id\":2,\"link\":\"https://via.placeholder.com/300/02f/fff.png\",\"created_at\":\"2021-08-04T18:20:38Z\",\"author\":\"Test \\u003ctest@example.com\\u003e\",\"status\":1}]}", string(body), "get submission")
-}
diff --git a/api/app/v1/middleware/auth.go b/api/app/v1/middleware/auth.go
deleted file mode 100644
index 7ef7768..0000000
--- a/api/app/v1/middleware/auth.go
+++ /dev/null
@@ -1,97 +0,0 @@
-package middleware
-
-import (
- "context"
- "time"
-
- "jokes-bapak2-api/app/v1/models"
- "jokes-bapak2-api/app/v1/platform/database"
-
- "github.com/Masterminds/squirrel"
- phccrypto "github.com/aldy505/phc-crypto"
- "github.com/gofiber/fiber/v2"
-)
-
-var psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
-var db = database.New()
-
-func RequireAuth() fiber.Handler {
- return func(c *fiber.Ctx) error {
- var auth models.Auth
- err := c.BodyParser(&auth)
- if err != nil {
- return err
- }
-
- // Check if key exists
- sql, args, err := psql.
- Select("token").
- From("administrators").
- Where(squirrel.Eq{"key": auth.Key}).
- ToSql()
- if err != nil {
- return err
- }
-
- var token string
- err = db.QueryRow(context.Background(), sql, args...).Scan(&token)
- if err != nil {
- if err.Error() == "no rows in result set" {
- return c.
- Status(fiber.StatusForbidden).
- JSON(models.Error{
- Error: "Invalid key",
- })
- }
- return err
- }
-
- crypto, err := phccrypto.Use(phccrypto.Argon2, phccrypto.Config{})
- if err != nil {
- return err
- }
-
- verify, err := crypto.Verify(token, auth.Token)
- if err != nil {
- return err
- }
-
- if verify {
- sql, args, err = psql.
- Update("administrators").
- Set("last_used", time.Now().UTC().Format(time.RFC3339)).
- ToSql()
- if err != nil {
- return err
- }
-
- _, err = db.Query(context.Background(), sql, args...)
- if err != nil {
- return err
- }
-
- sql, args, err = psql.
- Select("id").
- From("administrators").
- Where(squirrel.Eq{"key": auth.Key}).
- ToSql()
- if err != nil {
- return err
- }
-
- var id int
- err = db.QueryRow(context.Background(), sql, args...).Scan(&id)
- if err != nil {
- return err
- }
- c.Locals("userID", id)
- return c.Next()
- }
-
- return c.
- Status(fiber.StatusForbidden).
- JSON(models.Error{
- Error: "Invalid key",
- })
- }
-}
diff --git a/api/app/v1/models/errors.go b/api/app/v1/models/errors.go
deleted file mode 100644
index f7060e0..0000000
--- a/api/app/v1/models/errors.go
+++ /dev/null
@@ -1,14 +0,0 @@
-package models
-
-import "errors"
-
-var ErrNoRows = errors.New("no rows in result set")
-var ErrConnDone = errors.New("connection is already closed")
-var ErrTxDone = errors.New("transaction has already been committed or rolled back")
-
-var ErrNotFound = errors.New("record not found")
-var ErrEmpty = errors.New("record is empty")
-
-type Error struct {
- Error string `json:"error"`
-}
diff --git a/api/app/v1/platform/cache/cache.go b/api/app/v1/platform/cache/cache.go
deleted file mode 100644
index d2493e1..0000000
--- a/api/app/v1/platform/cache/cache.go
+++ /dev/null
@@ -1,16 +0,0 @@
-package cache
-
-import (
- "log"
- "time"
-
- "github.com/allegro/bigcache/v3"
-)
-
-func InMemory() *bigcache.BigCache {
- cache, err := bigcache.NewBigCache(bigcache.DefaultConfig(6 * time.Hour))
- if err != nil {
- log.Fatalln(err)
- }
- return cache
-}
diff --git a/api/app/v1/platform/cache/redis.go b/api/app/v1/platform/cache/redis.go
deleted file mode 100644
index 2a6eab8..0000000
--- a/api/app/v1/platform/cache/redis.go
+++ /dev/null
@@ -1,19 +0,0 @@
-package cache
-
-import (
- "log"
- "os"
-
- "github.com/go-redis/redis/v8"
- _ "github.com/joho/godotenv/autoload"
-)
-
-// Connect to the database
-func New() *redis.Client {
- opt, err := redis.ParseURL(os.Getenv("REDIS_URL"))
- if err != nil {
- log.Fatalln(err)
- }
- rdb := redis.NewClient(opt)
- return rdb
-}
diff --git a/api/app/v1/platform/database/placeholder.sql b/api/app/v1/platform/database/placeholder.sql
deleted file mode 100644
index 71023e5..0000000
--- a/api/app/v1/platform/database/placeholder.sql
+++ /dev/null
@@ -1,21 +0,0 @@
--- Access the data from your HTTP Request software (Postman or Insomnia)
--- with this auth:
--- key: test
--- token: password
-
-INSERT INTO "administrators" ("id", "key", "token", "last_used") VALUES
-(1, 'test', '$argon2id$v=19$m=65536,t=16,p=4$3a08c79fbf2222467a623df9a9ebf75802c65a4f9be36eb1df2f5d2052d53cb7$ce434bd38f7ba1fc1f2eb773afb8a1f7f2dad49140803ac6cb9d7256ce9826fb3b4afa1e2488da511c852fc6c33a76d5657eba6298a8e49d617b9972645b7106', '');
-
--- 10 jokes is enough right?
-
-INSERT INTO "jokesbapak2" ("id", "link", "creator") VALUES
-(1, 'https://picsum.photos/id/1000/500/500', 1),
-(2, 'https://picsum.photos/id/1001/500/500', 1),
-(3, 'https://picsum.photos/id/1002/500/500', 1),
-(4, 'https://picsum.photos/id/1003/500/500', 1),
-(5, 'https://picsum.photos/id/1004/500/500', 1),
-(6, 'https://picsum.photos/id/1005/500/500', 1),
-(7, 'https://picsum.photos/id/1006/500/500', 1),
-(8, 'https://picsum.photos/id/1010/500/500', 1),
-(9, 'https://picsum.photos/id/1008/500/500', 1),
-(10, 'https://picsum.photos/id/1009/500/500', 1);
\ No newline at end of file
diff --git a/api/app/v1/platform/database/postgres.go b/api/app/v1/platform/database/postgres.go
deleted file mode 100644
index eb7b926..0000000
--- a/api/app/v1/platform/database/postgres.go
+++ /dev/null
@@ -1,27 +0,0 @@
-package database
-
-import (
- "context"
- "log"
- "os"
-
- "github.com/jackc/pgx/v4/pgxpool"
- _ "github.com/joho/godotenv/autoload"
-)
-
-// Connect to the database
-func New() *pgxpool.Pool {
- poolConfig, err := pgxpool.ParseConfig(os.Getenv("DATABASE_URL"))
- if err != nil {
- log.Fatalln("Unable to create pool config", err)
- }
- poolConfig.MaxConns = 18
- poolConfig.MinConns = 2
-
- conn, err := pgxpool.ConnectConfig(context.Background(), poolConfig)
- if err != nil {
- log.Fatalln("Unable to create connection", err)
- }
-
- return conn
-}
diff --git a/api/app/v1/routes/health.go b/api/app/v1/routes/health.go
deleted file mode 100644
index c1dc52b..0000000
--- a/api/app/v1/routes/health.go
+++ /dev/null
@@ -1,16 +0,0 @@
-package routes
-
-import (
- "jokes-bapak2-api/app/v1/handler/health"
- "time"
-
- "github.com/gofiber/fiber/v2"
- "github.com/gofiber/fiber/v2/middleware/cache"
-)
-
-func Health(app *fiber.App) *fiber.App {
- // Health check
- app.Get("/health", cache.New(cache.Config{Expiration: 30 * time.Minute}), health.Health)
-
- return app
-}
diff --git a/api/app/v1/routes/joke.go b/api/app/v1/routes/joke.go
deleted file mode 100644
index 1bddc1d..0000000
--- a/api/app/v1/routes/joke.go
+++ /dev/null
@@ -1,35 +0,0 @@
-package routes
-
-import (
- "jokes-bapak2-api/app/v1/handler/joke"
- "jokes-bapak2-api/app/v1/middleware"
- "time"
-
- "github.com/gofiber/fiber/v2"
- "github.com/gofiber/fiber/v2/middleware/cache"
-)
-
-func Joke(app *fiber.App) *fiber.App {
- // Single route
- app.Get("/", joke.SingleJoke)
-
- // Today's joke
- app.Get("/today", cache.New(cache.Config{Expiration: 6 * time.Hour}), joke.TodayJoke)
-
- // Joke by ID
- app.Get("/id/:id", middleware.OnlyIntegerAsID(), joke.JokeByID)
-
- // Count total jokes
- app.Get("/total", cache.New(cache.Config{Expiration: 15 * time.Minute}), joke.TotalJokes)
-
- // Add new joke
- app.Put("/", middleware.RequireAuth(), joke.AddNewJoke)
-
- // Update a joke
- app.Patch("/id/:id", middleware.RequireAuth(), middleware.OnlyIntegerAsID(), joke.UpdateJoke)
-
- // Delete a joke
- app.Delete("/id/:id", middleware.RequireAuth(), middleware.OnlyIntegerAsID(), joke.DeleteJoke)
-
- return app
-}
diff --git a/api/core/administrator/id.go b/api/core/administrator/id.go
new file mode 100644
index 0000000..35d9e54
--- /dev/null
+++ b/api/core/administrator/id.go
@@ -0,0 +1,60 @@
+package administrator
+
+import (
+ "context"
+ "time"
+
+ "github.com/Masterminds/squirrel"
+ "github.com/jackc/pgx/v4/pgxpool"
+)
+
+func GetUserID(db *pgxpool.Pool, ctx context.Context, key string) (int, error) {
+ var query = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
+
+ c, err := db.Acquire(ctx)
+ if err != nil {
+ return 0, err
+ }
+ defer c.Release()
+
+ tx, err := c.Begin(ctx)
+ if err != nil {
+ return 0, err
+ }
+ defer tx.Rollback(ctx)
+
+ sql, args, err := query.
+ Update("administrators").
+ Set("last_used", time.Now().UTC().Format(time.RFC3339)).
+ ToSql()
+ if err != nil {
+ return 0, err
+ }
+
+ _, err = tx.Exec(ctx, sql, args...)
+ if err != nil {
+ return 0, err
+ }
+
+ sql, args, err = query.
+ Select("id").
+ From("administrators").
+ Where(squirrel.Eq{"key": key}).
+ ToSql()
+ if err != nil {
+ return 0, err
+ }
+
+ var id int
+ err = tx.QueryRow(ctx, sql, args...).Scan(&id)
+ if err != nil {
+ return 0, err
+ }
+
+ err = tx.Commit(ctx)
+ if err != nil {
+ return 0, err
+ }
+
+ return id, nil
+}
diff --git a/api/core/administrator/id_test.go b/api/core/administrator/id_test.go
new file mode 100644
index 0000000..7549513
--- /dev/null
+++ b/api/core/administrator/id_test.go
@@ -0,0 +1,57 @@
+package administrator_test
+
+import (
+ "context"
+ "jokes-bapak2-api/core/administrator"
+ "testing"
+ "time"
+)
+
+func TestGetUserID_Success(t *testing.T) {
+ ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(30*time.Second))
+ defer cancel()
+
+ defer Flush()
+
+ c, err := db.Acquire(ctx)
+ if err != nil {
+ t.Error("an error was thrown:", err)
+ }
+ defer c.Release()
+
+ _, err = c.Exec(
+ ctx,
+ `INSERT INTO administrators (id, key, token, last_used) VALUES ($1, $2, $3, $4)`,
+ administratorsData...,
+ )
+ if err != nil {
+ t.Error("an error was thrown:", err)
+ }
+
+ id, err := administrator.GetUserID(db, ctx, "very secure")
+ if err != nil {
+ t.Error("an error was thrown:", err)
+ }
+
+ if id != 1 {
+ t.Error("id is not correct, want: 1, got:", id)
+ }
+}
+
+func TestGetUserID_Failed(t *testing.T) {
+ ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(30*time.Second))
+ defer cancel()
+
+ defer Flush()
+
+ c, err := db.Acquire(ctx)
+ if err != nil {
+ t.Error("an error was thrown:", err)
+ }
+ defer c.Release()
+
+ id, err := administrator.GetUserID(db, ctx, "very secure")
+ if err == nil {
+ t.Error("an error was expected, got:", id)
+ }
+}
diff --git a/api/core/administrator/init_test.go b/api/core/administrator/init_test.go
new file mode 100644
index 0000000..a29863e
--- /dev/null
+++ b/api/core/administrator/init_test.go
@@ -0,0 +1,124 @@
+package administrator_test
+
+import (
+ "context"
+ "os"
+ "testing"
+ "time"
+
+ "github.com/jackc/pgx/v4/pgxpool"
+)
+
+var db *pgxpool.Pool
+
+var administratorsData = []interface{}{
+ 1, "very secure", "not the real one", time.Now().Format(time.RFC3339),
+}
+
+func TestMain(m *testing.M) {
+ defer Teardown()
+ Setup()
+
+ os.Exit(m.Run())
+}
+
+func Setup() {
+ ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(1*time.Minute))
+ defer cancel()
+
+ poolConfig, err := pgxpool.ParseConfig(os.Getenv("DATABASE_URL"))
+ if err != nil {
+ panic(err)
+ }
+
+ db, err = pgxpool.ConnectConfig(ctx, poolConfig)
+ if err != nil {
+ panic(err)
+ }
+
+ conn, err := db.Acquire(ctx)
+ if err != nil {
+ panic(err)
+ }
+ defer conn.Release()
+ tx, err := conn.Begin(ctx)
+ if err != nil {
+ panic(err)
+ }
+ defer tx.Rollback(ctx)
+
+ _, err = tx.Exec(
+ ctx,
+ `CREATE TABLE IF NOT EXISTS administrators (
+ id SERIAL PRIMARY KEY,
+ key VARCHAR(255) NOT NULL UNIQUE,
+ token TEXT,
+ last_used VARCHAR(255)
+ )`,
+ )
+ if err != nil {
+ panic(err)
+ }
+
+ err = tx.Commit(ctx)
+ if err != nil {
+ panic(err)
+ }
+}
+
+func Teardown() (err error) {
+ ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(1*time.Minute))
+ defer cancel()
+
+ defer db.Close()
+
+ c, err := db.Acquire(ctx)
+ if err != nil {
+ return err
+ }
+ defer c.Release()
+
+ tx, err := c.Begin(ctx)
+ if err != nil {
+ return err
+ }
+ defer tx.Rollback(ctx)
+
+ _, err = tx.Exec(ctx, "TRUNCATE TABLE submission RESTART IDENTITY CASCADE")
+ if err != nil {
+ return err
+ }
+ _, err = tx.Exec(ctx, "TRUNCATE TABLE jokesbapak2 RESTART IDENTITY CASCADE")
+ if err != nil {
+ return err
+ }
+ _, err = tx.Exec(ctx, "TRUNCATE TABLE administrators RESTART IDENTITY CASCADE")
+ if err != nil {
+ return err
+ }
+
+ err = tx.Commit(ctx)
+ if err != nil {
+ return err
+ }
+
+ return
+}
+
+func Flush() error {
+ ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(30*time.Second))
+ defer cancel()
+
+ conn, err := db.Acquire(ctx)
+ if err != nil {
+ return err
+ }
+ defer conn.Release()
+
+ _, err = conn.Exec(ctx, "TRUNCATE TABLE administrators RESTART IDENTITY CASCADE")
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
diff --git a/api/core/administrator/verify.go b/api/core/administrator/verify.go
new file mode 100644
index 0000000..bf60026
--- /dev/null
+++ b/api/core/administrator/verify.go
@@ -0,0 +1,41 @@
+package administrator
+
+import (
+ "context"
+ "errors"
+
+ "github.com/Masterminds/squirrel"
+ "github.com/jackc/pgx/v4"
+ "github.com/jackc/pgx/v4/pgxpool"
+)
+
+func CheckKeyExists(db *pgxpool.Pool, ctx context.Context, key string) (string, error) {
+ conn, err := db.Acquire(ctx)
+ if err != nil {
+ return "", err
+ }
+ defer conn.Release()
+
+ var query = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
+
+ // Check if key exists
+ sql, args, err := query.
+ Select("token").
+ From("administrators").
+ Where(squirrel.Eq{"key": key}).
+ ToSql()
+ if err != nil {
+ return "", err
+ }
+
+ var token string
+ err = conn.QueryRow(ctx, sql, args...).Scan(&token)
+ if err != nil {
+ if errors.Is(err, pgx.ErrNoRows) {
+ return "", nil
+ }
+ return "", err
+ }
+
+ return token, nil
+}
diff --git a/api/core/administrator/verify_test.go b/api/core/administrator/verify_test.go
new file mode 100644
index 0000000..6e21672
--- /dev/null
+++ b/api/core/administrator/verify_test.go
@@ -0,0 +1,70 @@
+package administrator_test
+
+import (
+ "context"
+ "jokes-bapak2-api/core/administrator"
+ "testing"
+ "time"
+)
+
+func TestCheckKeyExists_Success(t *testing.T) {
+ ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(30*time.Second))
+ defer cancel()
+
+ defer Flush()
+
+ c, err := db.Acquire(ctx)
+ if err != nil {
+ t.Error("an error was thrown:", err)
+ }
+ defer c.Release()
+
+ _, err = c.Exec(
+ ctx,
+ "INSERT INTO administrators (id, key, token, last_used) VALUES ($1, $2, $3, $4)",
+ administratorsData...,
+ )
+ if err != nil {
+ t.Error("an error was thrown:", err)
+ }
+
+ key, err := administrator.CheckKeyExists(db, ctx, "very secure")
+ if err != nil {
+ t.Error("an error was thrown:", err)
+ }
+
+ if key != "not the real one" {
+ t.Error("key isn't not the real one, got:", key)
+ }
+}
+
+func TestCheckKeyExists_Failing(t *testing.T) {
+ ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(30*time.Second))
+ defer cancel()
+
+ defer Flush()
+
+ c, err := db.Acquire(ctx)
+ if err != nil {
+ t.Error("an error was thrown:", err)
+ }
+ defer c.Release()
+
+ _, err = c.Exec(
+ ctx,
+ "INSERT INTO administrators (id, key, token, last_used) VALUES ($1, $2, $3, $4)",
+ administratorsData...,
+ )
+ if err != nil {
+ t.Error("an error was thrown:", err)
+ }
+
+ key, err := administrator.CheckKeyExists(db, ctx, "others")
+ if err != nil {
+ t.Error("an error was thrown:", err)
+ }
+
+ if key != "" {
+ t.Error("key is not empty, got:", key)
+ }
+}
diff --git a/api/app/v1/core/joke_getter.go b/api/core/joke/getter.go
similarity index 53%
rename from api/app/v1/core/joke_getter.go
rename to api/core/joke/getter.go
index 6297293..4ca97f9 100644
--- a/api/app/v1/core/joke_getter.go
+++ b/api/core/joke/getter.go
@@ -1,25 +1,34 @@
-package core
+package joke
import (
"context"
- "jokes-bapak2-api/app/v1/models"
+ "errors"
+ "jokes-bapak2-api/core/schema"
"math/rand"
+ "strconv"
+ "github.com/Masterminds/squirrel"
"github.com/allegro/bigcache/v3"
"github.com/georgysavva/scany/pgxscan"
+ "github.com/jackc/pgx"
"github.com/jackc/pgx/v4/pgxpool"
"github.com/pquerna/ffjson/ffjson"
)
// GetAllJSONJokes fetch the database for all the jokes then output it as a JSON []byte.
// Keep in mind, you will need to store it to memory yourself.
-func GetAllJSONJokes(db *pgxpool.Pool) ([]byte, error) {
- var jokes []models.Joke
- results, err := db.Query(context.Background(), "SELECT \"id\",\"link\" FROM \"jokesbapak2\" ORDER BY \"id\"")
+func GetAllJSONJokes(db *pgxpool.Pool, ctx context.Context) ([]byte, error) {
+ conn, err := db.Acquire(ctx)
+ if err != nil {
+ return []byte{}, err
+ }
+ defer conn.Release()
+
+ var jokes []schema.Joke
+ results, err := conn.Query(ctx, "SELECT \"id\",\"link\" FROM \"jokesbapak2\" ORDER BY \"id\"")
if err != nil {
return nil, err
}
-
defer results.Close()
err = pgxscan.ScanAll(&jokes, results)
@@ -35,17 +44,34 @@ func GetAllJSONJokes(db *pgxpool.Pool) ([]byte, error) {
return data, nil
}
+// Only return a link
+func GetRandomJokeFromDB(db *pgxpool.Pool, ctx context.Context) (string, error) {
+ conn, err := db.Acquire(ctx)
+ if err != nil {
+ return "", err
+ }
+ defer conn.Release()
+
+ var link string
+ err = conn.QueryRow(ctx, "SELECT link FROM jokesbapak2 ORDER BY random() LIMIT 1").Scan(&link)
+ if err != nil {
+ return "", err
+ }
+
+ return link, nil
+}
+
// GetRandomJokeFromCache returns a link string of a random joke from cache.
func GetRandomJokeFromCache(memory *bigcache.BigCache) (string, error) {
jokes, err := memory.Get("jokes")
if err != nil {
- if err.Error() == "Entry not found" {
- return "", models.ErrNotFound
+ if errors.Is(err, bigcache.ErrEntryNotFound) {
+ return "", schema.ErrNotFound
}
return "", err
}
- var data []models.Joke
+ var data []schema.Joke
err = ffjson.Unmarshal(jokes, &data)
if err != nil {
return "", nil
@@ -54,7 +80,7 @@ func GetRandomJokeFromCache(memory *bigcache.BigCache) (string, error) {
// Return an error if the database is empty
dataLength := len(data)
if dataLength == 0 {
- return "", models.ErrEmpty
+ return "", schema.ErrEmpty
}
random := rand.Intn(dataLength)
@@ -67,7 +93,7 @@ func GetRandomJokeFromCache(memory *bigcache.BigCache) (string, error) {
func CheckJokesCache(memory *bigcache.BigCache) (bool, error) {
_, err := memory.Get("jokes")
if err != nil {
- if err.Error() == "Entry not found" {
+ if errors.Is(err, bigcache.ErrEntryNotFound) {
return false, nil
}
return false, err
@@ -80,7 +106,7 @@ func CheckJokesCache(memory *bigcache.BigCache) (bool, error) {
func CheckTotalJokesCache(memory *bigcache.BigCache) (bool, error) {
_, err := memory.Get("total")
if err != nil {
- if err.Error() == "Entry not found" {
+ if errors.Is(err, bigcache.ErrEntryNotFound) {
return false, nil
}
return false, err
@@ -93,16 +119,16 @@ func CheckTotalJokesCache(memory *bigcache.BigCache) (bool, error) {
func GetCachedJokeByID(memory *bigcache.BigCache, id int) (string, error) {
jokes, err := memory.Get("jokes")
if err != nil {
- if err.Error() == "Entry not found" {
- return "", models.ErrNotFound
+ if errors.Is(err, bigcache.ErrEntryNotFound) {
+ return "", schema.ErrNotFound
}
return "", err
}
- var data []models.Joke
+ var data []schema.Joke
err = ffjson.Unmarshal(jokes, &data)
if err != nil {
- return "", nil
+ return "", err
}
// This is a simple solution, might convert it to goroutines and channels sometime soon.
@@ -119,11 +145,42 @@ func GetCachedJokeByID(memory *bigcache.BigCache, id int) (string, error) {
func GetCachedTotalJokes(memory *bigcache.BigCache) (int, error) {
total, err := memory.Get("total")
if err != nil {
- if err.Error() == "Entry not found" {
- return 0, models.ErrNotFound
+ if errors.Is(err, bigcache.ErrEntryNotFound) {
+ return 0, schema.ErrNotFound
}
return 0, err
}
+ i, err := strconv.Atoi(string(total))
+ if err != nil {
+ return 0, err
+ }
- return int(total[0]), nil
+ return i, nil
+}
+
+func CheckJokeExists(db *pgxpool.Pool, ctx context.Context, id string) (bool, error) {
+ conn, err := db.Acquire(ctx)
+ if err != nil {
+ return false, err
+ }
+ defer conn.Release()
+
+ var query = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
+
+ sql, args, err := query.
+ Select("id").
+ From("jokesbapak2").
+ Where(squirrel.Eq{"id": id}).
+ ToSql()
+ if err != nil {
+ return false, err
+ }
+
+ var jokeID int
+ err = conn.QueryRow(ctx, sql, args...).Scan(&jokeID)
+ if err != nil && errors.Is(err, pgx.ErrNoRows) {
+ return false, err
+ }
+
+ return strconv.Itoa(jokeID) == id, nil
}
diff --git a/api/core/joke/getter_test.go b/api/core/joke/getter_test.go
new file mode 100644
index 0000000..54783bb
--- /dev/null
+++ b/api/core/joke/getter_test.go
@@ -0,0 +1,341 @@
+package joke_test
+
+import (
+ "context"
+ "encoding/json"
+ "jokes-bapak2-api/core/joke"
+ "jokes-bapak2-api/core/schema"
+ "testing"
+ "time"
+
+ "github.com/jackc/pgx/v4"
+)
+
+func TestGetAllJSONJokes(t *testing.T) {
+ ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(30*time.Second))
+ defer cancel()
+
+ defer Flush()
+
+ conn, err := db.Acquire(ctx)
+ if err != nil {
+ t.Error("an error was thrown:", err)
+ }
+ defer conn.Release()
+
+ err = conn.BeginFunc(ctx, func(t pgx.Tx) error {
+ _, err := t.Exec(
+ ctx,
+ `INSERT INTO "administrators"
+ (id, key, token, last_used)
+ VALUES
+ ($1, $2, $3, $4),
+ ($5, $6, $7, $8)`,
+ administratorsData...,
+ )
+ if err != nil {
+ return err
+ }
+ _, err = t.Exec(
+ ctx,
+ `INSERT INTO "jokesbapak2"
+ (id, link, creator)
+ VALUES
+ ($1, $2, $3),
+ ($4, $5, $6),
+ ($7, $8, $9)`,
+ jokesData...,
+ )
+ if err != nil {
+ return err
+ }
+
+ return nil
+ })
+ if err != nil {
+ t.Error("an error was thrown:", err)
+ }
+
+ j, err := joke.GetAllJSONJokes(db, ctx)
+ if err != nil {
+ t.Error("an error was thrown:", err)
+ }
+
+ if string(j) == "" {
+ t.Error("j should not be empty")
+ }
+}
+
+func TestGetRandomJokeFromDB(t *testing.T) {
+ ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(30*time.Second))
+ defer cancel()
+
+ defer Flush()
+
+ conn, err := db.Acquire(ctx)
+ if err != nil {
+ t.Error("an error was thrown:", err)
+ }
+ defer conn.Release()
+
+ err = conn.BeginFunc(ctx, func(t pgx.Tx) error {
+ _, err := t.Exec(
+ ctx,
+ `INSERT INTO "administrators"
+ (id, key, token, last_used)
+ VALUES
+ ($1, $2, $3, $4),
+ ($5, $6, $7, $8)`,
+ administratorsData...,
+ )
+ if err != nil {
+ return err
+ }
+ _, err = t.Exec(
+ ctx,
+ `INSERT INTO "jokesbapak2"
+ (id, link, creator)
+ VALUES
+ ($1, $2, $3),
+ ($4, $5, $6),
+ ($7, $8, $9)`,
+ jokesData...,
+ )
+ if err != nil {
+ return err
+ }
+
+ return nil
+ })
+ if err != nil {
+ t.Error("an error was thrown:", err)
+ }
+
+ j, err := joke.GetRandomJokeFromDB(db, ctx)
+ if err != nil {
+ t.Error("an error was thrown:", err)
+ }
+
+ if j == "" {
+ t.Error("j should not be empty")
+ }
+}
+
+func TestGetRandomJokeFromCache(t *testing.T) {
+ defer Flush()
+
+ jokes := []schema.Joke{
+ {ID: 1, Link: "link1", Creator: 1},
+ {ID: 2, Link: "link2", Creator: 1},
+ {ID: 3, Link: "link3", Creator: 1},
+ }
+ data, err := json.Marshal(jokes)
+ if err != nil {
+ t.Error("an error was thrown:", err)
+ }
+
+ err = memory.Set("jokes", data)
+ if err != nil {
+ t.Error("an error was thrown:", err)
+ }
+
+ j, err := joke.GetRandomJokeFromCache(memory)
+ if err != nil {
+ t.Error("an error was thrown:", err)
+ }
+
+ if j == "" {
+ t.Error("j should not be empty")
+ }
+}
+
+func TestCheckJokesCache_True(t *testing.T) {
+ defer Flush()
+
+ jokes := []schema.Joke{
+ {ID: 1, Link: "link1", Creator: 1},
+ {ID: 2, Link: "link2", Creator: 1},
+ {ID: 3, Link: "link3", Creator: 1},
+ }
+ data, err := json.Marshal(jokes)
+ if err != nil {
+ t.Error("an error was thrown:", err)
+ }
+
+ err = memory.Set("jokes", data)
+ if err != nil {
+ t.Error("an error was thrown:", err)
+ }
+
+ j, err := joke.CheckJokesCache(memory)
+ if err != nil {
+ t.Error("an error was thrown:", err)
+ }
+
+ if j == false {
+ t.Error("j should not be false")
+ }
+}
+
+func TestCheckJokesCache_False(t *testing.T) {
+ defer Flush()
+
+ j, err := joke.CheckJokesCache(memory)
+ if err != nil {
+ t.Error("an error was thrown:", err)
+ }
+
+ if j == true {
+ t.Error("j should not be true")
+ }
+}
+
+func TestCheckTotalJokesCache_True(t *testing.T) {
+ defer Flush()
+
+ err := memory.Set("total", []byte("10"))
+ if err != nil {
+ t.Error("an error was thrown:", err)
+ }
+
+ j, err := joke.CheckTotalJokesCache(memory)
+ if err != nil {
+ t.Error("an error was thrown:", err)
+ }
+
+ if j == false {
+ t.Error("j should not be false")
+ }
+}
+
+func TestCheckTotalJokesCache_False(t *testing.T) {
+ defer Flush()
+
+ j, err := joke.CheckTotalJokesCache(memory)
+ if err != nil {
+ t.Error("an error was thrown:", err)
+ }
+
+ if j == true {
+ t.Error("j should not be true")
+ }
+}
+
+func TestGetCachedJokeByID(t *testing.T) {
+ defer Flush()
+
+ jokes := []schema.Joke{
+ {ID: 1, Link: "link1", Creator: 1},
+ {ID: 2, Link: "link2", Creator: 1},
+ {ID: 3, Link: "link3", Creator: 1},
+ }
+ data, err := json.Marshal(jokes)
+ if err != nil {
+ t.Error("an error was thrown:", err)
+ }
+
+ err = memory.Set("jokes", data)
+ if err != nil {
+ t.Error("an error was thrown:", err)
+ }
+
+ j, err := joke.GetCachedJokeByID(memory, 1)
+ if err != nil {
+ t.Error("an error was thrown:", err)
+ }
+
+ if j != "link1" {
+ t.Error("j should be link1, got:", j)
+ }
+
+ k, err := joke.GetCachedJokeByID(memory, 4)
+ if err != nil {
+ t.Error("an error was thrown:", err)
+ }
+
+ if k != "" {
+ t.Error("k should be empty, got:", k)
+ }
+}
+
+func TestGetCachedTotalJokes(t *testing.T) {
+ defer Flush()
+
+ err := memory.Set("total", []byte("10"))
+ if err != nil {
+ t.Error("an error was thrown:", err)
+ }
+
+ j, err := joke.GetCachedTotalJokes(memory)
+ if err != nil {
+ t.Error("an error was thrown:", err)
+ }
+
+ if j != 10 {
+ t.Error("j should be 10, got:", j)
+ }
+}
+
+func TestCheckJokeExists(t *testing.T) {
+ ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(30*time.Second))
+ defer cancel()
+
+ defer Flush()
+
+ conn, err := db.Acquire(ctx)
+ if err != nil {
+ t.Error("an error was thrown:", err)
+ }
+ defer conn.Release()
+
+ err = conn.BeginFunc(ctx, func(t pgx.Tx) error {
+ _, err := t.Exec(
+ ctx,
+ `INSERT INTO "administrators"
+ (id, key, token, last_used)
+ VALUES
+ ($1, $2, $3, $4),
+ ($5, $6, $7, $8)`,
+ administratorsData...,
+ )
+ if err != nil {
+ return err
+ }
+ _, err = t.Exec(
+ ctx,
+ `INSERT INTO "jokesbapak2"
+ (id, link, creator)
+ VALUES
+ ($1, $2, $3),
+ ($4, $5, $6),
+ ($7, $8, $9)`,
+ jokesData...,
+ )
+ if err != nil {
+ return err
+ }
+
+ return nil
+ })
+ if err != nil {
+ t.Error("an error was thrown:", err)
+ }
+
+ j, err := joke.CheckJokeExists(db, ctx, "1")
+ if err != nil {
+ t.Error("an error was thrown:", err)
+ }
+
+ if j == false {
+ t.Error("j should not be false")
+ }
+
+ k, err := joke.CheckJokeExists(db, ctx, "4")
+ if err != nil {
+ t.Error("an error was thrown:", err)
+ }
+
+ if k == true {
+ t.Error("k should not be true")
+ }
+}
diff --git a/api/core/joke/init_test.go b/api/core/joke/init_test.go
new file mode 100644
index 0000000..34a4e65
--- /dev/null
+++ b/api/core/joke/init_test.go
@@ -0,0 +1,195 @@
+package joke_test
+
+import (
+ "context"
+ "os"
+ "testing"
+ "time"
+
+ "github.com/allegro/bigcache/v3"
+ "github.com/go-redis/redis/v8"
+ "github.com/jackc/pgx/v4/pgxpool"
+)
+
+var db *pgxpool.Pool
+var cache *redis.Client
+var memory *bigcache.BigCache
+
+var jokesData = []interface{}{
+ 1, "https://via.placeholder.com/300/06f/fff.png", 1,
+ 2, "https://via.placeholder.com/300/07f/fff.png", 1,
+ 3, "https://via.placeholder.com/300/08f/fff.png", 1,
+}
+var administratorsData = []interface{}{
+ 1, "very secure", "not the real one", time.Now().Format(time.RFC3339), 2, "test", "$argon2id$v=19$m=65536,t=16,p=4$3a08c79fbf2222467a623df9a9ebf75802c65a4f9be36eb1df2f5d2052d53cb7$ce434bd38f7ba1fc1f2eb773afb8a1f7f2dad49140803ac6cb9d7256ce9826fb3b4afa1e2488da511c852fc6c33a76d5657eba6298a8e49d617b9972645b7106", "",
+}
+
+func TestMain(m *testing.M) {
+ defer Teardown()
+ Setup()
+ time.Sleep(3 * time.Second)
+
+ os.Exit(m.Run())
+}
+
+func Setup() {
+ ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(1*time.Minute))
+ defer cancel()
+
+ poolConfig, err := pgxpool.ParseConfig(os.Getenv("DATABASE_URL"))
+ if err != nil {
+ panic(err)
+ }
+
+ db, err = pgxpool.ConnectConfig(ctx, poolConfig)
+ if err != nil {
+ panic(err)
+ }
+
+ opt, err := redis.ParseURL(os.Getenv("REDIS_URL"))
+ if err != nil {
+ panic(err)
+ }
+
+ cache = redis.NewClient(opt)
+
+ memory, err = bigcache.NewBigCache(bigcache.DefaultConfig(6 * time.Hour))
+ if err != nil {
+ panic(err)
+ }
+
+ conn, err := db.Acquire(ctx)
+ if err != nil {
+ panic(err)
+ }
+ defer conn.Release()
+
+ tx, err := conn.Begin(ctx)
+ if err != nil {
+ panic(err)
+ }
+ defer tx.Rollback(ctx)
+
+ _, err = tx.Exec(
+ ctx,
+ `CREATE TABLE IF NOT EXISTS administrators (
+ id SERIAL PRIMARY KEY,
+ key VARCHAR(255) NOT NULL UNIQUE,
+ token TEXT,
+ last_used VARCHAR(255)
+ )`,
+ )
+ if err != nil {
+ panic(err)
+ }
+
+ _, err = tx.Exec(
+ ctx,
+ `CREATE TABLE IF NOT EXISTS jokesbapak2 (
+ id SERIAL PRIMARY KEY,
+ link TEXT UNIQUE,
+ creator INT NOT NULL REFERENCES "administrators" ("id")
+ )`,
+ )
+ if err != nil {
+ panic(err)
+ }
+ _, err = tx.Exec(
+ ctx,
+ `CREATE TABLE IF NOT EXISTS submission (
+ id SERIAL PRIMARY KEY,
+ link VARCHAR(255) UNIQUE NOT NULL,
+ created_at VARCHAR(255),
+ author VARCHAR(255) NOT NULL,
+ status SMALLINT DEFAULT 0
+ )`,
+ )
+ if err != nil {
+ panic(err)
+ }
+
+ err = tx.Commit(ctx)
+ if err != nil {
+ panic(err)
+ }
+}
+
+func Teardown() (err error) {
+ ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(30*time.Second))
+ defer cancel()
+
+ defer db.Close()
+
+ conn, err := db.Acquire(ctx)
+ if err != nil {
+ return err
+ }
+ defer conn.Release()
+
+ tx, err := conn.Begin(ctx)
+ if err != nil {
+ return err
+ }
+ defer tx.Rollback(ctx)
+
+ _, err = tx.Exec(ctx, "TRUNCATE TABLE submission RESTART IDENTITY CASCADE")
+ if err != nil {
+ return err
+ }
+ _, err = tx.Exec(ctx, "TRUNCATE TABLE jokesbapak2 RESTART IDENTITY CASCADE")
+ if err != nil {
+ return err
+ }
+ _, err = tx.Exec(ctx, "TRUNCATE TABLE administrators RESTART IDENTITY CASCADE")
+ if err != nil {
+ return err
+ }
+
+ err = tx.Commit(ctx)
+ if err != nil {
+ return err
+ }
+
+ err = cache.Close()
+ if err != nil {
+ return
+ }
+ err = memory.Close()
+ return
+}
+
+func Flush() error {
+ ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(30*time.Second))
+ defer cancel()
+
+ conn, err := db.Acquire(ctx)
+ if err != nil {
+ return err
+ }
+ defer conn.Release()
+
+ _, err = conn.Exec(ctx, "TRUNCATE TABLE submission RESTART IDENTITY CASCADE")
+ if err != nil {
+ return err
+ }
+ _, err = conn.Exec(ctx, "TRUNCATE TABLE jokesbapak2 RESTART IDENTITY CASCADE")
+ if err != nil {
+ return err
+ }
+ _, err = conn.Exec(ctx, "TRUNCATE TABLE administrators RESTART IDENTITY CASCADE")
+ if err != nil {
+ return err
+ }
+
+ err = cache.FlushAll(ctx).Err()
+ if err != nil {
+ return err
+ }
+
+ err = memory.Reset()
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
diff --git a/api/core/joke/setter.go b/api/core/joke/setter.go
new file mode 100644
index 0000000..c8ef51a
--- /dev/null
+++ b/api/core/joke/setter.go
@@ -0,0 +1,134 @@
+package joke
+
+import (
+ "context"
+ "jokes-bapak2-api/core/schema"
+
+ "github.com/Masterminds/squirrel"
+ "github.com/allegro/bigcache/v3"
+ "github.com/jackc/pgx/v4/pgxpool"
+ "github.com/pquerna/ffjson/ffjson"
+)
+
+// SetAllJSONJoke fetches jokes data from GetAllJSONJokes then set it to memory cache.
+func SetAllJSONJoke(db *pgxpool.Pool, ctx context.Context, memory *bigcache.BigCache) error {
+ jokes, err := GetAllJSONJokes(db, ctx)
+ if err != nil {
+ return err
+ }
+ err = memory.Set("jokes", jokes)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func SetTotalJoke(db *pgxpool.Pool, ctx context.Context, memory *bigcache.BigCache) error {
+ check, err := CheckJokesCache(memory)
+ if err != nil {
+ return err
+ }
+
+ if !check {
+ err = SetAllJSONJoke(db, ctx, memory)
+ if err != nil {
+ return err
+ }
+ }
+
+ jokes, err := memory.Get("jokes")
+ if err != nil {
+ return err
+ }
+
+ var data []schema.Joke
+ err = ffjson.Unmarshal(jokes, &data)
+ if err != nil {
+ return err
+ }
+
+ var total = []byte{byte(len(data))}
+ err = memory.Set("total", total)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func InsertJokeIntoDB(db *pgxpool.Pool, ctx context.Context, joke schema.Joke) error {
+ conn, err := db.Acquire(ctx)
+ if err != nil {
+ return err
+ }
+ defer conn.Release()
+
+ var query = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
+ sql, args, err := query.
+ Insert("jokesbapak2").
+ Columns("link", "creator").
+ Values(joke.Link, joke.Creator).
+ ToSql()
+ if err != nil {
+ return err
+ }
+
+ r, err := conn.Query(ctx, sql, args...)
+ if err != nil {
+ return err
+ }
+ defer r.Close()
+ return nil
+}
+
+func DeleteSingleJoke(db *pgxpool.Pool, ctx context.Context, id int) error {
+ conn, err := db.Acquire(ctx)
+ if err != nil {
+ return err
+ }
+ defer conn.Release()
+
+ var query = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
+ sql, args, err := query.
+ Delete("jokesbapak2").
+ Where(squirrel.Eq{"id": id}).
+ ToSql()
+ if err != nil {
+ return err
+ }
+
+ r, err := conn.Query(ctx, sql, args...)
+ if err != nil {
+ return err
+ }
+ defer r.Close()
+
+ return nil
+}
+
+func UpdateJoke(db *pgxpool.Pool, ctx context.Context, newJoke schema.Joke) error {
+ conn, err := db.Acquire(ctx)
+ if err != nil {
+ return err
+ }
+ defer conn.Release()
+
+ var query = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
+ sql, args, err := query.
+ Update("jokesbapak2").
+ Set("link", newJoke.Link).
+ Set("creator", newJoke.Creator).
+ Where(squirrel.Eq{"id": newJoke.ID}).
+ ToSql()
+ if err != nil {
+ return err
+ }
+
+ r, err := conn.Query(ctx, sql, args...)
+ if err != nil {
+ return err
+ }
+ defer r.Close()
+
+ return nil
+}
diff --git a/api/core/joke/setter_test.go b/api/core/joke/setter_test.go
new file mode 100644
index 0000000..eb24245
--- /dev/null
+++ b/api/core/joke/setter_test.go
@@ -0,0 +1,238 @@
+package joke_test
+
+import (
+ "context"
+ "jokes-bapak2-api/core/joke"
+ "jokes-bapak2-api/core/schema"
+ "testing"
+ "time"
+
+ "github.com/jackc/pgx/v4"
+)
+
+func TestSetAllJSONJoke(t *testing.T) {
+ ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(30*time.Second))
+ defer cancel()
+
+ defer Flush()
+
+ conn, err := db.Acquire(ctx)
+ if err != nil {
+ t.Error("an error was thrown:", err)
+ }
+ defer conn.Release()
+
+ err = conn.BeginFunc(ctx, func(t pgx.Tx) error {
+ _, err := t.Exec(
+ ctx,
+ `INSERT INTO "administrators"
+ (id, key, token, last_used)
+ VALUES
+ ($1, $2, $3, $4),
+ ($5, $6, $7, $8)`,
+ administratorsData...,
+ )
+ if err != nil {
+ return err
+ }
+ _, err = t.Exec(
+ ctx,
+ `INSERT INTO "jokesbapak2"
+ (id, link, creator)
+ VALUES
+ ($1, $2, $3),
+ ($4, $5, $6),
+ ($7, $8, $9)`,
+ jokesData...,
+ )
+ if err != nil {
+ return err
+ }
+
+ return nil
+ })
+ if err != nil {
+ t.Error("an error was thrown:", err)
+ }
+
+ err = joke.SetAllJSONJoke(db, ctx, memory)
+ if err != nil {
+ t.Error("an error was thrown:", err)
+ }
+}
+
+func TestSetTotalJoke(t *testing.T) {
+ ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(30*time.Second))
+ defer cancel()
+
+ defer Flush()
+
+ conn, err := db.Acquire(ctx)
+ if err != nil {
+ t.Error("an error was thrown:", err)
+ }
+ defer conn.Release()
+
+ err = conn.BeginFunc(ctx, func(t pgx.Tx) error {
+ _, err := t.Exec(
+ ctx,
+ `INSERT INTO "administrators"
+ (id, key, token, last_used)
+ VALUES
+ ($1, $2, $3, $4),
+ ($5, $6, $7, $8)`,
+ administratorsData...,
+ )
+ if err != nil {
+ return err
+ }
+ _, err = t.Exec(
+ ctx,
+ `INSERT INTO "jokesbapak2"
+ (id, link, creator)
+ VALUES
+ ($1, $2, $3),
+ ($4, $5, $6),
+ ($7, $8, $9)`,
+ jokesData...,
+ )
+ if err != nil {
+ return err
+ }
+
+ return nil
+ })
+ if err != nil {
+ t.Error("an error was thrown:", err)
+ }
+
+ err = joke.SetTotalJoke(db, ctx, memory)
+ if err != nil {
+ t.Error("an error was thrown:", err)
+ }
+}
+
+func TestInsertJokeIntoDB(t *testing.T) {
+ ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(30*time.Second))
+ defer cancel()
+
+ defer Flush()
+
+ data := schema.Joke{
+ ID: 1,
+ Link: "link1",
+ Creator: 1,
+ }
+ err := joke.InsertJokeIntoDB(db, ctx, data)
+ if err != nil {
+ t.Error("an error was thrown:", err)
+ }
+}
+
+func TestDeleteSingleJoke(t *testing.T) {
+ ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(30*time.Second))
+ defer cancel()
+
+ defer Flush()
+
+ conn, err := db.Acquire(ctx)
+ if err != nil {
+ t.Error("an error was thrown:", err)
+ }
+ defer conn.Release()
+
+ err = conn.BeginFunc(ctx, func(t pgx.Tx) error {
+ _, err := t.Exec(
+ ctx,
+ `INSERT INTO "administrators"
+ (id, key, token, last_used)
+ VALUES
+ ($1, $2, $3, $4),
+ ($5, $6, $7, $8)`,
+ administratorsData...,
+ )
+ if err != nil {
+ return err
+ }
+ _, err = t.Exec(
+ ctx,
+ `INSERT INTO "jokesbapak2"
+ (id, link, creator)
+ VALUES
+ ($1, $2, $3),
+ ($4, $5, $6),
+ ($7, $8, $9)`,
+ jokesData...,
+ )
+ if err != nil {
+ return err
+ }
+
+ return nil
+ })
+ if err != nil {
+ t.Error("an error was thrown:", err)
+ }
+
+ err = joke.DeleteSingleJoke(db, ctx, 1)
+ if err != nil {
+ t.Error("an error was thrown:", err)
+ }
+}
+
+func TestUpdateJoke(t *testing.T) {
+ ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(30*time.Second))
+ defer cancel()
+
+ defer Flush()
+
+ conn, err := db.Acquire(ctx)
+ if err != nil {
+ t.Error("an error was thrown:", err)
+ }
+ defer conn.Release()
+
+ err = conn.BeginFunc(ctx, func(t pgx.Tx) error {
+ _, err := t.Exec(
+ ctx,
+ `INSERT INTO "administrators"
+ (id, key, token, last_used)
+ VALUES
+ ($1, $2, $3, $4),
+ ($5, $6, $7, $8)`,
+ administratorsData...,
+ )
+ if err != nil {
+ return err
+ }
+ _, err = t.Exec(
+ ctx,
+ `INSERT INTO "jokesbapak2"
+ (id, link, creator)
+ VALUES
+ ($1, $2, $3),
+ ($4, $5, $6),
+ ($7, $8, $9)`,
+ jokesData...,
+ )
+ if err != nil {
+ return err
+ }
+
+ return nil
+ })
+ if err != nil {
+ t.Error("an error was thrown:", err)
+ }
+
+ newJoke := schema.Joke{
+ ID: 1,
+ Link: "link1",
+ Creator: 1,
+ }
+
+ err = joke.UpdateJoke(db, ctx, newJoke)
+ if err != nil {
+ t.Error("an error was thrown:", err)
+ }
+}
diff --git a/api/core/schema/err.go b/api/core/schema/err.go
new file mode 100644
index 0000000..055b60a
--- /dev/null
+++ b/api/core/schema/err.go
@@ -0,0 +1,10 @@
+package schema
+
+import "errors"
+
+var ErrNotFound = errors.New("record not found")
+var ErrEmpty = errors.New("record is empty")
+
+type Error struct {
+ Error string `json:"error"`
+}
diff --git a/api/core/schema/image_api.go b/api/core/schema/image_api.go
new file mode 100644
index 0000000..6c3c25f
--- /dev/null
+++ b/api/core/schema/image_api.go
@@ -0,0 +1,15 @@
+package schema
+
+type ImageAPI struct {
+ Data ImageAPIData `json:"data"`
+ Success bool `json:"success"`
+ Status int `json:"status"`
+}
+
+type ImageAPIData struct {
+ ID string `json:"id"`
+ Title string `json:"title"`
+ URLViewer string `json:"url_viewer"`
+ URL string `json:"url"`
+ DisplayURL string `json:"display_url"`
+}
diff --git a/api/core/schema/joke.go b/api/core/schema/joke.go
new file mode 100644
index 0000000..69a9914
--- /dev/null
+++ b/api/core/schema/joke.go
@@ -0,0 +1,7 @@
+package schema
+
+type Joke struct {
+ ID int `json:"id" form:"id" db:"id"`
+ Link string `json:"link" form:"link" db:"link"`
+ Creator int `json:"creator" form:"creator" db:"creator"`
+}
diff --git a/api/app/v1/models/submit.go b/api/core/schema/submit.go
similarity index 52%
rename from api/app/v1/models/submit.go
rename to api/core/schema/submit.go
index ded4eef..16b2466 100644
--- a/api/app/v1/models/submit.go
+++ b/api/core/schema/submit.go
@@ -1,4 +1,4 @@
-package models
+package schema
type Submission struct {
ID int `json:"id,omitempty" db:"id"`
@@ -17,21 +17,8 @@ type SubmissionQuery struct {
}
type ResponseSubmission struct {
- ID string `json:"id,omitempty"`
- Message string `json:"message,omitempty"`
- Data Submission `json:"data,omitempty"`
-}
-
-type ImageAPI struct {
- Data ImageAPIData `json:"data"`
- Success bool `json:"success"`
- Status int `json:"status"`
-}
-
-type ImageAPIData struct {
- ID string `json:"id"`
- Title string `json:"title"`
- URLViewer string `json:"url_viewer"`
- URL string `json:"url"`
- DisplayURL string `json:"display_url"`
+ ID string `json:"id,omitempty"`
+ Message string `json:"message,omitempty"`
+ Submission Submission `json:"submission,omitempty"`
+ AuthorPage string `json:"author_page,omitempty"`
}
diff --git a/api/core/submit/getter.go b/api/core/submit/getter.go
new file mode 100644
index 0000000..a447a62
--- /dev/null
+++ b/api/core/submit/getter.go
@@ -0,0 +1,115 @@
+package submit
+
+import (
+ "context"
+ "jokes-bapak2-api/core/schema"
+ "net/url"
+ "strconv"
+ "strings"
+
+ "github.com/aldy505/bob"
+ "github.com/georgysavva/scany/pgxscan"
+ "github.com/jackc/pgx/v4/pgxpool"
+)
+
+func GetSubmittedItems(db *pgxpool.Pool, ctx context.Context, queries schema.SubmissionQuery) ([]schema.Submission, error) {
+ var err error
+ var limit int
+ var offset int
+ var approved bool
+
+ if queries.Limit != "" {
+ limit, err = strconv.Atoi(queries.Limit)
+ if err != nil {
+ return []schema.Submission{}, err
+
+ }
+ }
+ if queries.Page != "" {
+ page, err := strconv.Atoi(queries.Page)
+ if err != nil {
+ return []schema.Submission{}, err
+
+ }
+ offset = (page - 1) * 20
+ }
+
+ if queries.Approved != "" {
+ approved, err = strconv.ParseBool(queries.Approved)
+ if err != nil {
+ return []schema.Submission{}, err
+
+ }
+ }
+
+ var status int
+
+ if approved {
+ status = 1
+ } else {
+ status = 0
+ }
+
+ sql, args, err := GetterQueryBuilder(queries, status, limit, offset)
+ if err != nil {
+ return []schema.Submission{}, err
+
+ }
+
+ conn, err := db.Acquire(ctx)
+ if err != nil {
+ return []schema.Submission{}, err
+ }
+ defer conn.Release()
+
+ var submissions []schema.Submission
+ results, err := conn.Query(ctx, sql, args...)
+ if err != nil {
+ return []schema.Submission{}, err
+ }
+ defer results.Close()
+
+ err = pgxscan.ScanAll(&submissions, results)
+ if err != nil {
+ return []schema.Submission{}, err
+ }
+
+ return submissions, nil
+}
+
+func GetterQueryBuilder(queries schema.SubmissionQuery, status, limit, offset int) (string, []interface{}, error) {
+ var sql string
+ var args []interface{}
+ var sqlQuery strings.Builder
+
+ sqlQuery.WriteString("SELECT * FROM submission WHERE TRUE")
+
+ if queries.Author != "" {
+ sqlQuery.WriteString(" AND author = ?")
+ escapedAuthor, err := url.QueryUnescape(queries.Author)
+ if err != nil {
+ return sql, args, err
+
+ }
+ args = append(args, escapedAuthor)
+ }
+
+ if queries.Approved != "" {
+ sqlQuery.WriteString(" AND status = ?")
+ args = append(args, status)
+ }
+
+ if limit > 0 {
+ sqlQuery.WriteString(" LIMIT " + strconv.Itoa(limit))
+ } else {
+ sqlQuery.WriteString(" LIMIT 20")
+ }
+
+ if queries.Page != "" {
+ sqlQuery.WriteString(" OFFSET " + strconv.Itoa(offset))
+ }
+
+ sql = bob.ReplacePlaceholder(sqlQuery.String(), bob.Dollar)
+
+ return sql, args, nil
+}
diff --git a/api/core/submit/getter_test.go b/api/core/submit/getter_test.go
new file mode 100644
index 0000000..8a5e759
--- /dev/null
+++ b/api/core/submit/getter_test.go
@@ -0,0 +1,68 @@
+package submit_test
+
+import (
+ "context"
+ "jokes-bapak2-api/core/schema"
+ "jokes-bapak2-api/core/submit"
+ "testing"
+ "time"
+)
+
+func TestGetSubmittedItems(t *testing.T) {
+ ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(30*time.Second))
+ defer cancel()
+
+ defer Flush()
+
+ c, err := db.Acquire(ctx)
+ if err != nil {
+ t.Error("an error was thrown:", err)
+ }
+ defer c.Release()
+
+ _, err = c.Exec(ctx, "INSERT INTO submission (id, link, created_at, author, status) VALUES ($1, $2, $3, $4, $5), ($6, $7, $8, $9, $10)", submissionData...)
+ if err != nil {
+ t.Error("an error was thrown:", err)
+ }
+
+ items, err := submit.GetSubmittedItems(db, ctx, schema.SubmissionQuery{})
+ if err != nil {
+ t.Error("an error was thrown:", err)
+ }
+
+ if len(items) != 2 {
+ t.Error("expected 2 items, got", len(items))
+ }
+}
+
+func TestGetterQueryBuilder(t *testing.T) {
+ s, _, err := submit.GetterQueryBuilder(schema.SubmissionQuery{}, 0, 0, 0)
+ if err != nil {
+ t.Error("an error was thrown:", err)
+ }
+
+ if s != "SELECT * FROM submission WHERE TRUE LIMIT 20" {
+ t.Error("expected query to be", "SELECT * FROM submission WHERE TRUE LIMIT 20", "got", s)
+ }
+
+ s, i, err := submit.GetterQueryBuilder(schema.SubmissionQuery{
+ Author: "Test ",
+ Approved: "true",
+ Page: "2",
+ }, 2, 15, 10)
+ if err != nil {
+ t.Error("an error was thrown:", err)
+ }
+
+ if s != "SELECT * FROM submission WHERE TRUE AND author = $1 AND status = $2 LIMIT 15 OFFSET 10" {
+ t.Error("expected query to be", "SELECT * FROM submission WHERE TRUE AND author = $1 AND status = $2 LIMIT 15 OFFSET 15", "got:", s)
+ }
+
+ if i[0].(string) != "Test " {
+ t.Error("expected first arg to be Test , got:", i[0].(string))
+ }
+
+ if i[1].(int) != 2 {
+ t.Error("expected second arg to be 1, got:", i[1].(int))
+ }
+}
diff --git a/api/core/submit/init_test.go b/api/core/submit/init_test.go
new file mode 100644
index 0000000..2901baa
--- /dev/null
+++ b/api/core/submit/init_test.go
@@ -0,0 +1,128 @@
+package submit_test
+
+import (
+ "context"
+ "os"
+ "testing"
+ "time"
+
+ "github.com/jackc/pgx/v4/pgxpool"
+)
+
+var db *pgxpool.Pool
+
+var submissionData = []interface{}{
+ 1, "https://via.placeholder.com/300/01f/fff.png", "2021-08-03T18:20:38Z", "Test ", 0,
+ 2, "https://via.placeholder.com/300/02f/fff.png", "2021-08-04T18:20:38Z", "Test ", 1,
+}
+
+func TestMain(m *testing.M) {
+ defer Teardown()
+ Setup()
+ time.Sleep(3 * time.Second)
+
+ os.Exit(m.Run())
+}
+
+func Setup() {
+ ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(1*time.Minute))
+ defer cancel()
+
+ poolConfig, err := pgxpool.ParseConfig(os.Getenv("DATABASE_URL"))
+ if err != nil {
+ panic(err)
+ }
+
+ db, err = pgxpool.ConnectConfig(ctx, poolConfig)
+ if err != nil {
+ panic(err)
+ }
+
+ conn, err := db.Acquire(ctx)
+ if err != nil {
+ panic(err)
+ }
+ defer conn.Release()
+
+ tx, err := conn.Begin(ctx)
+ if err != nil {
+ panic(err)
+ }
+ defer tx.Rollback(ctx)
+
+ _, err = tx.Exec(
+ ctx,
+ `CREATE TABLE IF NOT EXISTS submission (
+ id SERIAL PRIMARY KEY,
+ link VARCHAR(255) UNIQUE NOT NULL,
+ created_at VARCHAR(255),
+ author VARCHAR(255) NOT NULL,
+ status SMALLINT DEFAULT 0
+ )`,
+ )
+ if err != nil {
+ panic(err)
+ }
+
+ err = tx.Commit(ctx)
+ if err != nil {
+ panic(err)
+ }
+}
+
+func Teardown() (err error) {
+ ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(30*time.Second))
+ defer cancel()
+
+ defer db.Close()
+
+ conn, err := db.Acquire(ctx)
+ if err != nil {
+ return err
+ }
+ defer conn.Release()
+
+ tx, err := conn.Begin(ctx)
+ if err != nil {
+ return err
+ }
+ defer tx.Rollback(ctx)
+
+ _, err = tx.Exec(ctx, "DROP TABLE IF EXISTS submission CASCADE")
+ if err != nil {
+ return err
+ }
+ _, err = tx.Exec(ctx, "DROP TABLE IF EXISTS jokesbapak2 CASCADE")
+ if err != nil {
+ return err
+ }
+ _, err = tx.Exec(ctx, "DROP TABLE IF EXISTS administrators CASCADE")
+ if err != nil {
+ return err
+ }
+
+ err = tx.Commit(ctx)
+ if err != nil {
+ return err
+ }
+
+ return
+}
+
+func Flush() error {
+ ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(30*time.Second))
+ defer cancel()
+
+ conn, err := db.Acquire(ctx)
+ if err != nil {
+ return err
+ }
+ defer conn.Release()
+
+ _, err = conn.Exec(ctx, "TRUNCATE TABLE submission RESTART IDENTITY CASCADE")
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
diff --git a/api/app/v1/core/submit_setter.go b/api/core/submit/setter.go
similarity index 57%
rename from api/app/v1/core/submit_setter.go
rename to api/core/submit/setter.go
index 231c704..74e6155 100644
--- a/api/app/v1/core/submit_setter.go
+++ b/api/core/submit/setter.go
@@ -1,17 +1,22 @@
-package core
+package submit
import (
"bytes"
+ "context"
"io"
"io/ioutil"
- "jokes-bapak2-api/app/v1/models"
- "jokes-bapak2-api/app/v1/utils"
+ "jokes-bapak2-api/core/schema"
+ "jokes-bapak2-api/utils"
"mime/multipart"
"net/http"
"net/url"
"os"
+ "time"
+ "github.com/Masterminds/squirrel"
+ "github.com/georgysavva/scany/pgxscan"
"github.com/gojek/heimdall/v7/httpclient"
+ "github.com/jackc/pgx/v4/pgxpool"
"github.com/pquerna/ffjson/ffjson"
)
@@ -71,7 +76,7 @@ func UploadImage(client *httpclient.Client, image io.Reader) (string, error) {
return "", err
}
- var data models.ImageAPI
+ var data schema.ImageAPI
err = ffjson.Unmarshal(responseBody, &data)
if err != nil {
return "", err
@@ -79,3 +84,39 @@ func UploadImage(client *httpclient.Client, image io.Reader) (string, error) {
return data.Data.URL, nil
}
+
+func SubmitJoke(db *pgxpool.Pool, ctx context.Context, s schema.Submission, link string) (schema.Submission, error) {
+ var query = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
+
+ conn, err := db.Acquire(ctx)
+ if err != nil {
+ return schema.Submission{}, err
+ }
+ defer conn.Release()
+
+ now := time.Now().UTC().Format(time.RFC3339)
+
+ sql, args, err := query.
+ Insert("submission").
+ Columns("link", "created_at", "author").
+ Values(link, now, s.Author).
+ Suffix("RETURNING id,created_at,link,author,status").
+ ToSql()
+ if err != nil {
+ return schema.Submission{}, err
+ }
+
+ var submission schema.Submission
+ result, err := conn.Query(ctx, sql, args...)
+ if err != nil {
+ return schema.Submission{}, err
+ }
+ defer result.Close()
+
+ err = pgxscan.ScanOne(&submission, result)
+ if err != nil {
+ return schema.Submission{}, err
+ }
+
+ return submission, nil
+}
diff --git a/api/core/submit/setter_test.go b/api/core/submit/setter_test.go
new file mode 100644
index 0000000..524f18c
--- /dev/null
+++ b/api/core/submit/setter_test.go
@@ -0,0 +1,25 @@
+package submit_test
+
+import (
+ "context"
+ "jokes-bapak2-api/core/schema"
+ "jokes-bapak2-api/core/submit"
+ "testing"
+ "time"
+)
+
+func TestSubmitJoke(t *testing.T) {
+ ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(30*time.Second))
+ defer cancel()
+
+ defer Flush()
+
+ s, err := submit.SubmitJoke(db, ctx, schema.Submission{Author: "Test "}, "https://example.net/img.png")
+ if err != nil {
+ t.Error("an error was thrown:", err)
+ }
+
+ if s.Link != "https://example.net/img.png" {
+ t.Error("link is not correct, got:", s.Link)
+ }
+}
diff --git a/api/app/v1/core/submit_validation.go b/api/core/validator/author.go
similarity index 97%
rename from api/app/v1/core/submit_validation.go
rename to api/core/validator/author.go
index a1e23af..afc7d43 100644
--- a/api/app/v1/core/submit_validation.go
+++ b/api/core/validator/author.go
@@ -1,4 +1,4 @@
-package core
+package validator
import (
"regexp"
diff --git a/api/core/validator/author_test.go b/api/core/validator/author_test.go
new file mode 100644
index 0000000..cc1408b
--- /dev/null
+++ b/api/core/validator/author_test.go
@@ -0,0 +1,40 @@
+package validator_test
+
+import (
+ "jokes-bapak2-api/core/validator"
+ "testing"
+)
+
+func TestValidateAuthor_False(t *testing.T) {
+ v := validator.ValidateAuthor("Test Author")
+ if v != false {
+ t.Error("Expected false, got true")
+ }
+
+ v = validator.ValidateAuthor("Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. Donec.")
+ if v != false {
+ t.Error("Expected false, got true")
+ }
+
+ v = validator.ValidateAuthor("")
+ if v != false {
+ t.Error("Expected false, got true")
+ }
+
+ v = validator.ValidateAuthor("Test ")
+ if v != false {
+ t.Error("Expected false, got true")
+ }
+}
+
+func TestValidateAuthor_True(t *testing.T) {
+ v := validator.ValidateAuthor("Test Author ")
+ if v != true {
+ t.Error("Expected true, got false")
+ }
+}
diff --git a/api/app/v1/core/joke_validation.go b/api/core/validator/image.go
similarity index 94%
rename from api/app/v1/core/joke_validation.go
rename to api/core/validator/image.go
index 9d47236..eff0a7a 100644
--- a/api/app/v1/core/joke_validation.go
+++ b/api/core/validator/image.go
@@ -1,8 +1,8 @@
-package core
+package validator
import (
"errors"
- "jokes-bapak2-api/app/v1/utils"
+ "jokes-bapak2-api/utils"
"net/http"
"strings"
diff --git a/api/core/validator/image_test.go b/api/core/validator/image_test.go
new file mode 100644
index 0000000..8bf4143
--- /dev/null
+++ b/api/core/validator/image_test.go
@@ -0,0 +1,50 @@
+package validator_test
+
+import (
+ "jokes-bapak2-api/core/validator"
+ "testing"
+
+ "github.com/gojek/heimdall/v7/httpclient"
+)
+
+func TestCheckImageValidity_Error(t *testing.T) {
+ client := httpclient.NewClient()
+ b, err := validator.CheckImageValidity(client, "http://lorem-ipsum")
+ if err == nil {
+ t.Error("Expected error, got nil")
+ }
+
+ if b {
+ t.Error("Expected false, got true")
+ }
+
+ if err.Error() != "URL must use HTTPS protocol" {
+ t.Error("Expected error to be URL must use HTTPS protocol, got:", err)
+ }
+}
+
+func TestCheckImageValidity_False(t *testing.T) {
+ client := httpclient.NewClient()
+
+ b, err := validator.CheckImageValidity(client, "https://www.youtube.com/watch?v=yTJV6T37Reo")
+ if err != nil {
+ t.Error("Expected nil, got error")
+ }
+
+ if b {
+ t.Error("Expected false, got true")
+ }
+}
+
+func TestCheckImageValidity_True(t *testing.T) {
+ client := httpclient.NewClient()
+
+ b, err := validator.CheckImageValidity(client, "https://i.ytimg.com/vi/yTJV6T37Reo/maxresdefault.jpg")
+ if err != nil {
+ t.Error("Expected nil, got error")
+ }
+
+ if !b {
+ t.Error("Expected true, got false")
+ }
+}
diff --git a/api/core/validator/joke.go b/api/core/validator/joke.go
new file mode 100644
index 0000000..ab1dde1
--- /dev/null
+++ b/api/core/validator/joke.go
@@ -0,0 +1,69 @@
+package validator
+
+import (
+ "context"
+ "errors"
+
+ "github.com/Masterminds/squirrel"
+ "github.com/jackc/pgx/v4"
+ "github.com/jackc/pgx/v4/pgxpool"
+)
+
+// Validate if link already exists
+func JokeLinkExists(db *pgxpool.Pool, ctx context.Context, link string) (bool, error) {
+ conn, err := db.Acquire(ctx)
+ if err != nil {
+ return false, err
+ }
+ defer conn.Release()
+
+ var query = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
+
+ sql, args, err := query.
+ Select("link").
+ From("jokesbapak2").
+ Where(squirrel.Eq{"link": link}).
+ ToSql()
+ if err != nil {
+ return false, err
+ }
+
+ var validateLink string
+ err = conn.QueryRow(ctx, sql, args...).Scan(&validateLink)
+ if err != nil && err != pgx.ErrNoRows {
+ return false, err
+ }
+
+ return validateLink != "", nil
+}
+
+// Check if the joke exists
+func JokeIDExists(db *pgxpool.Pool, ctx context.Context, id int) (bool, error) {
+ conn, err := db.Acquire(ctx)
+ if err != nil {
+ return false, err
+ }
+ defer conn.Release()
+
+ var query = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
+ sql, args, err := query.
+ Select("id").
+ From("jokesbapak2").
+ Where(squirrel.Eq{"id": id}).
+ ToSql()
+ if err != nil {
+ return false, err
+ }
+
+ var jokeID int
+ err = conn.QueryRow(ctx, sql, args...).Scan(&jokeID)
+ if err != nil && !errors.Is(err, pgx.ErrNoRows) {
+ return false, err
+ }
+
+ if errors.Is(err, pgx.ErrNoRows) {
+ return false, nil
+ }
+
+ return true, nil
+}
diff --git a/api/core/validator/submit.go b/api/core/validator/submit.go
new file mode 100644
index 0000000..c8552c9
--- /dev/null
+++ b/api/core/validator/submit.go
@@ -0,0 +1,38 @@
+package validator
+
+import (
+ "context"
+
+ "github.com/Masterminds/squirrel"
+ "github.com/jackc/pgx/v4"
+ "github.com/jackc/pgx/v4/pgxpool"
+)
+
+func SubmitLinkExists(db *pgxpool.Pool, ctx context.Context, query squirrel.StatementBuilderType, link string) (bool, error) {
+ conn, err := db.Acquire(ctx)
+ if err != nil {
+ return false, err
+ }
+ defer conn.Release()
+
+ sql, args, err := query.
+ Select("link").
+ From("submission").
+ Where(squirrel.Eq{"link": link}).
+ ToSql()
+ if err != nil {
+ return false, err
+ }
+
+ var validateLink string
+ err = conn.QueryRow(ctx, sql, args...).Scan(&validateLink)
+ if err != nil && err != pgx.ErrNoRows {
+ return false, err
+ }
+
+ if err == nil && validateLink != "" {
+ return true, nil
+ }
+
+ return false, nil
+}
diff --git a/api/app/v1/documentation.json b/api/documentation.json
similarity index 99%
rename from api/app/v1/documentation.json
rename to api/documentation.json
index 827aded..958db5c 100644
--- a/api/app/v1/documentation.json
+++ b/api/documentation.json
@@ -19,6 +19,10 @@
"url": "https://jokesbapak2.herokuapp.com/v1",
"description": "Production"
},
+ {
+ "url": "https://jokesbapak2.herokuapp.com",
+ "description": "Production"
+ },
{
"url": "http://localhost:5000",
"description": "Development"
diff --git a/api/app/v1/documentation.yaml b/api/documentation.yaml
similarity index 98%
rename from api/app/v1/documentation.yaml
rename to api/documentation.yaml
index d15d255..4d67392 100644
--- a/api/app/v1/documentation.yaml
+++ b/api/documentation.yaml
@@ -16,6 +16,8 @@ info:
servers:
- url: "https://jokesbapak2.herokuapp.com/v1"
description: Production
+ - url: "https://jokesbapak2.herokuapp.com"
+ description: Production
- url: "http://localhost:5000"
description: Development
paths:
diff --git a/api/go.mod b/api/go.mod
index 49da1b8..1d66845 100644
--- a/api/go.mod
+++ b/api/go.mod
@@ -1,6 +1,6 @@
module jokes-bapak2-api
-go 1.16
+go 1.17
require (
github.com/Masterminds/squirrel v1.5.0
@@ -13,14 +13,44 @@ require (
github.com/go-redis/redis/v8 v8.11.0
github.com/gofiber/fiber/v2 v2.15.0
github.com/gojek/heimdall/v7 v7.0.2
+ github.com/jackc/pgx v3.6.2+incompatible
github.com/jackc/pgx/v4 v4.12.0
github.com/joho/godotenv v1.3.0
github.com/kr/text v0.2.0 // indirect
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
github.com/pquerna/ffjson v0.0.0-20190930134022-aa0246cd15f7
- github.com/stretchr/testify v1.7.0
+ github.com/stretchr/testify v1.7.0 // indirect
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
)
+
+require (
+ github.com/andybalholm/brotli v1.0.2 // indirect
+ github.com/cespare/xxhash/v2 v2.1.1 // indirect
+ github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
+ github.com/fsnotify/fsnotify v1.5.1 // indirect
+ github.com/gojek/valkyrie v0.0.0-20180215180059-6aee720afcdf // indirect
+ github.com/jackc/chunkreader/v2 v2.0.1 // indirect
+ github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 // indirect
+ github.com/jackc/pgconn v1.9.0 // indirect
+ github.com/jackc/pgio v1.0.0 // indirect
+ github.com/jackc/pgpassfile v1.0.0 // indirect
+ github.com/jackc/pgproto3/v2 v2.1.1 // indirect
+ github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
+ github.com/jackc/pgtype v1.8.0 // indirect
+ github.com/jackc/puddle v1.1.3 // indirect
+ github.com/klauspost/compress v1.12.2 // indirect
+ github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
+ github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
+ github.com/pkg/errors v0.9.1 // indirect
+ github.com/pmezard/go-difflib v1.0.0 // indirect
+ github.com/stretchr/objx v0.3.0 // indirect
+ github.com/valyala/bytebufferpool v1.0.0 // indirect
+ github.com/valyala/fasthttp v1.26.0 // indirect
+ github.com/valyala/tcplisten v1.0.0 // indirect
+ golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e // indirect
+ golang.org/x/text v0.3.6 // indirect
+)
diff --git a/api/go.sum b/api/go.sum
index baaf628..0148694 100644
--- a/api/go.sum
+++ b/api/go.sum
@@ -98,8 +98,9 @@ github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga
github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
-github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
+github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI=
+github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc=
github.com/georgysavva/scany v0.2.9 h1:Xt6rjYpHnMClTm/g+oZTnoSxUwiln5GqMNU+QeLNHQU=
github.com/georgysavva/scany v0.2.9/go.mod h1:yeOeC1BdIdl6hOwy8uefL2WNSlseFzbhlG/frrh65SA=
@@ -214,6 +215,8 @@ github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
+github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 h1:vr3AYkKovP8uR8AvSGGUK1IDqRa5lAAvEkZG1LKaCRc=
+github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80sQsxDoMokWK1W5TQtxBFNpzWTD84ibQ=
github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
diff --git a/api/handler/health/health.go b/api/handler/health/health.go
new file mode 100644
index 0000000..fd8d767
--- /dev/null
+++ b/api/handler/health/health.go
@@ -0,0 +1,40 @@
+package health
+
+import (
+ "github.com/go-redis/redis/v8"
+ "github.com/gofiber/fiber/v2"
+ "github.com/jackc/pgx/v4/pgxpool"
+)
+
+type Dependencies struct {
+ DB *pgxpool.Pool
+ Redis *redis.Client
+}
+
+func (d *Dependencies) Health(c *fiber.Ctx) error {
+ conn, err := d.DB.Acquire(c.Context())
+ if err != nil {
+ return err
+ }
+ defer conn.Release()
+
+ // Ping REDIS database
+ err = d.Redis.Ping(c.Context()).Err()
+ if err != nil {
+ return c.
+ Status(fiber.StatusServiceUnavailable).
+ JSON(Error{
+ Error: "REDIS: " + err.Error(),
+ })
+ }
+
+ _, err = conn.Query(c.Context(), "SELECT \"id\" FROM \"jokesbapak2\" LIMIT 1")
+ if err != nil {
+ return c.
+ Status(fiber.StatusServiceUnavailable).
+ JSON(Error{
+ Error: "POSTGRESQL: " + err.Error(),
+ })
+ }
+ return c.SendStatus(fiber.StatusOK)
+}
diff --git a/api/handler/health/schema.go b/api/handler/health/schema.go
new file mode 100644
index 0000000..cb99e1f
--- /dev/null
+++ b/api/handler/health/schema.go
@@ -0,0 +1,5 @@
+package health
+
+type Error struct {
+ Error string `json:"error"`
+}
diff --git a/api/handler/joke/dependencies.go b/api/handler/joke/dependencies.go
new file mode 100644
index 0000000..75d0b4c
--- /dev/null
+++ b/api/handler/joke/dependencies.go
@@ -0,0 +1,17 @@
+package joke
+
+import (
+ "github.com/Masterminds/squirrel"
+ "github.com/allegro/bigcache/v3"
+ "github.com/go-redis/redis/v8"
+ "github.com/gojek/heimdall/v7/httpclient"
+ "github.com/jackc/pgx/v4/pgxpool"
+)
+
+type Dependencies struct {
+ DB *pgxpool.Pool
+ Redis *redis.Client
+ Memory *bigcache.BigCache
+ HTTP *httpclient.Client
+ Query squirrel.StatementBuilderType
+}
diff --git a/api/handler/joke/joke_add.go b/api/handler/joke/joke_add.go
new file mode 100644
index 0000000..cfc13b7
--- /dev/null
+++ b/api/handler/joke/joke_add.go
@@ -0,0 +1,69 @@
+package joke
+
+import (
+ core "jokes-bapak2-api/core/joke"
+ "jokes-bapak2-api/core/schema"
+ "jokes-bapak2-api/core/validator"
+
+ "github.com/gofiber/fiber/v2"
+)
+
+func (d *Dependencies) AddNewJoke(c *fiber.Ctx) error {
+ var body schema.Joke
+ err := c.BodyParser(&body)
+ if err != nil {
+ return err
+ }
+
+ // Check link validity
+ valid, err := validator.CheckImageValidity(d.HTTP, body.Link)
+ if err != nil {
+ return err
+ }
+ if !valid {
+ return c.
+ Status(fiber.StatusBadRequest).
+ JSON(Error{
+ Error: "URL provided is not a valid image",
+ })
+ }
+
+ validateLink, err := validator.JokeLinkExists(d.DB, c.Context(), body.Link)
+ if err != nil {
+ return err
+ }
+
+ if !validateLink {
+ return c.Status(fiber.StatusConflict).JSON(Error{
+ Error: "Given link is already on the jokesbapak2 database",
+ })
+ }
+
+ err = core.InsertJokeIntoDB(
+ d.DB,
+ c.Context(),
+ schema.Joke{
+ Link: body.Link,
+ Creator: c.Locals("userID").(int),
+ },
+ )
+ if err != nil {
+ return err
+ }
+
+ err = core.SetAllJSONJoke(d.DB, c.Context(), d.Memory)
+ if err != nil {
+ return err
+ }
+
+ err = core.SetTotalJoke(d.DB, c.Context(), d.Memory)
+ if err != nil {
+ return err
+ }
+
+ return c.
+ Status(fiber.StatusCreated).
+ JSON(ResponseJoke{
+ Link: body.Link,
+ })
+}
diff --git a/api/handler/joke/joke_delete.go b/api/handler/joke/joke_delete.go
new file mode 100644
index 0000000..689ce63
--- /dev/null
+++ b/api/handler/joke/joke_delete.go
@@ -0,0 +1,51 @@
+package joke
+
+import (
+ core "jokes-bapak2-api/core/joke"
+ "jokes-bapak2-api/core/validator"
+ "strconv"
+
+ "github.com/gofiber/fiber/v2"
+)
+
+func (d *Dependencies) DeleteJoke(c *fiber.Ctx) error {
+ id, err := strconv.Atoi(c.Params("id"))
+ if err != nil {
+ return err
+ }
+
+ validate, err := validator.JokeIDExists(d.DB, c.Context(), id)
+ if err != nil {
+ return err
+ }
+
+ if validate {
+ return c.
+ Status(fiber.StatusNotAcceptable).
+ JSON(Error{
+ Error: "specified joke id does not exists",
+ })
+ }
+
+ err = core.DeleteSingleJoke(d.DB, c.Context(), id)
+ if err != nil {
+ return err
+ }
+
+ err = core.SetAllJSONJoke(d.DB, c.Context(), d.Memory)
+ if err != nil {
+ return err
+ }
+
+ err = core.SetTotalJoke(d.DB, c.Context(), d.Memory)
+ if err != nil {
+ return err
+ }
+
+ return c.
+ Status(fiber.StatusOK).
+ JSON(ResponseJoke{
+ Message: "specified joke id has been deleted",
+ })
+
+}
diff --git a/api/handler/joke/joke_get.go b/api/handler/joke/joke_get.go
new file mode 100644
index 0000000..ea75bfb
--- /dev/null
+++ b/api/handler/joke/joke_get.go
@@ -0,0 +1,151 @@
+package joke
+
+import (
+ "errors"
+ "io/ioutil"
+ core "jokes-bapak2-api/core/joke"
+ "jokes-bapak2-api/core/schema"
+ "jokes-bapak2-api/utils"
+ "strconv"
+ "time"
+
+ "github.com/gofiber/fiber/v2"
+)
+
+func (d *Dependencies) TodayJoke(c *fiber.Ctx) error {
+ // check from handler.Redis if today's joke already exists
+ // send the joke if exists
+ // get a new joke if it's not, then send it.
+ var joke Today
+ err := d.Redis.MGet(c.Context(), "today:link", "today:date", "today:image", "today:contentType").Scan(&joke)
+ if err != nil {
+ return err
+ }
+
+ eq, err := utils.IsToday(joke.Date)
+ if err != nil {
+ return err
+ }
+
+ if eq {
+ c.Set("Content-Type", joke.ContentType)
+ return c.Status(fiber.StatusOK).Send([]byte(joke.Image))
+ }
+
+ link, err := core.GetRandomJokeFromDB(d.DB, c.Context())
+ if err != nil {
+ return err
+ }
+
+ response, err := d.HTTP.Get(link, nil)
+ if err != nil {
+ return err
+ }
+
+ data, err := ioutil.ReadAll(response.Body)
+ if err != nil {
+ return err
+ }
+
+ now := time.Now().UTC().Format(time.RFC3339)
+ err = d.Redis.MSet(c.Context(), map[string]interface{}{
+ "today:link": link,
+ "today:date": now,
+ "today:image": string(data),
+ "today:contentType": response.Header.Get("content-type"),
+ }).Err()
+ if err != nil {
+ return err
+ }
+
+ c.Set("Content-Type", response.Header.Get("content-type"))
+ return c.Status(fiber.StatusOK).Send(data)
+}
+
+func (d *Dependencies) SingleJoke(c *fiber.Ctx) error {
+ checkCache, err := core.CheckJokesCache(d.Memory)
+ if err != nil {
+ return err
+ }
+
+ if !checkCache {
+ jokes, err := core.GetAllJSONJokes(d.DB, c.Context())
+ if err != nil {
+ return err
+ }
+
+ err = d.Memory.Set("jokes", jokes)
+ if err != nil {
+ return err
+ }
+ }
+
+ link, err := core.GetRandomJokeFromCache(d.Memory)
+ if err != nil && !errors.Is(err, schema.ErrEmpty) {
+ return err
+ }
+
+ // Get image data
+ response, err := d.HTTP.Get(link, nil)
+ if err != nil {
+ return err
+ }
+
+ data, err := ioutil.ReadAll(response.Body)
+ if err != nil {
+ return err
+ }
+
+ c.Set("Content-Type", response.Header.Get("content-type"))
+ return c.Status(fiber.StatusOK).Send(data)
+
+}
+
+func (d *Dependencies) JokeByID(c *fiber.Ctx) error {
+ checkCache, err := core.CheckJokesCache(d.Memory)
+ if err != nil {
+ return err
+ }
+
+ if !checkCache {
+ jokes, err := core.GetAllJSONJokes(d.DB, c.Context())
+ if err != nil {
+ return err
+ }
+
+ err = d.Memory.Set("jokes", jokes)
+ if err != nil {
+ return err
+ }
+ }
+
+ id, err := strconv.Atoi(c.Params("id"))
+ if err != nil {
+ return err
+ }
+
+ link, err := core.GetCachedJokeByID(d.Memory, id)
+ if err != nil {
+ return err
+ }
+
+ if link == "" {
+ return c.
+ Status(fiber.StatusNotFound).
+ Send([]byte("Requested ID was not found."))
+ }
+
+ // Get image data
+ response, err := d.HTTP.Get(link, nil)
+ if err != nil {
+ return err
+ }
+
+ data, err := ioutil.ReadAll(response.Body)
+ if err != nil {
+ return err
+ }
+
+ c.Set("Content-Type", response.Header.Get("content-type"))
+ return c.Status(fiber.StatusOK).Send(data)
+}
diff --git a/api/handler/joke/joke_total.go b/api/handler/joke/joke_total.go
new file mode 100644
index 0000000..a820134
--- /dev/null
+++ b/api/handler/joke/joke_total.go
@@ -0,0 +1,43 @@
+package joke
+
+import (
+ "errors"
+ core "jokes-bapak2-api/core/joke"
+ "strconv"
+
+ "github.com/allegro/bigcache/v3"
+ "github.com/gofiber/fiber/v2"
+)
+
+func (d *Dependencies) TotalJokes(c *fiber.Ctx) error {
+ checkTotal, err := core.CheckTotalJokesCache(d.Memory)
+ if err != nil {
+ return err
+ }
+
+ if !checkTotal {
+ err = core.SetTotalJoke(d.DB, c.Context(), d.Memory)
+ if err != nil {
+ return err
+ }
+ }
+
+ total, err := d.Memory.Get("total")
+
+ if err != nil {
+ if errors.Is(err, bigcache.ErrEntryNotFound) {
+ return c.
+ Status(fiber.StatusInternalServerError).
+ JSON(Error{
+ Error: "no data found",
+ })
+ }
+ return err
+ }
+
+ return c.
+ Status(fiber.StatusOK).
+ JSON(ResponseJoke{
+ Message: strconv.Itoa(int(total[0])),
+ })
+}
diff --git a/api/handler/joke/joke_update.go b/api/handler/joke/joke_update.go
new file mode 100644
index 0000000..2562a89
--- /dev/null
+++ b/api/handler/joke/joke_update.go
@@ -0,0 +1,86 @@
+package joke
+
+import (
+ core "jokes-bapak2-api/core/joke"
+ "jokes-bapak2-api/core/schema"
+ "jokes-bapak2-api/core/validator"
+ "strconv"
+
+ "github.com/gofiber/fiber/v2"
+)
+
+func (d *Dependencies) UpdateJoke(c *fiber.Ctx) error {
+ id := c.Params("id")
+ // Check if the joke exists
+
+ jokeExists, err := core.CheckJokeExists(d.DB, c.Context(), id)
+ if err != nil {
+ return err
+ }
+
+ if !jokeExists {
+ return c.
+ Status(fiber.StatusNotAcceptable).
+ JSON(Error{
+ Error: "specified joke id does not exists",
+ })
+ }
+
+ body := new(schema.Joke)
+ err = c.BodyParser(&body)
+ if err != nil {
+ return err
+ }
+
+ // Check link validity
+ valid, err := validator.CheckImageValidity(d.HTTP, body.Link)
+ if err != nil {
+ return err
+ }
+
+ if !valid {
+ return c.
+ Status(fiber.StatusBadRequest).
+ JSON(Error{
+ Error: "URL provided is not a valid image",
+ })
+ }
+
+ newID, err := strconv.Atoi(id)
+ if err != nil {
+ return err
+ }
+
+ newCreator, err := strconv.Atoi(c.Locals("userID").(string))
+ if err != nil {
+ return err
+ }
+
+ updatedJoke := schema.Joke{
+ Link: body.Link,
+ Creator: newCreator,
+ ID: newID,
+ }
+
+ err = core.UpdateJoke(d.DB, c.Context(), updatedJoke)
+ if err != nil {
+ return err
+ }
+
+ err = core.SetAllJSONJoke(d.DB, c.Context(), d.Memory)
+ if err != nil {
+ return err
+ }
+
+ err = core.SetTotalJoke(d.DB, c.Context(), d.Memory)
+ if err != nil {
+ return err
+ }
+
+ return c.
+ Status(fiber.StatusOK).
+ JSON(ResponseJoke{
+ Message: "specified joke id has been updated",
+ Link: body.Link,
+ })
+}
diff --git a/api/app/v1/models/joke.go b/api/handler/joke/schema.go
similarity index 58%
rename from api/app/v1/models/joke.go
rename to api/handler/joke/schema.go
index f2b66c4..b0180c1 100644
--- a/api/app/v1/models/joke.go
+++ b/api/handler/joke/schema.go
@@ -1,9 +1,8 @@
-package models
+package joke
-type Joke struct {
- ID int `json:"id" form:"id" db:"id"`
- Link string `json:"link" form:"link" db:"link"`
- Creator int `json:"creator" form:"creator" db:"creator"`
+type ResponseJoke struct {
+ Link string `json:"link,omitempty"`
+ Message string `json:"message,omitempty"`
}
type Today struct {
@@ -12,7 +11,6 @@ type Today struct {
ContentType string `redis:"today:contentType"`
}
-type ResponseJoke struct {
- Link string `json:"link,omitempty"`
- Message string `json:"message,omitempty"`
+type Error struct {
+ Error string `json:"error"`
}
diff --git a/api/handler/submit/dependencies.go b/api/handler/submit/dependencies.go
new file mode 100644
index 0000000..5f73c1b
--- /dev/null
+++ b/api/handler/submit/dependencies.go
@@ -0,0 +1,17 @@
+package submit
+
+import (
+ "github.com/Masterminds/squirrel"
+ "github.com/allegro/bigcache/v3"
+ "github.com/go-redis/redis/v8"
+ "github.com/gojek/heimdall/v7/httpclient"
+ "github.com/jackc/pgx/v4/pgxpool"
+)
+
+type Dependencies struct {
+ DB *pgxpool.Pool
+ Redis *redis.Client
+ Memory *bigcache.BigCache
+ HTTP *httpclient.Client
+ Query squirrel.StatementBuilderType
+}
diff --git a/api/handler/submit/submit_add.go b/api/handler/submit/submit_add.go
new file mode 100644
index 0000000..5525c1c
--- /dev/null
+++ b/api/handler/submit/submit_add.go
@@ -0,0 +1,99 @@
+package submit
+
+import (
+ "jokes-bapak2-api/core/schema"
+ core "jokes-bapak2-api/core/submit"
+ "jokes-bapak2-api/core/validator"
+ "net/url"
+ "strings"
+
+ "github.com/gofiber/fiber/v2"
+)
+
+func (d *Dependencies) SubmitJoke(c *fiber.Ctx) error {
+ conn, err := d.DB.Acquire(c.Context())
+ if err != nil {
+ return err
+ }
+ defer conn.Release()
+
+ var body schema.Submission
+ err = c.BodyParser(&body)
+ if err != nil {
+ return err
+ }
+
+ // Image and/or Link should not be empty
+ if body.Image == "" && body.Link == "" {
+ return c.Status(fiber.StatusBadRequest).JSON(schema.Error{
+ Error: "A link or an image should be supplied in a form of multipart/form-data",
+ })
+ }
+
+ // Author should be supplied
+ if body.Author == "" {
+ return c.Status(fiber.StatusBadRequest).JSON(schema.Error{
+ Error: "An author key consisting on the format \"yourname \" must be supplied",
+ })
+ } else {
+ // Validate format
+ valid := validator.ValidateAuthor(body.Author)
+ if !valid {
+ return c.Status(fiber.StatusBadRequest).JSON(schema.Error{
+ Error: "Please stick to the format of \"yourname \" and within 200 characters",
+ })
+ }
+ }
+
+ var link string
+
+ // Check link validity if link was provided
+ if body.Link != "" {
+ valid, err := validator.CheckImageValidity(d.HTTP, body.Link)
+ if err != nil {
+ return err
+ }
+ if !valid {
+ return c.Status(fiber.StatusBadRequest).JSON(schema.Error{
+ Error: "URL provided is not a valid image",
+ })
+ }
+
+ link = body.Link
+ }
+
+ // If image was provided
+ if body.Image != "" {
+ image := strings.NewReader(body.Image)
+
+ link, err = core.UploadImage(d.HTTP, image)
+ if err != nil {
+ return err
+ }
+ }
+
+ // Validate if link already exists
+ validateLink, err := validator.SubmitLinkExists(d.DB, c.Context(), d.Query, link)
+ if err != nil {
+ return err
+ }
+
+ if validateLink {
+ return c.Status(fiber.StatusConflict).JSON(schema.Error{
+ Error: "Given link is already on the submission queue.",
+ })
+ }
+
+ submission, err := core.SubmitJoke(d.DB, c.Context(), body, link)
+ if err != nil {
+ return err
+ }
+
+ return c.
+ Status(fiber.StatusCreated).
+ JSON(schema.ResponseSubmission{
+ Message: "Joke submitted. Please wait for a few days for admin to approve your submission.",
+ Submission: submission,
+ AuthorPage: "/submit?author=" + url.QueryEscape(body.Author),
+ })
+}
diff --git a/api/handler/submit/submit_get.go b/api/handler/submit/submit_get.go
new file mode 100644
index 0000000..ec04888
--- /dev/null
+++ b/api/handler/submit/submit_get.go
@@ -0,0 +1,28 @@
+package submit
+
+import (
+ "jokes-bapak2-api/core/schema"
+ core "jokes-bapak2-api/core/submit"
+
+ "github.com/gofiber/fiber/v2"
+)
+
+func (d *Dependencies) GetSubmission(c *fiber.Ctx) error {
+ query := new(schema.SubmissionQuery)
+ err := c.QueryParser(query)
+ if err != nil {
+ return err
+ }
+
+ submissions, err := core.GetSubmittedItems(d.DB, c.Context(), *query)
+ if err != nil {
+ return err
+ }
+
+ return c.
+ Status(fiber.StatusOK).
+ JSON(fiber.Map{
+ "count": len(submissions),
+ "jokes": submissions,
+ })
+}
diff --git a/api/main.go b/api/main.go
index 08b959b..07c430d 100644
--- a/api/main.go
+++ b/api/main.go
@@ -4,22 +4,100 @@ import (
"log"
"os"
"os/signal"
+
+ "context"
+ "jokes-bapak2-api/core/joke"
+ "jokes-bapak2-api/platform/database"
+ "jokes-bapak2-api/routes"
+
"time"
- v1 "jokes-bapak2-api/app/v1"
-
"github.com/gofiber/fiber/v2"
- "github.com/gofiber/fiber/v2/middleware/favicon"
- "github.com/gofiber/fiber/v2/middleware/limiter"
_ "github.com/joho/godotenv/autoload"
+
+ "github.com/Masterminds/squirrel"
+ "github.com/allegro/bigcache/v3"
+ "github.com/getsentry/sentry-go"
+ "github.com/go-redis/redis/v8"
+ "github.com/gofiber/fiber/v2/middleware/cors"
+ "github.com/gofiber/fiber/v2/middleware/etag"
+ "github.com/gofiber/fiber/v2/middleware/limiter"
+ "github.com/gojek/heimdall/v7/httpclient"
+ "github.com/jackc/pgx/v4/pgxpool"
)
func main() {
- timeoutDefault, _ := time.ParseDuration("1m")
+ // Setup PostgreSQL
+ poolConfig, err := pgxpool.ParseConfig(os.Getenv("DATABASE_URL"))
+ if err != nil {
+ log.Panicln("Unable to create pool config", err)
+ }
+ poolConfig.MaxConnIdleTime = time.Minute * 3
+ poolConfig.MaxConnLifetime = time.Minute * 5
+ poolConfig.MaxConns = 15
+ poolConfig.MinConns = 4
+
+ db, err := pgxpool.ConnectConfig(context.Background(), poolConfig)
+ if err != nil {
+ log.Panicln("Unable to create connection", err)
+ }
+ defer db.Close()
+
+ // Setup Redis
+ opt, err := redis.ParseURL(os.Getenv("REDIS_URL"))
+ if err != nil {
+ log.Fatalln(err)
+ }
+ rdb := redis.NewClient(opt)
+ defer rdb.Close()
+
+ // Setup In Memory
+ memory, err := bigcache.NewBigCache(bigcache.DefaultConfig(6 * time.Hour))
+ if err != nil {
+ log.Panicln(err)
+ }
+ defer memory.Close()
+
+ // Setup Sentry
+ err = sentry.Init(sentry.ClientOptions{
+ Dsn: os.Getenv("SENTRY_DSN"),
+ Environment: os.Getenv("ENV"),
+ AttachStacktrace: true,
+ // Enable printing of SDK debug messages.
+ // Useful when getting started or trying to figure something out.
+ Debug: true,
+ })
+ if err != nil {
+ log.Panicln(err)
+ }
+ defer sentry.Flush(2 * time.Second)
+
+ setupCtx, setupCancel := context.WithDeadline(context.Background(), time.Now().Add(time.Minute*4))
+ defer setupCancel()
+
+ err = database.Populate(db, setupCtx)
+ if err != nil {
+ sentry.CaptureException(err)
+ log.Panicln(err)
+ }
+
+ err = joke.SetAllJSONJoke(db, setupCtx, memory)
+ if err != nil {
+ log.Panicln(err)
+ }
+ err = joke.SetTotalJoke(db, setupCtx, memory)
+ if err != nil {
+ log.Panicln(err)
+ }
+
+ timeoutDefault := time.Minute * 1
app := fiber.New(fiber.Config{
- ReadTimeout: timeoutDefault,
- WriteTimeout: timeoutDefault,
+ ReadTimeout: timeoutDefault,
+ WriteTimeout: timeoutDefault,
+ CaseSensitive: true,
+ DisableKeepalive: true,
+ ErrorHandler: errorHandler,
})
app.Use(limiter.New(limiter.Config{
@@ -27,11 +105,21 @@ func main() {
Expiration: 1 * time.Minute,
LimitReached: limitHandler,
}))
- app.Use(favicon.New(favicon.Config{
- File: "./favicon.png",
- }))
- app.Mount("/v1", v1.New())
+ app.Use(cors.New())
+ app.Use(etag.New())
+
+ route := routes.Dependencies{
+ DB: db,
+ Redis: rdb,
+ Memory: memory,
+ HTTP: httpclient.NewClient(httpclient.WithHTTPTimeout(10 * time.Second)),
+ Query: squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar),
+ App: app,
+ }
+ route.Health()
+ route.Joke()
+ route.Submit()
// Start server (with or without graceful shutdown).
if os.Getenv("ENV") == "development" {
@@ -47,6 +135,14 @@ func limitHandler(c *fiber.Ctx) error {
})
}
+func errorHandler(c *fiber.Ctx, err error) error {
+ log.Println(err)
+ sentry.CaptureException(err)
+ return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
+ "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.
diff --git a/api/middleware/auth.go b/api/middleware/auth.go
new file mode 100644
index 0000000..3ef847d
--- /dev/null
+++ b/api/middleware/auth.go
@@ -0,0 +1,58 @@
+package middleware
+
+import (
+ "jokes-bapak2-api/core/administrator"
+
+ phccrypto "github.com/aldy505/phc-crypto"
+ "github.com/gofiber/fiber/v2"
+ "github.com/jackc/pgx/v4/pgxpool"
+)
+
+func RequireAuth(db *pgxpool.Pool) fiber.Handler {
+ return func(c *fiber.Ctx) error {
+ var auth Auth
+ err := c.BodyParser(&auth)
+ if err != nil {
+ return err
+ }
+
+ token, err := administrator.CheckKeyExists(db, c.Context(), auth.Key)
+ if err != nil {
+ return err
+ }
+
+ if token == "" {
+ return c.
+ Status(fiber.StatusForbidden).
+ JSON(Error{
+ Error: "Invalid key",
+ })
+ }
+
+ crypto, err := phccrypto.Use(phccrypto.Argon2, phccrypto.Config{})
+ if err != nil {
+ return err
+ }
+
+ verify, err := crypto.Verify(token, auth.Token)
+ if err != nil {
+ return err
+ }
+
+ if verify {
+ id, err := administrator.GetUserID(db, c.Context(), auth.Key)
+ if err != nil {
+ return err
+ }
+
+ c.Locals("userID", id)
+ return c.Next()
+ }
+
+ return c.
+ Status(fiber.StatusForbidden).
+ JSON(Error{
+ Error: "Invalid key",
+ })
+ }
+}
diff --git a/api/app/v1/models/general.go b/api/middleware/schema.go
similarity index 77%
rename from api/app/v1/models/general.go
rename to api/middleware/schema.go
index 11eae1e..9659dce 100644
--- a/api/app/v1/models/general.go
+++ b/api/middleware/schema.go
@@ -1,4 +1,4 @@
-package models
+package middleware
type Auth struct {
ID int `json:"id" form:"id" db:"id"`
@@ -6,3 +6,7 @@ type Auth struct {
Token string `json:"token" form:"token" db:"token"`
LastUsed string `json:"last_used" form:"last_used" db:"last_used"`
}
+
+type Error struct {
+ Error string `json:"error"`
+}
diff --git a/api/app/v1/middleware/validation.go b/api/middleware/validation.go
similarity index 88%
rename from api/app/v1/middleware/validation.go
rename to api/middleware/validation.go
index 0156d81..8e12c7c 100644
--- a/api/app/v1/middleware/validation.go
+++ b/api/middleware/validation.go
@@ -1,7 +1,6 @@
package middleware
import (
- "jokes-bapak2-api/app/v1/models"
"regexp"
"github.com/gofiber/fiber/v2"
@@ -21,7 +20,7 @@ func OnlyIntegerAsID() fiber.Handler {
return c.
Status(fiber.StatusBadRequest).
- JSON(models.Error{
+ JSON(Error{
Error: "only numbers are allowed as ID",
})
}
diff --git a/api/app/v1/platform/database/create.go b/api/platform/database/create.go
similarity index 59%
rename from api/app/v1/platform/database/create.go
rename to api/platform/database/create.go
index 0611f39..421dd86 100644
--- a/api/app/v1/platform/database/create.go
+++ b/api/platform/database/create.go
@@ -2,24 +2,46 @@ package database
import (
"context"
- "log"
"github.com/aldy505/bob"
+ "github.com/jackc/pgx/v4/pgxpool"
)
// Setup the table connection, create table if not exists
-func Setup() error {
- db := New()
+func Populate(db *pgxpool.Pool, ctx context.Context) error {
+ err := setupAuthTable(db, ctx)
+ if err != nil {
+ return err
+ }
- // administrators table
+ err = setupJokesTable(db, ctx)
+ if err != nil {
+ return err
+ }
+
+ err = setupSubmissionTable(db, ctx)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func setupAuthTable(db *pgxpool.Pool, ctx context.Context) error {
+ conn, err := db.Acquire(ctx)
+ if err != nil {
+ return err
+ }
+ defer conn.Release()
+
+ // Check if table exists
var tableAuthExists bool
- err := db.QueryRow(context.Background(), `SELECT EXISTS (
+ err = conn.QueryRow(ctx, `SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = 'administrators'
);`).Scan(&tableAuthExists)
if err != nil {
- log.Fatalln("16 - failed on checking table: ", err)
return err
}
@@ -27,33 +49,37 @@ func Setup() error {
sql, _, err := bob.
CreateTable("administrators").
AddColumn(bob.ColumnDef{Name: "id", Type: "SERIAL", Extras: []string{"PRIMARY KEY"}}).
- StringColumn("key", "UNIQUE").
+ StringColumn("key", "NOT NULL", "UNIQUE").
TextColumn("token").
StringColumn("last_used").
ToSql()
if err != nil {
- log.Fatalln("17 - failed on table creation: ", err)
return err
}
- _, err = db.Query(context.Background(), sql)
+ _, err = conn.Exec(ctx, sql)
if err != nil {
- log.Fatalln("18 - failed on table creation: ", err)
return err
}
}
+ return nil
+}
- // Jokesbapak2 table
+func setupJokesTable(db *pgxpool.Pool, ctx context.Context) error {
+ conn, err := db.Acquire(ctx)
+ if err != nil {
+ return err
+ }
+ defer conn.Release()
// Check if table exists
var tableJokesExists bool
- err = db.QueryRow(context.Background(), `SELECT EXISTS (
+ err = conn.QueryRow(ctx, `SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = 'jokesbapak2'
);`).Scan(&tableJokesExists)
if err != nil {
- log.Fatalln("10 - failed on checking table: ", err)
return err
}
@@ -65,28 +91,33 @@ func Setup() error {
AddColumn(bob.ColumnDef{Name: "creator", Type: "INT", Extras: []string{"NOT NULL", "REFERENCES \"administrators\" (\"id\")"}}).
ToSql()
if err != nil {
- log.Fatalln("11 - failed on table creation: ", err)
return err
}
- _, err = db.Query(context.Background(), sql)
+ _, err = conn.Exec(ctx, sql)
if err != nil {
- log.Fatalln("12 - failed on table creation: ", err)
return err
}
}
- // Submission table
+ return nil
+}
+
+func setupSubmissionTable(db *pgxpool.Pool, ctx context.Context) error {
+ conn, err := db.Acquire(ctx)
+ if err != nil {
+ return err
+ }
+ defer conn.Release()
//Check if table exists
var tableSubmissionExists bool
- err = db.QueryRow(context.Background(), `SELECT EXISTS (
- SELECT FROM information_schema.tables
- WHERE table_schema = 'public'
- AND table_name = 'submission'
- );`).Scan(&tableJokesExists)
+ err = conn.QueryRow(ctx, `SELECT EXISTS (
+ SELECT FROM information_schema.tables
+ WHERE table_schema = 'public'
+ AND table_name = 'submission'
+ );`).Scan(&tableSubmissionExists)
if err != nil {
- log.Fatalln("13 - failed on checking table: ", err)
return err
}
@@ -100,12 +131,12 @@ func Setup() error {
AddColumn(bob.ColumnDef{Name: "status", Type: "SMALLINT", Extras: []string{"DEFAULT 0"}}).
ToSql()
if err != nil {
- log.Fatalln("14 - failed on table creation: ", err)
+ return err
}
- _, err = db.Query(context.Background(), sql)
+ _, err = conn.Exec(ctx, sql)
if err != nil {
- log.Fatalln("15 - failed on table creation: ", err)
+ return err
}
}
diff --git a/api/platform/database/placeholder.sql b/api/platform/database/placeholder.sql
new file mode 100644
index 0000000..07866bf
--- /dev/null
+++ b/api/platform/database/placeholder.sql
@@ -0,0 +1,21 @@
+-- Access the data from your HTTP Request software (Postman or Insomnia)
+-- with this auth:
+-- key: test
+-- token: password
+
+INSERT INTO "administrators" ("id", "key", "token", "last_used") VALUES
+(1, 'test', '$argon2id$v=19$m=65536,t=16,p=4$3a08c79fbf2222467a623df9a9ebf75802c65a4f9be36eb1df2f5d2052d53cb7$ce434bd38f7ba1fc1f2eb773afb8a1f7f2dad49140803ac6cb9d7256ce9826fb3b4afa1e2488da511c852fc6c33a76d5657eba6298a8e49d617b9972645b7106', '');
+
+-- 10 jokes is enough right?
+
+INSERT INTO "jokesbapak2" ("link", "creator") VALUES
+('https://picsum.photos/id/1000/500/500', 1),
+('https://picsum.photos/id/1001/500/500', 1),
+('https://picsum.photos/id/1002/500/500', 1),
+('https://picsum.photos/id/1003/500/500', 1),
+('https://picsum.photos/id/1004/500/500', 1),
+('https://picsum.photos/id/1005/500/500', 1),
+('https://picsum.photos/id/1006/500/500', 1),
+('https://picsum.photos/id/1010/500/500', 1),
+('https://picsum.photos/id/1008/500/500', 1),
+('https://picsum.photos/id/1009/500/500', 1);
\ No newline at end of file
diff --git a/api/routes/dependencies.go b/api/routes/dependencies.go
new file mode 100644
index 0000000..65f6bb5
--- /dev/null
+++ b/api/routes/dependencies.go
@@ -0,0 +1,19 @@
+package routes
+
+import (
+ "github.com/Masterminds/squirrel"
+ "github.com/allegro/bigcache/v3"
+ "github.com/go-redis/redis/v8"
+ "github.com/gofiber/fiber/v2"
+ "github.com/gojek/heimdall/v7/httpclient"
+ "github.com/jackc/pgx/v4/pgxpool"
+)
+
+type Dependencies struct {
+ DB *pgxpool.Pool
+ Redis *redis.Client
+ Memory *bigcache.BigCache
+ HTTP *httpclient.Client
+ Query squirrel.StatementBuilderType
+ App *fiber.App
+}
diff --git a/api/routes/health.go b/api/routes/health.go
new file mode 100644
index 0000000..3b274b3
--- /dev/null
+++ b/api/routes/health.go
@@ -0,0 +1,19 @@
+package routes
+
+import (
+ "jokes-bapak2-api/handler/health"
+ "time"
+
+ "github.com/gofiber/fiber/v2/middleware/cache"
+)
+
+func (d *Dependencies) Health() {
+ // Health check
+ deps := health.Dependencies{
+ DB: d.DB,
+ Redis: d.Redis,
+ }
+
+ d.App.Get("/health", cache.New(cache.Config{Expiration: 30 * time.Minute}), deps.Health)
+ d.App.Get("/v1/health", cache.New(cache.Config{Expiration: 30 * time.Minute}), deps.Health)
+}
diff --git a/api/routes/joke.go b/api/routes/joke.go
new file mode 100644
index 0000000..88d531f
--- /dev/null
+++ b/api/routes/joke.go
@@ -0,0 +1,43 @@
+package routes
+
+import (
+ "jokes-bapak2-api/handler/joke"
+ "jokes-bapak2-api/middleware"
+ "time"
+
+ "github.com/gofiber/fiber/v2/middleware/cache"
+)
+
+func (d *Dependencies) Joke() {
+ deps := joke.Dependencies{
+ DB: d.DB,
+ Redis: d.Redis,
+ Memory: d.Memory,
+ HTTP: d.HTTP,
+ Query: d.Query,
+ }
+ // Single route
+ d.App.Get("/", deps.SingleJoke)
+ d.App.Get("/v1", deps.SingleJoke)
+
+ // Today's joke
+ d.App.Get("/today", cache.New(cache.Config{Expiration: 6 * time.Hour}), deps.TodayJoke)
+ d.App.Get("/v1/today", cache.New(cache.Config{Expiration: 6 * time.Hour}), deps.TodayJoke)
+
+ // Joke by ID
+ d.App.Get("/id/:id", middleware.OnlyIntegerAsID(), deps.JokeByID)
+ d.App.Get("/v1/id/:id", middleware.OnlyIntegerAsID(), deps.JokeByID)
+
+ // Count total jokes
+ d.App.Get("/total", cache.New(cache.Config{Expiration: 15 * time.Minute}), deps.TotalJokes)
+ d.App.Get("/v1/total", cache.New(cache.Config{Expiration: 15 * time.Minute}), deps.TotalJokes)
+
+ // Add new joke
+ d.App.Put("/", middleware.RequireAuth(d.DB), deps.AddNewJoke)
+
+ // Update a joke
+ d.App.Patch("/id/:id", middleware.RequireAuth(d.DB), middleware.OnlyIntegerAsID(), deps.UpdateJoke)
+
+ // Delete a joke
+ d.App.Delete("/id/:id", middleware.RequireAuth(d.DB), middleware.OnlyIntegerAsID(), deps.DeleteJoke)
+}
diff --git a/api/app/v1/routes/submit.go b/api/routes/submit.go
similarity index 54%
rename from api/app/v1/routes/submit.go
rename to api/routes/submit.go
index 24b9274..f880e8b 100644
--- a/api/app/v1/routes/submit.go
+++ b/api/routes/submit.go
@@ -1,16 +1,24 @@
package routes
import (
- "jokes-bapak2-api/app/v1/handler/submit"
+ "jokes-bapak2-api/handler/submit"
"time"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cache"
)
-func Submit(app *fiber.App) *fiber.App {
+func (d *Dependencies) Submit() {
+ deps := submit.Dependencies{
+ DB: d.DB,
+ Redis: d.Redis,
+ Memory: d.Memory,
+ HTTP: d.HTTP,
+ Query: d.Query,
+ }
+
// Get pending submitted joke
- app.Get(
+ d.App.Get(
"/submit",
cache.New(cache.Config{
Expiration: 5 * time.Minute,
@@ -18,10 +26,8 @@ func Submit(app *fiber.App) *fiber.App {
return c.OriginalURL()
},
}),
- submit.GetSubmission)
+ deps.GetSubmission)
// Add a joke
- app.Post("/submit", submit.SubmitJoke)
-
- return app
+ d.App.Post("/submit", deps.SubmitJoke)
}
diff --git a/api/app/v1/utils/array.go b/api/utils/array.go
similarity index 100%
rename from api/app/v1/utils/array.go
rename to api/utils/array.go
diff --git a/api/app/v1/utils/array_test.go b/api/utils/array_test.go
similarity index 92%
rename from api/app/v1/utils/array_test.go
rename to api/utils/array_test.go
index 3f57c03..74c36af 100644
--- a/api/app/v1/utils/array_test.go
+++ b/api/utils/array_test.go
@@ -1,7 +1,7 @@
package utils_test
import (
- "jokes-bapak2-api/app/v1/utils"
+ "jokes-bapak2-api/utils"
"testing"
)
diff --git a/api/app/v1/utils/date.go b/api/utils/date.go
similarity index 100%
rename from api/app/v1/utils/date.go
rename to api/utils/date.go
diff --git a/api/app/v1/utils/date_test.go b/api/utils/date_test.go
similarity index 96%
rename from api/app/v1/utils/date_test.go
rename to api/utils/date_test.go
index f96f077..de59927 100644
--- a/api/app/v1/utils/date_test.go
+++ b/api/utils/date_test.go
@@ -1,10 +1,9 @@
package utils_test
import (
+ "jokes-bapak2-api/utils"
"testing"
"time"
-
- "jokes-bapak2-api/app/v1/utils"
)
func TestIsToday_Today(t *testing.T) {
diff --git a/api/app/v1/utils/parse.go b/api/utils/parse.go
similarity index 100%
rename from api/app/v1/utils/parse.go
rename to api/utils/parse.go
diff --git a/api/app/v1/utils/parse_test.go b/api/utils/parse_test.go
similarity index 96%
rename from api/app/v1/utils/parse_test.go
rename to api/utils/parse_test.go
index db1dd0c..4388182 100644
--- a/api/app/v1/utils/parse_test.go
+++ b/api/utils/parse_test.go
@@ -1,10 +1,9 @@
package utils_test
import (
+ "jokes-bapak2-api/utils"
"strings"
"testing"
-
- "jokes-bapak2-api/app/v1/utils"
)
func TestParseToJSONBody(t *testing.T) {
diff --git a/api/app/v1/utils/random.go b/api/utils/random.go
similarity index 100%
rename from api/app/v1/utils/random.go
rename to api/utils/random.go
diff --git a/api/app/v1/utils/random_test.go b/api/utils/random_test.go
similarity index 93%
rename from api/app/v1/utils/random_test.go
rename to api/utils/random_test.go
index 9b1b642..c891510 100644
--- a/api/app/v1/utils/random_test.go
+++ b/api/utils/random_test.go
@@ -1,7 +1,7 @@
package utils_test
import (
- "jokes-bapak2-api/app/v1/utils"
+ "jokes-bapak2-api/utils"
"testing"
)
diff --git a/api/app/v1/utils/request.go b/api/utils/request.go
similarity index 100%
rename from api/app/v1/utils/request.go
rename to api/utils/request.go
diff --git a/api/app/v1/utils/request_test.go b/api/utils/request_test.go
similarity index 93%
rename from api/app/v1/utils/request_test.go
rename to api/utils/request_test.go
index 6e51489..95750bb 100644
--- a/api/app/v1/utils/request_test.go
+++ b/api/utils/request_test.go
@@ -1,10 +1,9 @@
package utils_test
import (
+ "jokes-bapak2-api/utils"
"net/http"
"testing"
-
- "jokes-bapak2-api/app/v1/utils"
)
func TestRequest_Get(t *testing.T) {