diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 29f8e4f..48d904d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,33 +2,34 @@ name: CI on: push: - branches: [ "master" ] + branches: ["master"] jobs: api-build: name: API runs-on: ubuntu-latest - container: golang:1.17-buster + container: golang:1.19-bullseye timeout-minutes: 15 services: - postgres: - image: postgres:13-alpine + bucket: + image: minio/minio:edge-cicd env: - PGDATABASE: jokesbapak2 - POSTGRES_DB: jokesbapak2 - PGUSER: postgres - POSTGRES_USER: postgres - PGPASSWORD: password - POSTGRES_PASSWORD: password - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 + MINIO_ROOT_USER: root + MINIO_ROOT_PASSWORD: verysecurepassword + MINIO_ACCESS_KEY: minio_access_key + MINIO_SECRET_KEY: minio_access_key ports: - - 5432:5432 + - 9000:9000 + options: >- + --health-cmd "curl -f http://bucket:9000/minio/health/live" + --health-interval 45s + --health-timeout 30s + --health-retries 10 + --health-start-period 120s + volumes: + - minio-data:/data redis: - image: redis:6-alpine + image: redis:6-bullseye ports: - 6379:6379 defaults: @@ -46,11 +47,13 @@ jobs: run: go build main.go - name: Run test & coverage - run: go test -v -coverprofile=coverage.out -covermode=atomic ./... + run: go test -v -race -coverprofile=coverage.out -covermode=atomic ./... env: ENV: development PORT: 5000 - DATABASE_URL: postgres://postgres:password@postgres:5432/jokesbapak2 + MINIO_HOST: bucket:9000 + MINIO_ACCESS_ID: root + MINIO_SECRET_KEY: verysecurepassword REDIS_URL: redis://@redis:6379 - name: Initialize CodeQL @@ -79,7 +82,7 @@ jobs: client-build: name: Client runs-on: ubuntu-latest - container: node:14-buster + container: node:18-bullseye timeout-minutes: 15 defaults: run: @@ -119,4 +122,4 @@ jobs: with: environment: production set_commits: skip - version: ${{ github.sha }} \ No newline at end of file + version: ${{ github.sha }} diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 44203c7..209a92a 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -2,13 +2,13 @@ name: PR on: pull_request: - branches: [ "*" ] + branches: ["*"] jobs: client-build: name: Client runs-on: ubuntu-latest - container: node:14-buster + container: node:18-bullseye timeout-minutes: 15 defaults: run: @@ -45,27 +45,28 @@ jobs: api-build: name: API runs-on: ubuntu-latest - container: golang:1.17-buster + container: golang:1.19-bullseye timeout-minutes: 15 services: - postgres: - image: postgres:13-alpine + bucket: + image: minio/minio:edge-cicd env: - PGDATABASE: jokesbapak2 - POSTGRES_DB: jokesbapak2 - PGUSER: postgres - POSTGRES_USER: postgres - PGPASSWORD: password - POSTGRES_PASSWORD: password - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 + MINIO_ROOT_USER: root + MINIO_ROOT_PASSWORD: verysecurepassword + MINIO_ACCESS_KEY: minio_access_key + MINIO_SECRET_KEY: minio_access_key ports: - - 5432:5432 + - 9000:9000 + options: >- + --health-cmd "curl -f http://bucket:9000/minio/health/live" + --health-interval 45s + --health-timeout 30s + --health-retries 10 + --health-start-period 120s + volumes: + - minio-data:/data redis: - image: redis:6-alpine + image: redis:6-bullseye ports: - 6379:6379 defaults: @@ -87,7 +88,9 @@ jobs: env: ENV: development PORT: 5000 - DATABASE_URL: postgres://postgres:password@postgres:5432/jokesbapak2 + MINIO_HOST: bucket:9000 + MINIO_ACCESS_ID: root + MINIO_SECRET_KEY: verysecurepassword REDIS_URL: redis://@redis:6379 - name: Initialize CodeQL @@ -100,4 +103,4 @@ jobs: - uses: codecov/codecov-action@v2 with: - flags: api \ No newline at end of file + flags: api diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..65da6d3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,219 @@ +# Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode,goland,webstorm,datagrip +# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode,goland,webstorm,datagrip + +### GoLand ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### GoLand Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +# https://plugins.jetbrains.com/plugin/7973-sonarlint +.idea/**/sonarlint/ + +# SonarQube Plugin +# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin +.idea/**/sonarIssues.xml + +# Markdown Navigator plugin +# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced +.idea/**/markdown-navigator.xml +.idea/**/markdown-navigator-enh.xml +.idea/**/markdown-navigator/ + +# Cache file creation bug +# See https://youtrack.jetbrains.com/issue/JBR-2257 +.idea/$CACHE_FILE$ + +# CodeStream plugin +# https://plugins.jetbrains.com/plugin/12206-codestream +.idea/codestream.xml + +# Azure Toolkit for IntelliJ plugin +# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij +.idea/**/azureSettings.xml + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +# Support for Project snippet scope +.vscode/*.code-snippets + +# Ignore code-workspaces +*.code-workspace + +### WebStorm ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff + +# AWS User-specific + +# Generated files + +# Sensitive or high-churn files + +# Gradle + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake + +# Mongo Explorer plugin + +# File-based project format + +# IntelliJ + +# mpeltonen/sbt-idea plugin + +# JIRA plugin + +# Cursive Clojure plugin + +# SonarLint plugin + +# Crashlytics plugin (for Android Studio and IntelliJ) + +# Editor-based Rest Client + +# Android studio 3.1+ serialized cache file + +### WebStorm Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +# https://plugins.jetbrains.com/plugin/7973-sonarlint + +# SonarQube Plugin +# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin + +# Markdown Navigator plugin +# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced + +# Cache file creation bug +# See https://youtrack.jetbrains.com/issue/JBR-2257 + +# CodeStream plugin +# https://plugins.jetbrains.com/plugin/12206-codestream + +# Azure Toolkit for IntelliJ plugin +# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij + +# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,goland,webstorm,datagrip + +data \ No newline at end of file diff --git a/api/Dockerfile b/api/Dockerfile index b0c07de..e5cf156 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -1,12 +1,21 @@ -FROM golang:1.17-buster +FROM golang:1.19.0-bullseye AS builder WORKDIR /app COPY . . -RUN go mod download -RUN go build -v main.go +RUN go build -o main . + +FROM debian:bullseye AS runtime + +WORKDIR /app + +COPY --from=builder /app/main . + +ENV PORT=5000 +ENV HOSTNAME=0.0.0.0 +ENV ENVIRONMENT=production EXPOSE ${PORT} -CMD ["./main"] \ No newline at end of file +ENTRYPOINT [ "/app/main" ] \ No newline at end of file diff --git a/api/core/administrator/id.go b/api/core/administrator/id.go deleted file mode 100644 index 35d9e54..0000000 --- a/api/core/administrator/id.go +++ /dev/null @@ -1,60 +0,0 @@ -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 deleted file mode 100644 index 7549513..0000000 --- a/api/core/administrator/id_test.go +++ /dev/null @@ -1,57 +0,0 @@ -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 deleted file mode 100644 index a29863e..0000000 --- a/api/core/administrator/init_test.go +++ /dev/null @@ -1,124 +0,0 @@ -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 deleted file mode 100644 index bf60026..0000000 --- a/api/core/administrator/verify.go +++ /dev/null @@ -1,41 +0,0 @@ -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 deleted file mode 100644 index 6e21672..0000000 --- a/api/core/administrator/verify_test.go +++ /dev/null @@ -1,70 +0,0 @@ -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/core/joke/getter.go b/api/core/joke/getter.go index 4ca97f9..17893f2 100644 --- a/api/core/joke/getter.go +++ b/api/core/joke/getter.go @@ -2,185 +2,126 @@ package joke import ( "context" + "encoding/hex" "errors" - "jokes-bapak2-api/core/schema" + "fmt" + "io" + "log" "math/rand" "strconv" + "time" - "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" + "github.com/go-redis/redis/v8" + "github.com/minio/minio-go/v7" ) -// 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, ctx context.Context) ([]byte, error) { - conn, err := db.Acquire(ctx) +// GetRandomJoke will acquire a random joke from the bucket. +func GetRandomJoke(ctx context.Context, bucket *minio.Client, cache *redis.Client, memory *bigcache.BigCache) (image []byte, contentType string, err error) { + totalJokes, err := GetTotalJoke(ctx, bucket, cache, memory) 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) - if err != nil { - return nil, err + return []byte{}, "", fmt.Errorf("getting total joke: %w", err) } - data, err := ffjson.Marshal(jokes) + randomIndex := rand.Intn(totalJokes - 1) + + joke, contentType, err := GetJokeByID(ctx, bucket, cache, memory, randomIndex) if err != nil { - return nil, err + return []byte{}, "", fmt.Errorf("getting joke by id: %w", err) } - return data, nil + return joke, contentType, 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 +// GetJokeByID wil acquire a joke by its' ID. +// +// An ID is defined as the index on the joke list that is sorted +// by it's creation (or modification) time. +func GetJokeByID(ctx context.Context, bucket *minio.Client, cache *redis.Client, memory *bigcache.BigCache, id int) (image []byte, contentType string, err error) { + jokeFromMemory, err := memory.Get("id:" + strconv.Itoa(id)) + if err != nil && !errors.Is(err, bigcache.ErrEntryNotFound) { + return []byte{}, "", fmt.Errorf("acquiring joke from memory: %w", 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 errors.Is(err, bigcache.ErrEntryNotFound) { - return "", schema.ErrNotFound + if err == nil { + contentTypeFromMemory, err := memory.Get("id:" + strconv.Itoa(id) + ":content-type") + if err != nil && !errors.Is(err, bigcache.ErrEntryNotFound) { + return []byte{}, "", fmt.Errorf("acquiring joke content type from memory: %w", err) } - return "", err + + return jokeFromMemory, string(contentTypeFromMemory), nil } - var data []schema.Joke - err = ffjson.Unmarshal(jokes, &data) - if err != nil { - return "", nil + jokeFromCache, err := cache.Get(ctx, "jokes:id:"+strconv.Itoa(id)).Result() + if err != nil && !errors.Is(err, redis.Nil) { + return []byte{}, "", fmt.Errorf("acquiring joke from cache: %w", err) } - // Return an error if the database is empty - dataLength := len(data) - if dataLength == 0 { - return "", schema.ErrEmpty - } - - random := rand.Intn(dataLength) - joke := data[random].Link - - return joke, nil -} - -// CheckJokesCache checks if there is some value inside jokes cache. -func CheckJokesCache(memory *bigcache.BigCache) (bool, error) { - _, err := memory.Get("jokes") - if err != nil { - if errors.Is(err, bigcache.ErrEntryNotFound) { - return false, nil + if err == nil { + // Get content type + contentTypeFromCache, err := cache.Get(ctx, "jokes:id:"+strconv.Itoa(id)+":content-type").Result() + if err != nil && !errors.Is(err, redis.Nil) { + return []byte{}, "", fmt.Errorf("acquiring content type from cache: %w", err) } - return false, err - } - return true, nil -} - -// CheckTotalJokesCache literally does what the name is for -func CheckTotalJokesCache(memory *bigcache.BigCache) (bool, error) { - _, err := memory.Get("total") - if err != nil { - if errors.Is(err, bigcache.ErrEntryNotFound) { - return false, nil + // Decode hex string to bytes + imageBytes, err := hex.DecodeString(jokeFromCache) + if err != nil { + return []byte{}, "", fmt.Errorf("decoding hex string: %w", err) } - return false, err + + defer func(id int, imageBytes []byte) { + err := memory.Set("id:"+strconv.Itoa(id), imageBytes) + if err != nil { + log.Printf("setting memory cache: %s", err.Error()) + } + + err = memory.Set("id:"+strconv.Itoa(id)+":content-type", []byte(contentTypeFromCache)) + if err != nil { + log.Printf("setting memory cache: %s", err.Error()) + } + }(id, imageBytes) + + return imageBytes, contentTypeFromCache, nil } - return true, nil -} - -// GetCachedJokeByID returns a link string of a certain ID from cache. -func GetCachedJokeByID(memory *bigcache.BigCache, id int) (string, error) { - jokes, err := memory.Get("jokes") + jokes, err := ListJokesFromBucket(ctx, bucket, cache) if err != nil { - if errors.Is(err, bigcache.ErrEntryNotFound) { - return "", schema.ErrNotFound + return []byte{}, "", fmt.Errorf("listing jokes: %w", err) + } + + object, err := bucket.GetObject(ctx, JokesBapak2Bucket, jokes[id].FileName, minio.GetObjectOptions{}) + if err != nil { + return []byte{}, "", fmt.Errorf("getting object: %w", err) + } + defer func() { + err := object.Close() + if err != nil { + log.Printf("closing image reader: %s", err.Error()) } - return "", err - } + }() - var data []schema.Joke - err = ffjson.Unmarshal(jokes, &data) + image, err = io.ReadAll(object) if err != nil { - return "", err + return []byte{}, "", fmt.Errorf("reading object: %w", err) } - // This is a simple solution, might convert it to goroutines and channels sometime soon. - for _, v := range data { - if v.ID == id { - return v.Link, nil + defer func(id int, image []byte) { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + imageString := hex.EncodeToString(image) + + err := cache.Set(ctx, "jokes:id:"+strconv.Itoa(id), imageString, time.Hour*1).Err() + if err != nil { + log.Printf("setting cache: %s", err.Error()) } - } - return "", nil -} - -// GetCachedTotalJokes -func GetCachedTotalJokes(memory *bigcache.BigCache) (int, error) { - total, err := memory.Get("total") - if err != nil { - if errors.Is(err, bigcache.ErrEntryNotFound) { - return 0, schema.ErrNotFound + err = cache.Set(ctx, "jokes:id:"+strconv.Itoa(id)+":content-type", jokes[id].ContentType, time.Hour*1).Err() + if err != nil { + log.Printf("setting cache: %s", err.Error()) } - return 0, err - } - i, err := strconv.Atoi(string(total)) - if err != nil { - return 0, err - } + }(id, image) - 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 + return image, jokes[id].ContentType, nil } diff --git a/api/core/joke/getter_test.go b/api/core/joke/getter_test.go index 54783bb..792c75d 100644 --- a/api/core/joke/getter_test.go +++ b/api/core/joke/getter_test.go @@ -2,340 +2,70 @@ 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)) +func TestGetRandomJoke(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) defer cancel() - defer Flush() - - conn, err := db.Acquire(ctx) + image, contentType, err := joke.GetRandomJoke(ctx, bucket, cache, memory) 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) + t.Errorf("unexpected error: %v", err) } - j, err := joke.GetAllJSONJokes(db, ctx) - if err != nil { - t.Error("an error was thrown:", err) + if contentType != "image/jpeg" { + t.Errorf("expecting contentType of 'image/jpeg', instead got %s", contentType) } - if string(j) == "" { - t.Error("j should not be empty") + if len(image) == 0 { + t.Error("empty image") } } -func TestGetRandomJokeFromDB(t *testing.T) { - ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(30*time.Second)) +func TestGetJokeById(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) defer cancel() - defer Flush() - - conn, err := db.Acquire(ctx) + image, contentType, err := joke.GetJokeByID(ctx, bucket, cache, memory, 0) if err != nil { - t.Error("an error was thrown:", err) + t.Errorf("unexpected error: %v", 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 - } + if contentType != "image/jpeg" { + t.Errorf("expecting contentType of 'image/jpeg', instead got %s", contentType) + } - return nil - }) + if len(image) == 0 { + t.Error("empty image") + } + + cachedImage, cachedContentType, err := joke.GetJokeByID(ctx, bucket, cache, memory, 0) if err != nil { - t.Error("an error was thrown:", err) + t.Errorf("unexpected error: %v", err) } - j, err := joke.GetRandomJokeFromDB(db, ctx) + if cachedContentType != contentType { + t.Errorf("difference in contentType: original %s vs cached %s", contentType, cachedContentType) + } + + if string(cachedImage) != string(image) { + t.Errorf("difference in image bytes") + } + + cachedImage2, cachedContentType2, err := joke.GetJokeByID(ctx, bucket, cache, memory, 0) if err != nil { - t.Error("an error was thrown:", err) + t.Errorf("unexpected error: %v", 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") + if cachedContentType2 != contentType { + t.Errorf("difference in contentType: original %s vs cached %s", contentType, cachedContentType2) } + + if string(cachedImage2) != string(image) { + t.Errorf("difference in image bytes") + } + } diff --git a/api/core/joke/init_test.go b/api/core/joke/init_test.go deleted file mode 100644 index 34a4e65..0000000 --- a/api/core/joke/init_test.go +++ /dev/null @@ -1,195 +0,0 @@ -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/joke.go b/api/core/joke/joke.go new file mode 100644 index 0000000..8932f7d --- /dev/null +++ b/api/core/joke/joke.go @@ -0,0 +1,14 @@ +package joke + +import "time" + +// JokesBapak2Bucket defines the bucket that the jokes resides in. +const JokesBapak2Bucket = "jokesbapak2" + +// Joke provides a simple struct that points +// to the information of the joke. +type Joke struct { + FileName string + ContentType string + ModifiedAt time.Time +} diff --git a/api/core/joke/joke_test.go b/api/core/joke/joke_test.go new file mode 100644 index 0000000..feaad14 --- /dev/null +++ b/api/core/joke/joke_test.go @@ -0,0 +1,156 @@ +package joke_test + +import ( + "context" + "fmt" + "log" + "os" + "testing" + "time" + + "github.com/allegro/bigcache/v3" + "github.com/go-redis/redis/v8" + "github.com/minio/minio-go/v7" + "github.com/minio/minio-go/v7/pkg/credentials" +) + +var bucket *minio.Client +var cache *redis.Client +var memory *bigcache.BigCache + +func TestMain(m *testing.M) { + redisURL, ok := os.LookupEnv("REDIS_URL") + if !ok { + redisURL = "redis://@localhost:6379" + } + + minioHost, ok := os.LookupEnv("MINIO_HOST") + if !ok { + minioHost = "localhost:9000" + } + + minioID, ok := os.LookupEnv("MINIO_ACCESS_ID") + if !ok { + minioID = "minio" + } + + minioSecret, ok := os.LookupEnv("MINIO_SECRET_KEY") + if !ok { + minioSecret = "password" + } + + minioToken, ok := os.LookupEnv("MINIO_TOKEN") + if !ok { + minioToken = "" + } + + parsedRedisURL, err := redis.ParseURL(redisURL) + if err != nil { + log.Fatalf("parsing redis url: %s", err.Error()) + return + } + + redisClient := redis.NewClient(parsedRedisURL) + + minioClient, err := minio.New(minioHost, &minio.Options{ + Creds: credentials.NewStaticV4(minioID, minioSecret, minioToken), + }) + if err != nil { + log.Fatalf("creating minio client: %s", err.Error()) + } + + memoryInstance, err := bigcache.NewBigCache(bigcache.DefaultConfig(time.Second * 30)) + if err != nil { + log.Fatalf("creating bigcache client: %s", err.Error()) + return + } + + bucket = minioClient + cache = redisClient + memory = memoryInstance + + setupCtx, setupCancel := context.WithTimeout(context.Background(), time.Minute) + defer setupCancel() + + err = setupBucketStorage(setupCtx, minioClient) + if err != nil { + log.Fatalf("set up bucket storage: %v", err) + return + } + + exitCode := m.Run() + + cleanupCtx, cleanupCancel := context.WithTimeout(context.Background(), time.Minute) + defer cleanupCancel() + + err = redisClient.FlushAll(cleanupCtx).Err() + if err != nil { + log.Printf("flushing redis: %s", err.Error()) + } + + err = minioClient.RemoveBucketWithOptions(cleanupCtx, "jokesbapak2", minio.RemoveBucketOptions{ForceDelete: true}) + if err != nil { + log.Printf("removing bucket: %s", err.Error()) + } + + err = memoryInstance.Close() + if err != nil { + log.Printf("closing cache client: %s", err.Error()) + } + + err = redisClient.Close() + if err != nil { + log.Printf("closing redis client: %s", err.Error()) + } + + os.Exit(exitCode) +} + +func setupBucketStorage(ctx context.Context, minioClient *minio.Client) error { + bucketFound, err := minioClient.BucketExists(ctx, "jokesbapak2") + if err != nil { + return fmt.Errorf("checking MinIO bucket: %w", err) + } + + if !bucketFound { + err = minioClient.MakeBucket(ctx, "jokesbapak2", minio.MakeBucketOptions{}) + if err != nil { + return fmt.Errorf("creating MinIO bucket: %w", err) + } + + policy := `{ + "Version":"2012-10-17", + "Statement":[ + { + "Sid": "AddPerm", + "Effect": "Allow", + "Principal": "*", + "Action":["s3:GetObject"], + "Resource":["arn:aws:s3:::jokesbapak2/*"] + } + ] + }` + + err = minioClient.SetBucketPolicy(ctx, "jokesbapak2", policy) + if err != nil { + return fmt.Errorf("setting bucket policy: %w", err) + } + } + + sampleFiles := []string{ + "../../samples/sample1.jpg", + "../../samples/sample2.jpg", + "../../samples/sample3.jpg", + "../../samples/sample4.jpg", + "../../samples/sample5.jpg", + } + + for i, file := range sampleFiles { + _, err := minioClient.FPutObject(ctx, "jokesbapak2", fmt.Sprintf("sample%d.jpg", i), file, minio.PutObjectOptions{ContentType: "image/jpeg"}) + if err != nil { + return fmt.Errorf("putting object: %w", err) + } + } + + return nil +} diff --git a/api/core/joke/list.go b/api/core/joke/list.go new file mode 100644 index 0000000..5bac8bf --- /dev/null +++ b/api/core/joke/list.go @@ -0,0 +1,79 @@ +package joke + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "log" + "sort" + "time" + + "github.com/go-redis/redis/v8" + "github.com/minio/minio-go/v7" +) + +// ListJokesFromBucket provides a sorted list of joke by its' last modified field. +// +// It will return an empty slice if there is nothing on the bucket. +func ListJokesFromBucket(ctx context.Context, bucket *minio.Client, cache *redis.Client) ([]Joke, error) { + cached, err := cache.Get(ctx, "jokes:list").Result() + if err != nil && !errors.Is(err, redis.Nil) { + return []Joke{}, fmt.Errorf("acquiring joke list from cache: %w", err) + } + + if err == nil { + var jokes []Joke + err := json.Unmarshal([]byte(cached), &jokes) + if err != nil { + return []Joke{}, fmt.Errorf("unmarshalling json: %w", err) + } + + return jokes, nil + } + + objects := bucket.ListObjects(ctx, JokesBapak2Bucket, minio.ListObjectsOptions{Recursive: true}) + + var jokes []Joke + for object := range objects { + if object.Err != nil { + return []Joke{}, fmt.Errorf("enumerating objects: %w", object.Err) + } + + var contentType = object.ContentType + + if contentType == "" { + stat, err := bucket.StatObject(ctx, JokesBapak2Bucket, object.Key, minio.StatObjectOptions{}) + if err != nil { + return []Joke{}, fmt.Errorf("stat object: %w", err) + } + + contentType = stat.ContentType + } + if !object.IsDeleteMarker { + jokes = append(jokes, Joke{ModifiedAt: object.LastModified, FileName: object.Key, ContentType: contentType}) + } + } + + sort.SliceStable(jokes, func(i, j int) bool { + return jokes[i].ModifiedAt.Before(jokes[i].ModifiedAt) + }) + + defer func(jokes []Joke) { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + marshalled, err := json.Marshal(jokes) + if err != nil { + log.Printf("marshalling json: %s", err.Error()) + return + } + + err = cache.Set(ctx, "jokes:list", string(marshalled), time.Hour*6).Err() + if err != nil { + log.Printf("setting jokes:list cache: %s", err.Error()) + } + }(jokes) + + return jokes, nil +} diff --git a/api/core/joke/list_test.go b/api/core/joke/list_test.go new file mode 100644 index 0000000..ae8a083 --- /dev/null +++ b/api/core/joke/list_test.go @@ -0,0 +1,22 @@ +package joke_test + +import ( + "context" + "jokes-bapak2-api/core/joke" + "testing" + "time" +) + +func TestListJokeFromBucket(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) + defer cancel() + + jokes, err := joke.ListJokesFromBucket(ctx, bucket, cache) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + if len(jokes) != 5 { + t.Errorf("expected joke to have a length of 5, instead got %d", len(jokes)) + } +} diff --git a/api/core/joke/setter.go b/api/core/joke/setter.go deleted file mode 100644 index 335b29c..0000000 --- a/api/core/joke/setter.go +++ /dev/null @@ -1,135 +0,0 @@ -package joke - -import ( - "context" - "strconv" - "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(strconv.Itoa(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 deleted file mode 100644 index eb24245..0000000 --- a/api/core/joke/setter_test.go +++ /dev/null @@ -1,238 +0,0 @@ -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/joke/today.go b/api/core/joke/today.go new file mode 100644 index 0000000..e3471d3 --- /dev/null +++ b/api/core/joke/today.go @@ -0,0 +1,95 @@ +package joke + +import ( + "context" + "encoding/hex" + "errors" + "fmt" + "log" + "time" + + "github.com/allegro/bigcache/v3" + "github.com/go-redis/redis/v8" + "github.com/minio/minio-go/v7" +) + +// GetTodaysJoke will acquire today's joke. If it's not exists yet, +// it will acquire a random joke (from the GetRandomJoke method) +// and set it as today's joke. +func GetTodaysJoke(ctx context.Context, bucket *minio.Client, cache *redis.Client, memory *bigcache.BigCache) (image []byte, contentType string, err error) { + // Today's date: + today := time.Now().Format("2006-01-02") + + jokeFromMemory, err := memory.Get("today:file:" + today) + if err != nil && !errors.Is(err, bigcache.ErrEntryNotFound) { + return []byte{}, "", fmt.Errorf("acquiring joke from memory: %w", err) + } + + if err == nil { + contentTypeFromMemory, err := memory.Get("today:content-type:" + today) + if err != nil && !errors.Is(err, bigcache.ErrEntryNotFound) { + return []byte{}, "", fmt.Errorf("acquiring joke content type from memory: %w", err) + } + + return jokeFromMemory, string(contentTypeFromMemory), nil + } + + jokeFromCache, err := cache.Get(ctx, "jokes:today:"+today).Result() + if err != nil && !errors.Is(err, redis.Nil) { + return []byte{}, "", fmt.Errorf("acquiring joke from cache: %w", err) + } + + if err == nil { + // Get content type + contentTypeFromCache, err := cache.Get(ctx, "jokes:today:"+today+":content-type").Result() + if err != nil && !errors.Is(err, redis.Nil) { + return []byte{}, "", fmt.Errorf("acquiring content type from cache: %w", err) + } + + // Decode hex string to bytes + imageBytes, err := hex.DecodeString(jokeFromCache) + if err != nil { + return []byte{}, "", fmt.Errorf("decoding hex string: %w", err) + } + + defer func(today string, imageBytes []byte) { + err := memory.Set("today:"+today, imageBytes) + if err != nil { + log.Printf("setting memory cache: %s", err.Error()) + } + + err = memory.Set("today:"+today+":content-type", []byte(contentTypeFromCache)) + if err != nil { + log.Printf("setting memory cache: %s", err.Error()) + } + }(today, imageBytes) + + return imageBytes, contentTypeFromCache, nil + } + + // If everything not exists, we get a new random joke + randomJoke, contentType, err := GetRandomJoke(ctx, bucket, cache, memory) + if err != nil { + return []byte{}, "", fmt.Errorf("acquiring new random joke: %w", err) + } + + defer func(today string, joke []byte) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) + defer cancel() + + // Encode to hex string + encodedImage := hex.EncodeToString(joke) + + err := cache.Set(ctx, "jokes:today:"+today, encodedImage, time.Hour*24).Err() + if err != nil { + log.Printf("setting today cache to redis: %s", err.Error()) + } + + err = cache.Set(ctx, "jokes:today:"+today+":content-type", contentType, time.Hour*24).Err() + if err != nil { + log.Printf("setting today cache to redis: %s", err.Error()) + } + }(today, randomJoke) + + return randomJoke, contentType, nil +} diff --git a/api/core/joke/today_test.go b/api/core/joke/today_test.go new file mode 100644 index 0000000..20ecafc --- /dev/null +++ b/api/core/joke/today_test.go @@ -0,0 +1,39 @@ +package joke_test + +import ( + "context" + "jokes-bapak2-api/core/joke" + "testing" + "time" +) + +func TestGetTodaysJoke(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) + defer cancel() + + image, contentType, err := joke.GetTodaysJoke(ctx, bucket, cache, memory) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + if contentType != "image/jpeg" { + t.Errorf("expecting contentType of 'image/jpeg', instead got %s", contentType) + } + + if len(image) == 0 { + t.Errorf("empty image") + } + + cachedImage, cachedContentType, err := joke.GetTodaysJoke(ctx, bucket, cache, memory) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + if contentType != cachedContentType { + t.Errorf("difference on contentType: original %s vs cached %s", contentType, cachedContentType) + } + + if string(image) != string(cachedImage) { + t.Errorf("difference in image") + } +} diff --git a/api/core/joke/total.go b/api/core/joke/total.go new file mode 100644 index 0000000..4ac0b21 --- /dev/null +++ b/api/core/joke/total.go @@ -0,0 +1,67 @@ +package joke + +import ( + "context" + "errors" + "fmt" + "log" + "strconv" + "time" + + "github.com/allegro/bigcache/v3" + "github.com/go-redis/redis/v8" + "github.com/minio/minio-go/v7" +) + +// GetTotalJoke returns the total jokes that exists on the bucket. +func GetTotalJoke(ctx context.Context, bucket *minio.Client, cache *redis.Client, memory *bigcache.BigCache) (int, error) { + totalJokesFromMemory, err := memory.Get("total") + if err != nil && !errors.Is(err, bigcache.ErrEntryNotFound) { + return 0, fmt.Errorf("acquiring total joke from memory: %w", err) + } + + if err == nil { + total, err := strconv.Atoi(string(totalJokesFromMemory)) + if err != nil { + return 0, fmt.Errorf("parsing string to int: %w", err) + } + + return total, nil + } + + totalJokesFromCache, err := cache.Get(ctx, "jokes:total").Result() + if err != nil && !errors.Is(err, redis.Nil) { + return 0, fmt.Errorf("acquiring total joke from redis: %w", err) + } + + if err == nil { + total, err := strconv.Atoi(string(totalJokesFromCache)) + if err != nil { + return 0, fmt.Errorf("parsing string to int: %w", err) + } + + return total, nil + } + + jokes, err := ListJokesFromBucket(ctx, bucket, cache) + if err != nil { + return 0, fmt.Errorf("listing jokes: %w", err) + } + + defer func(total int) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) + defer cancel() + + err := cache.Set(ctx, "jokes:total", strconv.Itoa(total), time.Hour*3).Err() + if err != nil { + log.Printf("setting total jokes to redis: %s", err.Error()) + } + + err = memory.Set("total", []byte(strconv.Itoa(total))) + if err != nil { + log.Printf("setting total jokes to memory: %s", err.Error()) + } + }(len(jokes)) + + return len(jokes), nil +} diff --git a/api/core/joke/total_test.go b/api/core/joke/total_test.go new file mode 100644 index 0000000..cd62d30 --- /dev/null +++ b/api/core/joke/total_test.go @@ -0,0 +1,22 @@ +package joke_test + +import ( + "context" + "jokes-bapak2-api/core/joke" + "testing" + "time" +) + +func TestGetTotalJoke(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) + defer cancel() + + total, err := joke.GetTotalJoke(ctx, bucket, cache, memory) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + if total != 5 { + t.Errorf("expecting total to be 5 instead got %d", total) + } +} diff --git a/api/core/joke/uploader.go b/api/core/joke/uploader.go new file mode 100644 index 0000000..6f73b5a --- /dev/null +++ b/api/core/joke/uploader.go @@ -0,0 +1,28 @@ +package joke + +import ( + "context" + "fmt" + "io" + + "github.com/minio/minio-go/v7" +) + +// Uploader uploads a reader stream (io.Reader) to bucket. +func Uploader(ctx context.Context, bucket *minio.Client, key string, payload io.Reader, fileSize int64, contentType string) (string, error) { + info, err := bucket.PutObject( + ctx, + JokesBapak2Bucket, // bucketName + key, // object name, + payload, // reader + fileSize, // obuject size, + minio.PutObjectOptions{ + ContentType: contentType, + }, + ) + if err != nil { + return "", fmt.Errorf("uploading object: %w", err) + } + + return info.Key, nil +} diff --git a/api/core/schema/joke.go b/api/core/schema/joke.go index 69a9914..3ad0fd7 100644 --- a/api/core/schema/joke.go +++ b/api/core/schema/joke.go @@ -1,7 +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"` + ID int `json:"id"` + Link string `json:"link"` + Creator int `json:"creator"` } diff --git a/api/core/schema/submit.go b/api/core/schema/submit.go index 16b2466..5de8dc2 100644 --- a/api/core/schema/submit.go +++ b/api/core/schema/submit.go @@ -1,12 +1,12 @@ package schema type Submission struct { - ID int `json:"id,omitempty" db:"id"` - Link string `json:"link" form:"link" db:"link"` - Image string `json:"image,omitempty" form:"image"` - CreatedAt string `json:"created_at" db:"created_at"` - Author string `json:"author" form:"author" db:"author"` - Status int `json:"status" db:"status"` + ID int `json:"id,omitempty"` + Link string `json:"link"` + Image string `json:"image,omitempty"` + CreatedAt string `json:"created_at"` + Author string `json:"author"` + Status int `json:"status"` } type SubmissionQuery struct { diff --git a/api/core/submit/getter.go b/api/core/submit/getter.go deleted file mode 100644 index a447a62..0000000 --- a/api/core/submit/getter.go +++ /dev/null @@ -1,115 +0,0 @@ -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 deleted file mode 100644 index 3347acf..0000000 --- a/api/core/submit/getter_test.go +++ /dev/null @@ -1,91 +0,0 @@ -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() - - tx, err := c.Begin(ctx) - if err != nil { - t.Error("an error was thrown:", err) - } - defer tx.Rollback(ctx) - - _, err = tx.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) - } - - err = tx.Commit(ctx) - 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 deleted file mode 100644 index 2901baa..0000000 --- a/api/core/submit/init_test.go +++ /dev/null @@ -1,128 +0,0 @@ -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/core/submit/setter.go b/api/core/submit/setter.go deleted file mode 100644 index 74e6155..0000000 --- a/api/core/submit/setter.go +++ /dev/null @@ -1,122 +0,0 @@ -package submit - -import ( - "bytes" - "context" - "io" - "io/ioutil" - "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" -) - -// UploadImage process the image from the user to be uploaded to the cloud storage. -// Returns the image URL. -func UploadImage(client *httpclient.Client, image io.Reader) (string, error) { - hostURL := os.Getenv("IMAGE_API_URL") - fileName, err := utils.RandomString(10) - if err != nil { - return "", err - } - - body := &bytes.Buffer{} - writer := multipart.NewWriter(body) - - fw, err := writer.CreateFormField("image") - if err != nil { - return "", err - } - - _, err = io.Copy(fw, image) - if err != nil { - return "", err - } - - err = writer.Close() - if err != nil { - return "", err - } - - headers := http.Header{ - "Content-Type": []string{writer.FormDataContentType()}, - "User-Agent": []string{"JokesBapak2 API"}, - "Accept": []string{"application/json"}, - } - - requestURL, err := url.Parse(hostURL) - if err != nil { - return "", err - } - - params := url.Values{} - params.Add("key", os.Getenv("IMAGE_API_KEY")) - params.Add("name", fileName) - - requestURL.RawQuery = params.Encode() - - res, err := client.Post(requestURL.String(), bytes.NewReader(body.Bytes()), headers) - if err != nil { - return "", err - } - - defer res.Body.Close() - - responseBody, err := ioutil.ReadAll(res.Body) - if err != nil { - return "", err - } - - var data schema.ImageAPI - err = ffjson.Unmarshal(responseBody, &data) - if err != nil { - return "", err - } - - 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 deleted file mode 100644 index 524f18c..0000000 --- a/api/core/submit/setter_test.go +++ /dev/null @@ -1,25 +0,0 @@ -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/core/validator/author.go b/api/core/validator/author.go deleted file mode 100644 index afc7d43..0000000 --- a/api/core/validator/author.go +++ /dev/null @@ -1,25 +0,0 @@ -package validator - -import ( - "regexp" - "strings" -) - -func ValidateAuthor(author string) bool { - if len(author) > 200 { - return false - } - - split := strings.Split(author, " ") - if strings.HasPrefix(split[0], "<") && strings.HasSuffix(split[0], ">") { - return false - } - if !strings.HasPrefix(split[len(split)-1], "<") && !strings.HasSuffix(split[len(split)-1], ">") { - return false - } - - email := strings.Replace(split[len(split)-1], "<", "", 1) - email = strings.Replace(email, ">", "", 1) - pattern := regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$") - return pattern.MatchString(email) -} diff --git a/api/core/validator/author_test.go b/api/core/validator/author_test.go deleted file mode 100644 index cc1408b..0000000 --- a/api/core/validator/author_test.go +++ /dev/null @@ -1,40 +0,0 @@ -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/core/validator/image.go b/api/core/validator/image.go deleted file mode 100644 index eff0a7a..0000000 --- a/api/core/validator/image.go +++ /dev/null @@ -1,30 +0,0 @@ -package validator - -import ( - "errors" - "jokes-bapak2-api/utils" - "net/http" - "strings" - - "github.com/gojek/heimdall/v7/httpclient" -) - -var ValidContentType = []string{"image/jpeg", "image/png", "image/webp", "image/gif"} - -// CheckImageValidity calls to the image host to check whether it's a valid image or not. -func CheckImageValidity(client *httpclient.Client, link string) (bool, error) { - if strings.Contains(link, "https://") { - // Perform HTTP call to link - res, err := client.Get(link, http.Header{"User-Agent": []string{"JokesBapak2 API"}}) - if err != nil { - return false, err - } - - if res.StatusCode == 200 && utils.IsIn(ValidContentType, res.Header.Get("content-type")) { - return true, nil - } - - return false, nil - } - return false, errors.New("URL must use HTTPS protocol") -} diff --git a/api/core/validator/image_test.go b/api/core/validator/image_test.go deleted file mode 100644 index 8bf4143..0000000 --- a/api/core/validator/image_test.go +++ /dev/null @@ -1,50 +0,0 @@ -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 deleted file mode 100644 index ab1dde1..0000000 --- a/api/core/validator/joke.go +++ /dev/null @@ -1,69 +0,0 @@ -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 deleted file mode 100644 index c8552c9..0000000 --- a/api/core/validator/submit.go +++ /dev/null @@ -1,38 +0,0 @@ -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/go.mod b/api/go.mod index add354b..f2819d7 100644 --- a/api/go.mod +++ b/api/go.mod @@ -1,37 +1,30 @@ module jokes-bapak2-api -go 1.17 +go 1.19 require ( - github.com/Masterminds/squirrel v1.5.1 github.com/aldy505/bob v0.0.4 - github.com/aldy505/phc-crypto v1.1.0 github.com/allegro/bigcache/v3 v3.0.1 - github.com/georgysavva/scany v0.2.9 github.com/getsentry/sentry-go v0.11.0 github.com/go-redis/redis/v8 v8.11.4 - github.com/gofiber/fiber/v2 v2.21.0 - github.com/gojek/heimdall/v7 v7.0.2 - github.com/jackc/pgx v3.6.2+incompatible github.com/jackc/pgx/v4 v4.13.0 - github.com/joho/godotenv v1.4.0 - github.com/kr/text v0.2.0 // indirect github.com/pquerna/ffjson v0.0.0-20190930134022-aa0246cd15f7 - github.com/stretchr/testify v1.7.0 // indirect - golang.org/x/sys v0.0.0-20211108224332-cbcd623f202e // indirect - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect ) require ( - github.com/andybalholm/brotli v1.0.4 // indirect + github.com/go-chi/chi/v5 v5.0.7 + github.com/minio/minio-go/v7 v7.0.35 +) + +require ( github.com/cespare/xxhash/v2 v2.1.2 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/dustin/go-humanize v1.0.0 // indirect github.com/fsnotify/fsnotify v1.5.1 // indirect - github.com/gojek/valkyrie v0.0.0-20190210220504-8f62c1e7ba45 // indirect + github.com/google/uuid v1.3.0 // 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.10.0 // indirect github.com/jackc/pgio v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect @@ -39,15 +32,20 @@ require ( github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect github.com/jackc/pgtype v1.8.1 // indirect github.com/jackc/puddle v1.1.4 // indirect - github.com/klauspost/compress v1.13.6 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.15.9 // indirect + github.com/klauspost/cpuid/v2 v2.1.0 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect + github.com/minio/md5-simd v1.1.2 // indirect + github.com/minio/sha256-simd v1.0.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // 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.31.0 // indirect - github.com/valyala/tcplisten v1.0.0 // indirect - golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa // indirect + github.com/rs/xid v1.4.0 // indirect + github.com/sirupsen/logrus v1.9.0 // indirect + golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa // indirect + golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect golang.org/x/text v0.3.7 // indirect + gopkg.in/ini.v1 v1.66.6 // indirect ) diff --git a/api/go.sum b/api/go.sum index 5e1a1ce..fc9d91c 100644 --- a/api/go.sum +++ b/api/go.sum @@ -2,32 +2,20 @@ github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOv github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= github.com/CloudyKit/jet/v3 v3.0.0/go.mod h1:HKQPgSJmdK8hdoAbKUUWajkHyHo4RaU5rMdUywE7VMo= -github.com/DataDog/datadog-go v3.7.1+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= -github.com/Masterminds/squirrel v1.5.1 h1:kWAKlLLJFxZG7N2E0mBMNWVp5AuUX+JUrnhFN74Eg+w= -github.com/Masterminds/squirrel v1.5.1/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0= -github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/aldy505/bob v0.0.4 h1:36lj6JUHxGp7yt672aKcC8gk6rXpIRO/aqclQ9aXDa8= github.com/aldy505/bob v0.0.4/go.mod h1:uckrZqhg9zmbLA4MpKueIeQfrdriNqbmMalvf0+qPG4= -github.com/aldy505/phc-crypto v1.1.0 h1:BagRKCrB7FOYy5vnuXR6xs6ml2gJD/CvSJkX/Ozo63w= -github.com/aldy505/phc-crypto v1.1.0/go.mod h1:LJugClOkOWKnpLrWhSaIDRN/5ftvZPD48S5oXsT7iTg= github.com/allegro/bigcache/v3 v3.0.1 h1:Q4Xl3chywXuJNOw7NV+MeySd3zGQDj4KCpkCg0te8mc= github.com/allegro/bigcache/v3 v3.0.1/go.mod h1:aPyh7jEvrog9zAwx5N7+JUQX5dZTSGpxF1LAR4dr35I= -github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= -github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= -github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= -github.com/cactus/go-statsd-client/statsd v0.0.0-20200423205355-cb0885a1018c/go.mod h1:l/bIBLeOl9eX+wxJAzxS4TveKRtAqlyDpHjhkfO0MEI= github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= -github.com/cockroachdb/cockroach-go/v2 v2.0.3 h1:ZA346ACHIZctef6trOTwBAEvPVm1k0uLm/bb2Atc+S8= -github.com/cockroachdb/cockroach-go/v2 v2.0.3/go.mod h1:hAuDgiVgDVkfirP9JnhXEfcXEPRKBpYdGz+l7mvYSzw= github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= @@ -36,19 +24,17 @@ github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7 github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM= -github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw= github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= @@ -57,13 +43,13 @@ github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4 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= github.com/getsentry/sentry-go v0.11.0 h1:qro8uttJGvNAMr5CLcFI9CHR0aDzXl0Vs3Pmw/oTPg8= github.com/getsentry/sentry-go v0.11.0/go.mod h1:KBQIxiZAetw62Cj8Ri964vAEWVdgfaUCn30Q3bCvANo= github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM= github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98= +github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8= +github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= @@ -71,24 +57,13 @@ github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= github.com/go-redis/redis/v8 v8.11.4 h1:kHoYkfZP6+pe04aFTnhDH6GDROa5yJdHJVNxV3F46Tg= github.com/go-redis/redis/v8 v8.11.4/go.mod h1:2Z2wHZXdQpCDXEGzqMockDpNyYvi2l4Pxt6RJr792+w= -github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= -github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= -github.com/gofiber/fiber/v2 v2.21.0 h1:tdRNrgqWqcHWBwE3o51oAleEVsil4Ro02zd2vMEuP4Q= -github.com/gofiber/fiber/v2 v2.21.0/go.mod h1:MR1usVH3JHYRyQwMe2eZXRSZHRX38fkV+A7CPB+DlDQ= -github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= -github.com/gojek/heimdall/v7 v7.0.2 h1:+YutGXZ8oEWbCJIwjRnkKmoTl+Oxt1Urs3hc/FR0sxU= -github.com/gojek/heimdall/v7 v7.0.2/go.mod h1:Z43HtMid7ysSjmsedPTXAki6jcdcNVnjn5pmsTyiMic= -github.com/gojek/valkyrie v0.0.0-20180215180059-6aee720afcdf/go.mod h1:QzhUKaYKJmcbTnCYCAVQrroCOY7vOOI8cSQ4NbuhYf0= -github.com/gojek/valkyrie v0.0.0-20190210220504-8f62c1e7ba45 h1:jrnJW3T+GsaQCD26fe6ERlNpgLB5HlekzBU4lOscr80= -github.com/gojek/valkyrie v0.0.0-20190210220504-8f62c1e7ba45/go.mod h1:QzhUKaYKJmcbTnCYCAVQrroCOY7vOOI8cSQ4NbuhYf0= -github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= @@ -99,7 +74,6 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -110,6 +84,8 @@ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= @@ -122,21 +98,13 @@ github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/ github.com/iris-contrib/jade v1.1.3/go.mod h1:H/geBymxJhShH5kecoiOCSssPX7QWYH7UaeZTSWddIk= github.com/iris-contrib/pongo2 v0.0.1/go.mod h1:Ssh+00+3GAZqSQb30AvBRNxBx7rf0GqwkjqxNd0u65g= github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrOcOqfqxa4hXw= -github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0= github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= 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= -github.com/jackc/pgconn v1.4.0/go.mod h1:Y2O3ZDF0q4mMacyWV3AstPJpeHXWGEetiFttmq5lahk= -github.com/jackc/pgconn v1.5.0/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI= -github.com/jackc/pgconn v1.5.1-0.20200601181101-fa742c524853/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI= -github.com/jackc/pgconn v1.6.4/go.mod h1:w2pne1C2tZgP+TvjqLpOigGzNqjBgQW9dUw/4Chex78= -github.com/jackc/pgconn v1.7.0/go.mod h1:sF/lPpNEMEOp+IYhyQGdAvrG20gWf6A1tKlr0v7JMeA= github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= @@ -150,61 +118,37 @@ github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5W github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A= github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= -github.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgproto3/v2 v2.0.2/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgproto3/v2 v2.0.5/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgproto3/v2 v2.1.1 h1:7PQ/4gLoqnl87ZxL7xjO0DR5gYuviDCZxQJsUlFW1eI= github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= -github.com/jackc/pgtype v1.2.0/go.mod h1:5m2OfMh1wTK7x+Fk952IDmI4nw3nPrvtQdM0ZT4WpC0= -github.com/jackc/pgtype v1.3.0/go.mod h1:b0JqxHvPmljG+HQ5IsvQ0yqeSi4nGcDTVjFoiLDb0Ik= -github.com/jackc/pgtype v1.3.1-0.20200510190516-8cd94a14c75a/go.mod h1:vaogEUkALtxZMCH411K+tKzNpwzCKU+AnPzBKZ+I+Po= -github.com/jackc/pgtype v1.3.1-0.20200606141011-f6355165a91c/go.mod h1:cvk9Bgu/VzJ9/lxTO5R5sf80p0DiucVtN7ZxvaC4GmQ= -github.com/jackc/pgtype v1.4.2/go.mod h1:JCULISAZBFGrHaOXIIFiyfzW5VY0GRitRr8NeJsrdig= github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= github.com/jackc/pgtype v1.8.1 h1:9k0IXtdJXHJbyAWQgbWr1lU+MEhPXZz6RIXxfR5oxXs= github.com/jackc/pgtype v1.8.1/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= -github.com/jackc/pgx v3.6.2+incompatible h1:2zP5OD7kiyR3xzRYMhOcXVvkDZsImVXfj+yIyTQf3/o= -github.com/jackc/pgx v3.6.2+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I= github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= -github.com/jackc/pgx/v4 v4.5.0/go.mod h1:EpAKPLdnTorwmPUUsqrPxy5fphV18j9q3wrfRXgo+kA= -github.com/jackc/pgx/v4 v4.6.0/go.mod h1:vPh43ZzxijXUVJ+t/EmXBtFmbFVO72cuneCT9oAlxAg= -github.com/jackc/pgx/v4 v4.6.1-0.20200510190926-94ba730bb1e9/go.mod h1:t3/cdRQl6fOLDxqtlyhe9UWgfIi9R8+8v8GKV5TRA/o= -github.com/jackc/pgx/v4 v4.6.1-0.20200606145419-4e5062306904/go.mod h1:ZDaNWkt9sW1JMiNn0kdYBaLelIhw7Pg4qd+Vk6tw7Hg= -github.com/jackc/pgx/v4 v4.8.1/go.mod h1:4HOLxrl8wToZJReD04/yB20GDwf4KBYETvlHciCnwW0= github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= github.com/jackc/pgx/v4 v4.13.0 h1:JCjhT5vmhMAf/YwBHLvrBn4OGdIQBiFG6ym8Zmdx570= github.com/jackc/pgx/v4 v4.13.0/go.mod h1:9P4X524sErlaxj0XSGZk7s+LD0eOyu1ZDUrrpznYDF0= github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v1.1.2/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.4 h1:5Ey/o5IfV7dYX6Znivq+N9MdK1S18OJI5OJq6EAAADw= github.com/jackc/puddle v1.1.4/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jinzhu/gorm v1.9.12/go.mod h1:vhTjlKSJUTWNtcbQtrMBFCxy7eXTzeCAzfL5fBZT/Qs= -github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= -github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= -github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= -github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= -github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= github.com/kataras/golog v0.0.10/go.mod h1:yJ8YKCmyL+nWjERB90Qwn+bdyBZsaQwU3bTVFgkFIp8= @@ -215,20 +159,19 @@ github.com/kataras/sitemap v0.0.5/go.mod h1:KY2eugMKiPwsJgx7+U103YZehfvNGOXURubc github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= -github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= -github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= -github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/compress v1.15.9 h1:wKRjX6JRtDdrE9qwa4b/Cip7ACOshUI4smpCQanqjSY= +github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.1.0 h1:eyi1Ad2aNJMW95zcSbmGg7Cg6cq3ADwLpMAP96d8rF0= +github.com/klauspost/cpuid/v2 v2.1.0/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/labstack/echo/v4 v4.1.11/go.mod h1:i541M3Fj6f76NZtHSj7TXnyM8n2gaodfvfxNnFqi74g= github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= @@ -237,10 +180,7 @@ github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhR github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.4.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= @@ -252,17 +192,24 @@ github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= -github.com/mattn/go-sqlite3 v2.0.1+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= github.com/mediocregopher/radix/v3 v3.4.2/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8= github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc= +github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= +github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= +github.com/minio/minio-go/v7 v7.0.35 h1:JuPPxWLdxQmNLSaS8AWZnO5HBadeI1xg6FGrEELQEVU= +github.com/minio/minio-go/v7 v7.0.35/go.mod h1:nCrRzjoSUQh8hgKKtu3Y708OLvRLtuASMg2/nvmbarw= +github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= +github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= @@ -290,9 +237,10 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pquerna/ffjson v0.0.0-20190930134022-aa0246cd15f7 h1:xoIK0ctDddBMnc74udxJYBqlo9Ylnsp1waqjLsnef20= github.com/pquerna/ffjson v0.0.0-20190930134022-aa0246cd15f7/go.mod h1:YARuvh7BUWHNhzDq2OM5tzR2RiCcN2D7sapiKyCel/M= -github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/rs/xid v1.4.0 h1:qd7wPTDkN6KQx2VmMBLrpHkiyQwgFXRnkOLacUiaSNY= +github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= @@ -301,13 +249,13 @@ github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdh github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= -github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/shopspring/decimal v0.0.0-20200419222939-1884f454f8ea/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= @@ -319,8 +267,6 @@ github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DM github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= -github.com/stretchr/objx v0.3.0 h1:NGXK3lHquSN08v5vWalVI/L8XU9hdzE/G6xsrze47As= -github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -332,15 +278,10 @@ github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVM github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= -github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w= -github.com/valyala/fasthttp v1.31.0 h1:lrauRLII19afgCs2fnWRJ4M5IkV0lo2FqA61uGkNBfE= -github.com/valyala/fasthttp v1.31.0/go.mod h1:2rsYD01CKFrjjsvFxx75KlEUNpWNBY9JWD3K/7o2Cus= github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= -github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= -github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= @@ -364,35 +305,28 @@ go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa h1:idItI2DDfCokpg0N51B2VtiLdJ4vAuXC9fnCb2gACo4= -golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -401,8 +335,8 @@ golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= -golang.org/x/net v0.0.0-20210510120150-4163338589ed h1:p9UgmWI9wKpfYmgaV/IZKGdXc5qEK45tDwwwDyjS26I= -golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -417,7 +351,6 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -429,11 +362,12 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211108224332-cbcd623f202e h1:9nbuBbpiqktwdlzHKUohsD5+y2a0QvX98gIWK2ARkqc= -golang.org/x/sys v0.0.0-20211108224332-cbcd623f202e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -463,8 +397,6 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -475,19 +407,18 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.66.6 h1:LATuAqN/shcYAOkv3wl2L4rkaKqkcgTBQjOyYDvcPKI= +gopkg.in/ini.v1 v1.66.6/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= diff --git a/api/handler/health/health.go b/api/handler/health/health.go index fd8d767..03be5b5 100644 --- a/api/handler/health/health.go +++ b/api/handler/health/health.go @@ -1,40 +1,47 @@ package health import ( + "context" + "net/http" + "time" + "github.com/go-redis/redis/v8" - "github.com/gofiber/fiber/v2" - "github.com/jackc/pgx/v4/pgxpool" + "github.com/minio/minio-go/v7" ) +// Dependencies provides a struct for dependency injection +// on health package type Dependencies struct { - DB *pgxpool.Pool - Redis *redis.Client + Bucket *minio.Client + Cache *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() +// Health provides a http handler for healthcheck +func (d *Dependencies) Health(w http.ResponseWriter, r *http.Request) { + ctx, cancel := context.WithTimeout(r.Context(), time.Second*15) + defer cancel() - // Ping REDIS database - err = d.Redis.Ping(c.Context()).Err() + var bucketOk = true + var cacheOk = true + + cancel, err := d.Bucket.HealthCheck(time.Second * 15) if err != nil { - return c. - Status(fiber.StatusServiceUnavailable). - JSON(Error{ - Error: "REDIS: " + err.Error(), - }) + bucketOk = false } - _, 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(), - }) + if cancel != nil { + cancel() } - return c.SendStatus(fiber.StatusOK) + + _, err = d.Cache.Ping(ctx).Result() + if err != nil { + cacheOk = false + } + + if !bucketOk || !cacheOk { + w.WriteHeader(http.StatusServiceUnavailable) + return + } + + w.WriteHeader(http.StatusOK) } diff --git a/api/handler/health/schema.go b/api/handler/health/schema.go deleted file mode 100644 index cb99e1f..0000000 --- a/api/handler/health/schema.go +++ /dev/null @@ -1,5 +0,0 @@ -package health - -type Error struct { - Error string `json:"error"` -} diff --git a/api/handler/joke/dependencies.go b/api/handler/joke/dependencies.go index 75d0b4c..ce8c737 100644 --- a/api/handler/joke/dependencies.go +++ b/api/handler/joke/dependencies.go @@ -1,17 +1,15 @@ 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" + "github.com/minio/minio-go/v7" ) +// Dependencies provides a struct for dependency injection +// on joke package type Dependencies struct { - DB *pgxpool.Pool Redis *redis.Client Memory *bigcache.BigCache - HTTP *httpclient.Client - Query squirrel.StatementBuilderType + Bucket *minio.Client } diff --git a/api/handler/joke/get.go b/api/handler/joke/get.go new file mode 100644 index 0000000..fb9edea --- /dev/null +++ b/api/handler/joke/get.go @@ -0,0 +1,66 @@ +package joke + +import ( + core "jokes-bapak2-api/core/joke" + "net/http" + "strconv" + + "github.com/go-chi/chi/v5" +) + +// TodayJoke provides http handler for today's joke +func (d *Dependencies) TodayJoke(w http.ResponseWriter, r *http.Request) { + joke, contentType, err := core.GetTodaysJoke(r.Context(), d.Bucket, d.Redis, d.Memory) + if err != nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(`{"error":` + strconv.Quote(err.Error()) + `}`)) + return + } + + w.Header().Set("Content-Type", contentType) + w.WriteHeader(http.StatusOK) + w.Write(joke) +} + +// SingleJoke provides http handler for acquiring random single joke +func (d *Dependencies) SingleJoke(w http.ResponseWriter, r *http.Request) { + joke, contentType, err := core.GetRandomJoke(r.Context(), d.Bucket, d.Redis, d.Memory) + if err != nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(`{"error":` + strconv.Quote(err.Error()) + `}`)) + return + } + + w.Header().Set("Content-Type", contentType) + w.WriteHeader(http.StatusOK) + w.Write(joke) + +} + +// JokeByID provides http handler for acquiring a joke by ID +func (d *Dependencies) JokeByID(w http.ResponseWriter, r *http.Request) { + id := chi.URLParamFromCtx(r.Context(), "id") + + // Parse id to int + parsedID, err := strconv.Atoi(id) + if err != nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(`{"error":` + strconv.Quote(err.Error()) + `}`)) + return + } + + joke, contentType, err := core.GetJokeByID(r.Context(), d.Bucket, d.Redis, d.Memory, parsedID) + if err != nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(`{"error":` + strconv.Quote(err.Error()) + `}`)) + return + } + + w.Header().Set("Content-Type", contentType) + w.WriteHeader(http.StatusOK) + w.Write(joke) +} diff --git a/api/handler/joke/joke_add.go b/api/handler/joke/joke_add.go deleted file mode 100644 index 13f8f75..0000000 --- a/api/handler/joke/joke_add.go +++ /dev/null @@ -1,69 +0,0 @@ -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 deleted file mode 100644 index 689ce63..0000000 --- a/api/handler/joke/joke_delete.go +++ /dev/null @@ -1,51 +0,0 @@ -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 deleted file mode 100644 index ea75bfb..0000000 --- a/api/handler/joke/joke_get.go +++ /dev/null @@ -1,151 +0,0 @@ -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 deleted file mode 100644 index 298114f..0000000 --- a/api/handler/joke/joke_total.go +++ /dev/null @@ -1,42 +0,0 @@ -package joke - -import ( - "errors" - core "jokes-bapak2-api/core/joke" - - "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: string(total), - }) -} diff --git a/api/handler/joke/joke_update.go b/api/handler/joke/joke_update.go deleted file mode 100644 index 2562a89..0000000 --- a/api/handler/joke/joke_update.go +++ /dev/null @@ -1,86 +0,0 @@ -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/handler/joke/schema.go b/api/handler/joke/schema.go deleted file mode 100644 index b0180c1..0000000 --- a/api/handler/joke/schema.go +++ /dev/null @@ -1,16 +0,0 @@ -package joke - -type ResponseJoke struct { - Link string `json:"link,omitempty"` - Message string `json:"message,omitempty"` -} - -type Today struct { - Date string `redis:"today:date"` - Image string `redis:"today:image"` - ContentType string `redis:"today:contentType"` -} - -type Error struct { - Error string `json:"error"` -} diff --git a/api/handler/joke/total.go b/api/handler/joke/total.go new file mode 100644 index 0000000..bc250f5 --- /dev/null +++ b/api/handler/joke/total.go @@ -0,0 +1,22 @@ +package joke + +import ( + core "jokes-bapak2-api/core/joke" + "net/http" + "strconv" +) + +// TotalJokes provides a HTTP handler for acquiring total jokes +func (d *Dependencies) TotalJokes(w http.ResponseWriter, r *http.Request) { + total, err := core.GetTotalJoke(r.Context(), d.Bucket, d.Redis, d.Memory) + if err != nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(`{"error":` + strconv.Quote(err.Error()) + `}`)) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"message":` + strconv.Itoa(total) + `}`)) +} diff --git a/api/handler/submit/dependencies.go b/api/handler/submit/dependencies.go deleted file mode 100644 index 5f73c1b..0000000 --- a/api/handler/submit/dependencies.go +++ /dev/null @@ -1,17 +0,0 @@ -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 deleted file mode 100644 index 5525c1c..0000000 --- a/api/handler/submit/submit_add.go +++ /dev/null @@ -1,99 +0,0 @@ -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 deleted file mode 100644 index ec04888..0000000 --- a/api/handler/submit/submit_get.go +++ /dev/null @@ -1,28 +0,0 @@ -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 07c430d..059769d 100644 --- a/api/main.go +++ b/api/main.go @@ -1,55 +1,72 @@ package main import ( + "errors" "log" + "net/http" "os" "os/signal" "context" "jokes-bapak2-api/core/joke" - "jokes-bapak2-api/platform/database" "jokes-bapak2-api/routes" + "github.com/go-redis/redis/v8" + "github.com/minio/minio-go/v7" + "github.com/minio/minio-go/v7/pkg/credentials" + "time" - "github.com/gofiber/fiber/v2" - _ "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" + "github.com/go-chi/chi/v5" ) func main() { - // Setup PostgreSQL - poolConfig, err := pgxpool.ParseConfig(os.Getenv("DATABASE_URL")) - if err != nil { - log.Panicln("Unable to create pool config", err) + redisURL, ok := os.LookupEnv("REDIS_URL") + if !ok { + redisURL = "redis://@localhost:6379" } - 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) + minioHost, ok := os.LookupEnv("MINIO_HOST") + if !ok { + minioHost = "localhost:9000" } - defer db.Close() - // Setup Redis - opt, err := redis.ParseURL(os.Getenv("REDIS_URL")) - if err != nil { - log.Fatalln(err) + minioID, ok := os.LookupEnv("MINIO_ACCESS_ID") + if !ok { + minioID = "minio" + } + + minioSecret, ok := os.LookupEnv("MINIO_SECRET_KEY") + if !ok { + minioSecret = "password" + } + + minioToken, ok := os.LookupEnv("MINIO_TOKEN") + if !ok { + minioToken = "" + } + + sentryDsn, ok := os.LookupEnv("SENTRY_DSN") + if !ok { + sentryDsn = "" + } + + port, ok := os.LookupEnv("PORT") + if !ok { + port = "5000" + } + + hostname, ok := os.LookupEnv("HOSTNAME") + if !ok { + hostname = "127.0.0.1" + } + + environment, ok := os.LookupEnv("ENVIRONMENT") + if !ok { + environment = "development" } - rdb := redis.NewClient(opt) - defer rdb.Close() // Setup In Memory memory, err := bigcache.NewBigCache(bigcache.DefaultConfig(6 * time.Hour)) @@ -58,122 +75,87 @@ func main() { } defer memory.Close() + // Setup MinIO + minioClient, err := minio.New(minioHost, &minio.Options{ + Creds: credentials.NewStaticV4(minioID, minioSecret, minioToken), + }) + if err != nil { + log.Fatalf("setting up minio client: %s", err.Error()) + return + } + + parsedRedisURL, err := redis.ParseURL(redisURL) + if err != nil { + log.Fatalf("parsing redis url: %s", err.Error()) + return + } + + redisClient := redis.NewClient(parsedRedisURL) + defer func() { + err := redisClient.Close() + if err != nil { + log.Printf("closing redis client: %s", err.Error()) + } + }() + // Setup Sentry err = sentry.Init(sentry.ClientOptions{ - Dsn: os.Getenv("SENTRY_DSN"), - Environment: os.Getenv("ENV"), + Dsn: sentryDsn, + Environment: environment, AttachStacktrace: true, // Enable printing of SDK debug messages. // Useful when getting started or trying to figure something out. - Debug: true, + Debug: environment != "production", }) if err != nil { - log.Panicln(err) + log.Fatalf("setting up sentry: %s", err.Error()) + return } 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) + _, _, err = joke.GetTodaysJoke(setupCtx, minioClient, redisClient, memory) if err != nil { - sentry.CaptureException(err) - log.Panicln(err) + log.Fatalf("getting initial joke data: %s", err.Error()) + return } - err = joke.SetAllJSONJoke(db, setupCtx, memory) - if err != nil { - log.Panicln(err) - } - err = joke.SetTotalJoke(db, setupCtx, memory) - if err != nil { - log.Panicln(err) + healthRouter := routes.Health(minioClient, redisClient) + jokeRouter := routes.Joke(minioClient, redisClient, memory) + + router := chi.NewRouter() + + router.Mount("/health", healthRouter) + router.Mount("/", jokeRouter) + + server := &http.Server{ + Handler: router, + Addr: hostname + ":" + port, + ReadTimeout: time.Minute, + WriteTimeout: time.Minute, + IdleTimeout: time.Second * 30, + ReadHeaderTimeout: time.Minute, } - timeoutDefault := time.Minute * 1 - - app := fiber.New(fiber.Config{ - ReadTimeout: timeoutDefault, - WriteTimeout: timeoutDefault, - CaseSensitive: true, - DisableKeepalive: true, - ErrorHandler: errorHandler, - }) - - app.Use(limiter.New(limiter.Config{ - Max: 30, - Expiration: 1 * time.Minute, - LimitReached: limitHandler, - })) - - 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" { - StartServer(app) - } else { - StartServerWithGracefulShutdown(app) - } -} - -func limitHandler(c *fiber.Ctx) error { - return c.Status(fiber.StatusTooManyRequests).JSON(fiber.Map{ - "message": "we only allow up to 15 request per minute", - }) -} - -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. - idleConnsClosed := make(chan struct{}) + exitSignal := make(chan os.Signal, 1) + signal.Notify(exitSignal, os.Interrupt) go func() { - sigint := make(chan os.Signal, 1) - signal.Notify(sigint, os.Interrupt) // Catch OS signals. - <-sigint - - // Received an interrupt signal, shutdown. - if err := a.Shutdown(); err != nil { - // Error from closing listeners, or context timeout: - log.Printf("Oops... Server is not shutting down! Reason: %v", err) + err := server.ListenAndServe() + if err != nil && !errors.Is(err, http.ErrServerClosed) { + log.Fatalf("listening http server: %v", err) } - - close(idleConnsClosed) }() - // Run server. - if err := a.Listen(os.Getenv("HOST") + ":" + os.Getenv("PORT")); err != nil { - log.Printf("Oops... Server is not running! Reason: %v", err) - } + <-exitSignal - <-idleConnsClosed -} + shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), time.Second*30) + defer shutdownCancel() -// StartServer func for starting a simple server. -func StartServer(a *fiber.App) { - // Run server. - if err := a.Listen(os.Getenv("HOST") + ":" + os.Getenv("PORT")); err != nil { - log.Printf("Oops... Server is not running! Reason: %v", err) + err = server.Shutdown(shutdownCtx) + if err != nil { + log.Printf("shutting down http server: %v", err) } } diff --git a/api/middleware/auth.go b/api/middleware/auth.go deleted file mode 100644 index 3ef847d..0000000 --- a/api/middleware/auth.go +++ /dev/null @@ -1,58 +0,0 @@ -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/middleware/schema.go b/api/middleware/schema.go deleted file mode 100644 index 9659dce..0000000 --- a/api/middleware/schema.go +++ /dev/null @@ -1,12 +0,0 @@ -package middleware - -type Auth struct { - ID int `json:"id" form:"id" db:"id"` - Key string `json:"key" form:"key" db:"key"` - 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/middleware/validation.go b/api/middleware/validation.go deleted file mode 100644 index 8e12c7c..0000000 --- a/api/middleware/validation.go +++ /dev/null @@ -1,27 +0,0 @@ -package middleware - -import ( - "regexp" - - "github.com/gofiber/fiber/v2" -) - -func OnlyIntegerAsID() fiber.Handler { - return func(c *fiber.Ctx) error { - regex, err := regexp.Compile(`([0-9]+)`) - if err != nil { - return err - } - - loc := regex.FindStringIndex(c.Params("id")) - if loc[1] == len(c.Params("id")) { - return c.Next() - } - - return c. - Status(fiber.StatusBadRequest). - JSON(Error{ - Error: "only numbers are allowed as ID", - }) - } -} diff --git a/api/routes/dependencies.go b/api/routes/dependencies.go deleted file mode 100644 index 65f6bb5..0000000 --- a/api/routes/dependencies.go +++ /dev/null @@ -1,19 +0,0 @@ -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 index 3b274b3..d6cda70 100644 --- a/api/routes/health.go +++ b/api/routes/health.go @@ -2,18 +2,22 @@ package routes import ( "jokes-bapak2-api/handler/health" - "time" - "github.com/gofiber/fiber/v2/middleware/cache" + "github.com/go-chi/chi/v5" + "github.com/go-redis/redis/v8" + "github.com/minio/minio-go/v7" ) -func (d *Dependencies) Health() { - // Health check - deps := health.Dependencies{ - DB: d.DB, - Redis: d.Redis, +// Health provides route for healthcheck endpoints. +func Health(bucket *minio.Client, cache *redis.Client) *chi.Mux { + dependency := &health.Dependencies{ + Bucket: bucket, + Cache: cache, } - 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) + router := chi.NewRouter() + + router.Get("/", dependency.Health) + + return router } diff --git a/api/routes/joke.go b/api/routes/joke.go index 88d531f..df1fa4f 100644 --- a/api/routes/joke.go +++ b/api/routes/joke.go @@ -2,42 +2,34 @@ package routes import ( "jokes-bapak2-api/handler/joke" - "jokes-bapak2-api/middleware" - "time" - "github.com/gofiber/fiber/v2/middleware/cache" + "github.com/allegro/bigcache/v3" + "github.com/go-chi/chi/v5" + "github.com/go-redis/redis/v8" + "github.com/minio/minio-go/v7" ) -func (d *Dependencies) Joke() { - deps := joke.Dependencies{ - DB: d.DB, - Redis: d.Redis, - Memory: d.Memory, - HTTP: d.HTTP, - Query: d.Query, +// Joke provides route for jokes. +func Joke(bucket *minio.Client, cache *redis.Client, memory *bigcache.BigCache) *chi.Mux { + deps := &joke.Dependencies{ + Memory: memory, + Bucket: bucket, + Redis: cache, } + + router := chi.NewRouter() + // Single route - d.App.Get("/", deps.SingleJoke) - d.App.Get("/v1", deps.SingleJoke) + router.Get("/", 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) + router.Get("/today", deps.TodayJoke) // Joke by ID - d.App.Get("/id/:id", middleware.OnlyIntegerAsID(), deps.JokeByID) - d.App.Get("/v1/id/:id", middleware.OnlyIntegerAsID(), deps.JokeByID) + router.Get("/id/{id}", 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) + router.Get("/total", 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) + return router } diff --git a/api/routes/submit.go b/api/routes/submit.go deleted file mode 100644 index f880e8b..0000000 --- a/api/routes/submit.go +++ /dev/null @@ -1,33 +0,0 @@ -package routes - -import ( - "jokes-bapak2-api/handler/submit" - "time" - - "github.com/gofiber/fiber/v2" - "github.com/gofiber/fiber/v2/middleware/cache" -) - -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 - d.App.Get( - "/submit", - cache.New(cache.Config{ - Expiration: 5 * time.Minute, - KeyGenerator: func(c *fiber.Ctx) string { - return c.OriginalURL() - }, - }), - deps.GetSubmission) - - // Add a joke - d.App.Post("/submit", deps.SubmitJoke) -} diff --git a/api/samples/sample1.jpg b/api/samples/sample1.jpg new file mode 100644 index 0000000..2547b23 Binary files /dev/null and b/api/samples/sample1.jpg differ diff --git a/api/samples/sample2.jpg b/api/samples/sample2.jpg new file mode 100644 index 0000000..7ea17ed Binary files /dev/null and b/api/samples/sample2.jpg differ diff --git a/api/samples/sample3.jpg b/api/samples/sample3.jpg new file mode 100644 index 0000000..1612777 Binary files /dev/null and b/api/samples/sample3.jpg differ diff --git a/api/samples/sample4.jpg b/api/samples/sample4.jpg new file mode 100644 index 0000000..8cb907e Binary files /dev/null and b/api/samples/sample4.jpg differ diff --git a/api/samples/sample5.jpg b/api/samples/sample5.jpg new file mode 100644 index 0000000..9be2997 Binary files /dev/null and b/api/samples/sample5.jpg differ diff --git a/database/postgres/Dockerfile b/database/postgres/Dockerfile index 8ec5916..43b28c8 100644 --- a/database/postgres/Dockerfile +++ b/database/postgres/Dockerfile @@ -1,4 +1,4 @@ -FROM postgres:13.3-alpine +FROM postgres:14.5-alpine WORKDIR /var/lib/postgresql diff --git a/docker-compose.yml b/docker-compose.yml index 89e6abc..f6abde7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -27,10 +27,8 @@ services: db: build: ./database/postgres/ command: > - -c ssl=on - -c ssl_cert_file=/var/lib/postgresql/server.crt - -c ssl_key_file=/var/lib/postgresql/server.key - restart: always + -c ssl=on -c ssl_cert_file=/var/lib/postgresql/server.crt -c ssl_key_file=/var/lib/postgresql/server.key + restart: unless-stopped ports: - 5432:5432 environment: @@ -40,18 +38,51 @@ services: PGDATA: /data/postgres # I got this key from somewhere. It works when you run it locally. POSTGRES_SSL_CA_CERT: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURjekNDQWx1Z0F3SUJBZ0lVR3lDaElvR3g0 + healthcheck: + test: pg_isready -U postgres + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s volumes: - ./database/postgres/data:/data/postgres cache: image: redis:6.2.4-alpine - restart: always + restart: unless-stopped ports: - 6379:6379 + healthcheck: + test: redis-cli -a foobared ping | grep PONG + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s volumes: - ./database/redis/etc:/usr/local/etc/redis - ./database/redis/data:/data + bucket: + image: quay.io/minio/minio:RELEASE.2022-02-05T04-40-59Z + command: server /data --console-address ":9001" + restart: unless-stopped + ports: + - 9001:9001 + - 9000:9000 + environment: + MINIO_ROOT_USER: minio + MINIO_ROOT_PASSWORD: password + MINIO_ACCESS_KEY: minio_access_key + MINIO_SECRET_KEY: minio_secret_key + healthcheck: + test: "curl -f http://localhost:9000/minio/health/live" + interval: 30s + timeout: 10s + retries: 5 + start_period: 60s + volumes: + - ./data/minio:/data + cache-admin: image: rediscommander/redis-commander:latest restart: always @@ -73,4 +104,4 @@ services: environment: DATABASE_URL: postgres://postgres:password@db/jokesbapak2 depends_on: - - db \ No newline at end of file + - db