Merge pull request #14 from aldy505/refactor/backend-rewrite

refactor: rewrite backend
This commit is contained in:
Reinaldy Rafli 2022-09-03 21:41:44 +07:00 committed by GitHub
commit 0b880bd1e0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
67 changed files with 1286 additions and 3194 deletions

View File

@ -2,33 +2,34 @@ name: CI
on: on:
push: push:
branches: [ "master" ] branches: ["master"]
jobs: jobs:
api-build: api-build:
name: API name: API
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: golang:1.17-buster container: golang:1.19-bullseye
timeout-minutes: 15 timeout-minutes: 15
services: services:
postgres: bucket:
image: postgres:13-alpine image: minio/minio:edge-cicd
env: env:
PGDATABASE: jokesbapak2 MINIO_ROOT_USER: root
POSTGRES_DB: jokesbapak2 MINIO_ROOT_PASSWORD: verysecurepassword
PGUSER: postgres MINIO_ACCESS_KEY: minio_access_key
POSTGRES_USER: postgres MINIO_SECRET_KEY: minio_access_key
PGPASSWORD: password
POSTGRES_PASSWORD: password
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports: 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: redis:
image: redis:6-alpine image: redis:6-bullseye
ports: ports:
- 6379:6379 - 6379:6379
defaults: defaults:
@ -46,11 +47,13 @@ jobs:
run: go build main.go run: go build main.go
- name: Run test & coverage - 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:
ENV: development ENV: development
PORT: 5000 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 REDIS_URL: redis://@redis:6379
- name: Initialize CodeQL - name: Initialize CodeQL
@ -79,7 +82,7 @@ jobs:
client-build: client-build:
name: Client name: Client
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: node:14-buster container: node:18-bullseye
timeout-minutes: 15 timeout-minutes: 15
defaults: defaults:
run: run:
@ -119,4 +122,4 @@ jobs:
with: with:
environment: production environment: production
set_commits: skip set_commits: skip
version: ${{ github.sha }} version: ${{ github.sha }}

View File

@ -2,13 +2,13 @@ name: PR
on: on:
pull_request: pull_request:
branches: [ "*" ] branches: ["*"]
jobs: jobs:
client-build: client-build:
name: Client name: Client
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: node:14-buster container: node:18-bullseye
timeout-minutes: 15 timeout-minutes: 15
defaults: defaults:
run: run:
@ -45,27 +45,28 @@ jobs:
api-build: api-build:
name: API name: API
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: golang:1.17-buster container: golang:1.19-bullseye
timeout-minutes: 15 timeout-minutes: 15
services: services:
postgres: bucket:
image: postgres:13-alpine image: minio/minio:edge-cicd
env: env:
PGDATABASE: jokesbapak2 MINIO_ROOT_USER: root
POSTGRES_DB: jokesbapak2 MINIO_ROOT_PASSWORD: verysecurepassword
PGUSER: postgres MINIO_ACCESS_KEY: minio_access_key
POSTGRES_USER: postgres MINIO_SECRET_KEY: minio_access_key
PGPASSWORD: password
POSTGRES_PASSWORD: password
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports: 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: redis:
image: redis:6-alpine image: redis:6-bullseye
ports: ports:
- 6379:6379 - 6379:6379
defaults: defaults:
@ -87,7 +88,9 @@ jobs:
env: env:
ENV: development ENV: development
PORT: 5000 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 REDIS_URL: redis://@redis:6379
- name: Initialize CodeQL - name: Initialize CodeQL
@ -100,4 +103,4 @@ jobs:
- uses: codecov/codecov-action@v2 - uses: codecov/codecov-action@v2
with: with:
flags: api flags: api

219
.gitignore vendored Normal file
View File

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

View File

@ -1,12 +1,21 @@
FROM golang:1.17-buster FROM golang:1.19.0-bullseye AS builder
WORKDIR /app WORKDIR /app
COPY . . COPY . .
RUN go mod download RUN go build -o main .
RUN go build -v main.go
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} EXPOSE ${PORT}
CMD ["./main"] ENTRYPOINT [ "/app/main" ]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,185 +2,126 @@ package joke
import ( import (
"context" "context"
"encoding/hex"
"errors" "errors"
"jokes-bapak2-api/core/schema" "fmt"
"io"
"log"
"math/rand" "math/rand"
"strconv" "strconv"
"time"
"github.com/Masterminds/squirrel"
"github.com/allegro/bigcache/v3" "github.com/allegro/bigcache/v3"
"github.com/georgysavva/scany/pgxscan" "github.com/go-redis/redis/v8"
"github.com/jackc/pgx" "github.com/minio/minio-go/v7"
"github.com/jackc/pgx/v4/pgxpool"
"github.com/pquerna/ffjson/ffjson"
) )
// GetAllJSONJokes fetch the database for all the jokes then output it as a JSON []byte. // GetRandomJoke will acquire a random joke from the bucket.
// Keep in mind, you will need to store it to memory yourself. func GetRandomJoke(ctx context.Context, bucket *minio.Client, cache *redis.Client, memory *bigcache.BigCache) (image []byte, contentType string, err error) {
func GetAllJSONJokes(db *pgxpool.Pool, ctx context.Context) ([]byte, error) { totalJokes, err := GetTotalJoke(ctx, bucket, cache, memory)
conn, err := db.Acquire(ctx)
if err != nil { if err != nil {
return []byte{}, err return []byte{}, "", fmt.Errorf("getting total joke: %w", 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
} }
data, err := ffjson.Marshal(jokes) randomIndex := rand.Intn(totalJokes - 1)
joke, contentType, err := GetJokeByID(ctx, bucket, cache, memory, randomIndex)
if err != nil { 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 // GetJokeByID wil acquire a joke by its' ID.
func GetRandomJokeFromDB(db *pgxpool.Pool, ctx context.Context) (string, error) { //
conn, err := db.Acquire(ctx) // An ID is defined as the index on the joke list that is sorted
if err != nil { // by it's creation (or modification) time.
return "", err 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))
defer conn.Release() if err != nil && !errors.Is(err, bigcache.ErrEntryNotFound) {
return []byte{}, "", fmt.Errorf("acquiring joke from memory: %w", err)
var link string
err = conn.QueryRow(ctx, "SELECT link FROM jokesbapak2 ORDER BY random() LIMIT 1").Scan(&link)
if err != nil {
return "", err
} }
return link, nil if err == nil {
} contentTypeFromMemory, err := memory.Get("id:" + strconv.Itoa(id) + ":content-type")
if err != nil && !errors.Is(err, bigcache.ErrEntryNotFound) {
// GetRandomJokeFromCache returns a link string of a random joke from cache. return []byte{}, "", fmt.Errorf("acquiring joke content type from memory: %w", err)
func GetRandomJokeFromCache(memory *bigcache.BigCache) (string, error) {
jokes, err := memory.Get("jokes")
if err != nil {
if errors.Is(err, bigcache.ErrEntryNotFound) {
return "", schema.ErrNotFound
} }
return "", err
return jokeFromMemory, string(contentTypeFromMemory), nil
} }
var data []schema.Joke jokeFromCache, err := cache.Get(ctx, "jokes:id:"+strconv.Itoa(id)).Result()
err = ffjson.Unmarshal(jokes, &data) if err != nil && !errors.Is(err, redis.Nil) {
if err != nil { return []byte{}, "", fmt.Errorf("acquiring joke from cache: %w", err)
return "", nil
} }
// Return an error if the database is empty if err == nil {
dataLength := len(data) // Get content type
if dataLength == 0 { contentTypeFromCache, err := cache.Get(ctx, "jokes:id:"+strconv.Itoa(id)+":content-type").Result()
return "", schema.ErrEmpty if err != nil && !errors.Is(err, redis.Nil) {
} return []byte{}, "", fmt.Errorf("acquiring content type from cache: %w", err)
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
} }
return false, err
}
return true, nil // Decode hex string to bytes
} imageBytes, err := hex.DecodeString(jokeFromCache)
if err != nil {
// CheckTotalJokesCache literally does what the name is for return []byte{}, "", fmt.Errorf("decoding hex string: %w", err)
func CheckTotalJokesCache(memory *bigcache.BigCache) (bool, error) {
_, err := memory.Get("total")
if err != nil {
if errors.Is(err, bigcache.ErrEntryNotFound) {
return false, nil
} }
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 jokes, err := ListJokesFromBucket(ctx, bucket, cache)
}
// 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")
if err != nil { if err != nil {
if errors.Is(err, bigcache.ErrEntryNotFound) { return []byte{}, "", fmt.Errorf("listing jokes: %w", err)
return "", schema.ErrNotFound }
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 image, err = io.ReadAll(object)
err = ffjson.Unmarshal(jokes, &data)
if err != nil { 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. defer func(id int, image []byte) {
for _, v := range data { ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
if v.ID == id { defer cancel()
return v.Link, nil
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 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())
// 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
} }
return 0, err }(id, image)
}
i, err := strconv.Atoi(string(total))
if err != nil {
return 0, err
}
return i, nil return image, jokes[id].ContentType, 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
} }

View File

@ -2,340 +2,70 @@ package joke_test
import ( import (
"context" "context"
"encoding/json"
"jokes-bapak2-api/core/joke" "jokes-bapak2-api/core/joke"
"jokes-bapak2-api/core/schema"
"testing" "testing"
"time" "time"
"github.com/jackc/pgx/v4"
) )
func TestGetAllJSONJokes(t *testing.T) { func TestGetRandomJoke(t *testing.T) {
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(30*time.Second)) ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
defer cancel() defer cancel()
defer Flush() image, contentType, err := joke.GetRandomJoke(ctx, bucket, cache, memory)
conn, err := db.Acquire(ctx)
if err != nil { 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
}
return nil
})
if err != nil {
t.Error("an error was thrown:", err)
} }
j, err := joke.GetAllJSONJokes(db, ctx) if contentType != "image/jpeg" {
if err != nil { t.Errorf("expecting contentType of 'image/jpeg', instead got %s", contentType)
t.Error("an error was thrown:", err)
} }
if string(j) == "" { if len(image) == 0 {
t.Error("j should not be empty") t.Error("empty image")
} }
} }
func TestGetRandomJokeFromDB(t *testing.T) { func TestGetJokeById(t *testing.T) {
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(30*time.Second)) ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
defer cancel() defer cancel()
defer Flush() image, contentType, err := joke.GetJokeByID(ctx, bucket, cache, memory, 0)
conn, err := db.Acquire(ctx)
if err != nil { 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 { if contentType != "image/jpeg" {
_, err := t.Exec( t.Errorf("expecting contentType of 'image/jpeg', instead got %s", contentType)
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 len(image) == 0 {
}) t.Error("empty image")
}
cachedImage, cachedContentType, err := joke.GetJokeByID(ctx, bucket, cache, memory, 0)
if err != nil { 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 { if err != nil {
t.Error("an error was thrown:", err) t.Errorf("unexpected error: %v", err)
} }
if j == "" { if cachedContentType2 != contentType {
t.Error("j should not be empty") t.Errorf("difference in contentType: original %s vs cached %s", contentType, cachedContentType2)
}
}
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 string(cachedImage2) != string(image) {
t.Errorf("difference in image bytes")
}
} }

View File

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

14
api/core/joke/joke.go Normal file
View File

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

156
api/core/joke/joke_test.go Normal file
View File

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

79
api/core/joke/list.go Normal file
View File

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

View File

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

View File

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

View File

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

95
api/core/joke/today.go Normal file
View File

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

View File

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

67
api/core/joke/total.go Normal file
View File

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

View File

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

28
api/core/joke/uploader.go Normal file
View File

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

View File

@ -1,7 +1,7 @@
package schema package schema
type Joke struct { type Joke struct {
ID int `json:"id" form:"id" db:"id"` ID int `json:"id"`
Link string `json:"link" form:"link" db:"link"` Link string `json:"link"`
Creator int `json:"creator" form:"creator" db:"creator"` Creator int `json:"creator"`
} }

View File

@ -1,12 +1,12 @@
package schema package schema
type Submission struct { type Submission struct {
ID int `json:"id,omitempty" db:"id"` ID int `json:"id,omitempty"`
Link string `json:"link" form:"link" db:"link"` Link string `json:"link"`
Image string `json:"image,omitempty" form:"image"` Image string `json:"image,omitempty"`
CreatedAt string `json:"created_at" db:"created_at"` CreatedAt string `json:"created_at"`
Author string `json:"author" form:"author" db:"author"` Author string `json:"author"`
Status int `json:"status" db:"status"` Status int `json:"status"`
} }
type SubmissionQuery struct { type SubmissionQuery struct {

View File

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

View File

@ -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 <example@test.com>",
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 <example@test.com>" {
t.Error("expected first arg to be Test <example@test.com>, got:", i[0].(string))
}
if i[1].(int) != 2 {
t.Error("expected second arg to be 1, got:", i[1].(int))
}
}

View File

@ -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 <test@example.com>", 0,
2, "https://via.placeholder.com/300/02f/fff.png", "2021-08-04T18:20:38Z", "Test <test@example.com>", 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
}

View File

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

View File

@ -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 <example@test.com>"}, "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)
}
}

View File

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

View File

@ -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 <Author")
if v != false {
t.Error("Expected false, got true")
}
v = validator.ValidateAuthor("Test <Author>")
if v != false {
t.Error("Expected false, got true")
}
}
func TestValidateAuthor_True(t *testing.T) {
v := validator.ValidateAuthor("Test Author <author@mail.com>")
if v != true {
t.Error("Expected true, got false")
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,37 +1,30 @@
module jokes-bapak2-api module jokes-bapak2-api
go 1.17 go 1.19
require ( require (
github.com/Masterminds/squirrel v1.5.1
github.com/aldy505/bob v0.0.4 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/allegro/bigcache/v3 v3.0.1
github.com/georgysavva/scany v0.2.9
github.com/getsentry/sentry-go v0.11.0 github.com/getsentry/sentry-go v0.11.0
github.com/go-redis/redis/v8 v8.11.4 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/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/pquerna/ffjson v0.0.0-20190930134022-aa0246cd15f7
github.com/stretchr/testify v1.7.0 // indirect golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect
golang.org/x/sys v0.0.0-20211108224332-cbcd623f202e // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
) )
require ( 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/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/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/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/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/pgconn v1.10.0 // indirect
github.com/jackc/pgio v1.0.0 // indirect github.com/jackc/pgio v1.0.0 // indirect
github.com/jackc/pgpassfile 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/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
github.com/jackc/pgtype v1.8.1 // indirect github.com/jackc/pgtype v1.8.1 // indirect
github.com/jackc/puddle v1.1.4 // 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/builder v0.0.0-20180802200727-47ae307949d0 // indirect
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // 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/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rs/xid v1.4.0 // indirect
github.com/stretchr/objx v0.3.0 // indirect github.com/sirupsen/logrus v1.9.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa // indirect
github.com/valyala/fasthttp v1.31.0 // indirect golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa // indirect
golang.org/x/text v0.3.7 // indirect golang.org/x/text v0.3.7 // indirect
gopkg.in/ini.v1 v1.66.6 // indirect
) )

View File

@ -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/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/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/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/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/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/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/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 h1:36lj6JUHxGp7yt672aKcC8gk6rXpIRO/aqclQ9aXDa8=
github.com/aldy505/bob v0.0.4/go.mod h1:uckrZqhg9zmbLA4MpKueIeQfrdriNqbmMalvf0+qPG4= 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 h1:Q4Xl3chywXuJNOw7NV+MeySd3zGQDj4KCpkCg0te8mc=
github.com/allegro/bigcache/v3 v3.0.1/go.mod h1:aPyh7jEvrog9zAwx5N7+JUQX5dZTSGpxF1LAR4dr35I= 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/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/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 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 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 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= 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/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/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= 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/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/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.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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/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/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-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 h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 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/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/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/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/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= 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 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI=
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= 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/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 h1:qro8uttJGvNAMr5CLcFI9CHR0aDzXl0Vs3Pmw/oTPg8=
github.com/getsentry/sentry-go v0.11.0/go.mod h1:KBQIxiZAetw62Cj8Ri964vAEWVdgfaUCn30Q3bCvANo= 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-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/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-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 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= 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= 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-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 h1:kHoYkfZP6+pe04aFTnhDH6GDROa5yJdHJVNxV3F46Tg=
github.com/go-redis/redis/v8 v8.11.4/go.mod h1:2Z2wHZXdQpCDXEGzqMockDpNyYvi2l4Pxt6RJr792+w= 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-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/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/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/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= 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 h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 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.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.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 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.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 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/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/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.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/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/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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 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/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/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 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/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/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/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 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.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 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= 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-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-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 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.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.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= 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/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 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 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 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.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-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/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.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.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 h1:7PQ/4gLoqnl87ZxL7xjO0DR5gYuviDCZxQJsUlFW1eI=
github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 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 h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg=
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= 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-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-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= 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-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM=
github.com/jackc/pgtype v1.8.1 h1:9k0IXtdJXHJbyAWQgbWr1lU+MEhPXZz6RIXxfR5oxXs= 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/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-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-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.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.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 h1:JCjhT5vmhMAf/YwBHLvrBn4OGdIQBiFG6ym8Zmdx570=
github.com/jackc/pgx/v4 v4.13.0/go.mod h1:9P4X524sErlaxj0XSGZk7s+LD0eOyu1ZDUrrpznYDF0= 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-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 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.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.4 h1:5Ey/o5IfV7dYX6Znivq+N9MdK1S18OJI5OJq6EAAADw= 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/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.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.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/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/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= 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/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.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.9.7/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.15.9 h1:wKRjX6JRtDdrE9qwa4b/Cip7ACOshUI4smpCQanqjSY=
github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= 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.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/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.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.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= 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.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/echo/v4 v4.1.11/go.mod h1:i541M3Fj6f76NZtHSj7TXnyM8n2gaodfvfxNnFqi74g=
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= 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= 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/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.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.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.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 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 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.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.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 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/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
github.com/mediocregopher/radix/v3 v3.4.2/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8= 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/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/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 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-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/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 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.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/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/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= 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/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 h1:xoIK0ctDddBMnc74udxJYBqlo9Ylnsp1waqjLsnef20=
github.com/pquerna/ffjson v0.0.0-20190930134022-aa0246cd15f7/go.mod h1:YARuvh7BUWHNhzDq2OM5tzR2RiCcN2D7sapiKyCel/M= 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/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.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.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= 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= 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/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/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-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 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= 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/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.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 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/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/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 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.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.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 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.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 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 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/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/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/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.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/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 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/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/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= 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= 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-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-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-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-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-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-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-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-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-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-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-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-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-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 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/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.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.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 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-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-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-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-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-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-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-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-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= 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-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-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-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-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0=
golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/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-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-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-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-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-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/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-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-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-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-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-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-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211108224332-cbcd623f202e/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-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 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= 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-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 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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-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-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 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= 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 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-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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 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/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/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/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.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/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 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 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.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.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.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=

View File

@ -1,40 +1,47 @@
package health package health
import ( import (
"context"
"net/http"
"time"
"github.com/go-redis/redis/v8" "github.com/go-redis/redis/v8"
"github.com/gofiber/fiber/v2" "github.com/minio/minio-go/v7"
"github.com/jackc/pgx/v4/pgxpool"
) )
// Dependencies provides a struct for dependency injection
// on health package
type Dependencies struct { type Dependencies struct {
DB *pgxpool.Pool Bucket *minio.Client
Redis *redis.Client Cache *redis.Client
} }
func (d *Dependencies) Health(c *fiber.Ctx) error { // Health provides a http handler for healthcheck
conn, err := d.DB.Acquire(c.Context()) func (d *Dependencies) Health(w http.ResponseWriter, r *http.Request) {
if err != nil { ctx, cancel := context.WithTimeout(r.Context(), time.Second*15)
return err defer cancel()
}
defer conn.Release()
// Ping REDIS database var bucketOk = true
err = d.Redis.Ping(c.Context()).Err() var cacheOk = true
cancel, err := d.Bucket.HealthCheck(time.Second * 15)
if err != nil { if err != nil {
return c. bucketOk = false
Status(fiber.StatusServiceUnavailable).
JSON(Error{
Error: "REDIS: " + err.Error(),
})
} }
_, err = conn.Query(c.Context(), "SELECT \"id\" FROM \"jokesbapak2\" LIMIT 1") if cancel != nil {
if err != nil { cancel()
return c.
Status(fiber.StatusServiceUnavailable).
JSON(Error{
Error: "POSTGRESQL: " + err.Error(),
})
} }
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)
} }

View File

@ -1,5 +0,0 @@
package health
type Error struct {
Error string `json:"error"`
}

View File

@ -1,17 +1,15 @@
package joke package joke
import ( import (
"github.com/Masterminds/squirrel"
"github.com/allegro/bigcache/v3" "github.com/allegro/bigcache/v3"
"github.com/go-redis/redis/v8" "github.com/go-redis/redis/v8"
"github.com/gojek/heimdall/v7/httpclient" "github.com/minio/minio-go/v7"
"github.com/jackc/pgx/v4/pgxpool"
) )
// Dependencies provides a struct for dependency injection
// on joke package
type Dependencies struct { type Dependencies struct {
DB *pgxpool.Pool
Redis *redis.Client Redis *redis.Client
Memory *bigcache.BigCache Memory *bigcache.BigCache
HTTP *httpclient.Client Bucket *minio.Client
Query squirrel.StatementBuilderType
} }

66
api/handler/joke/get.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

22
api/handler/joke/total.go Normal file
View File

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

View File

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

View File

@ -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 <youremail@mail>\" 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 <youremail@mail>\" 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),
})
}

View File

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

View File

@ -1,55 +1,72 @@
package main package main
import ( import (
"errors"
"log" "log"
"net/http"
"os" "os"
"os/signal" "os/signal"
"context" "context"
"jokes-bapak2-api/core/joke" "jokes-bapak2-api/core/joke"
"jokes-bapak2-api/platform/database"
"jokes-bapak2-api/routes" "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" "time"
"github.com/gofiber/fiber/v2"
_ "github.com/joho/godotenv/autoload"
"github.com/Masterminds/squirrel"
"github.com/allegro/bigcache/v3" "github.com/allegro/bigcache/v3"
"github.com/getsentry/sentry-go" "github.com/getsentry/sentry-go"
"github.com/go-redis/redis/v8" "github.com/go-chi/chi/v5"
"github.com/gofiber/fiber/v2/middleware/cors"
"github.com/gofiber/fiber/v2/middleware/etag"
"github.com/gofiber/fiber/v2/middleware/limiter"
"github.com/gojek/heimdall/v7/httpclient"
"github.com/jackc/pgx/v4/pgxpool"
) )
func main() { func main() {
// Setup PostgreSQL redisURL, ok := os.LookupEnv("REDIS_URL")
poolConfig, err := pgxpool.ParseConfig(os.Getenv("DATABASE_URL")) if !ok {
if err != nil { redisURL = "redis://@localhost:6379"
log.Panicln("Unable to create pool config", err)
} }
poolConfig.MaxConnIdleTime = time.Minute * 3
poolConfig.MaxConnLifetime = time.Minute * 5
poolConfig.MaxConns = 15
poolConfig.MinConns = 4
db, err := pgxpool.ConnectConfig(context.Background(), poolConfig) minioHost, ok := os.LookupEnv("MINIO_HOST")
if err != nil { if !ok {
log.Panicln("Unable to create connection", err) minioHost = "localhost:9000"
} }
defer db.Close()
// Setup Redis minioID, ok := os.LookupEnv("MINIO_ACCESS_ID")
opt, err := redis.ParseURL(os.Getenv("REDIS_URL")) if !ok {
if err != nil { minioID = "minio"
log.Fatalln(err) }
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 // Setup In Memory
memory, err := bigcache.NewBigCache(bigcache.DefaultConfig(6 * time.Hour)) memory, err := bigcache.NewBigCache(bigcache.DefaultConfig(6 * time.Hour))
@ -58,122 +75,87 @@ func main() {
} }
defer memory.Close() 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 // Setup Sentry
err = sentry.Init(sentry.ClientOptions{ err = sentry.Init(sentry.ClientOptions{
Dsn: os.Getenv("SENTRY_DSN"), Dsn: sentryDsn,
Environment: os.Getenv("ENV"), Environment: environment,
AttachStacktrace: true, AttachStacktrace: true,
// Enable printing of SDK debug messages. // Enable printing of SDK debug messages.
// Useful when getting started or trying to figure something out. // Useful when getting started or trying to figure something out.
Debug: true, Debug: environment != "production",
}) })
if err != nil { if err != nil {
log.Panicln(err) log.Fatalf("setting up sentry: %s", err.Error())
return
} }
defer sentry.Flush(2 * time.Second) defer sentry.Flush(2 * time.Second)
setupCtx, setupCancel := context.WithDeadline(context.Background(), time.Now().Add(time.Minute*4)) setupCtx, setupCancel := context.WithDeadline(context.Background(), time.Now().Add(time.Minute*4))
defer setupCancel() defer setupCancel()
err = database.Populate(db, setupCtx) _, _, err = joke.GetTodaysJoke(setupCtx, minioClient, redisClient, memory)
if err != nil { if err != nil {
sentry.CaptureException(err) log.Fatalf("getting initial joke data: %s", err.Error())
log.Panicln(err) return
} }
err = joke.SetAllJSONJoke(db, setupCtx, memory) healthRouter := routes.Health(minioClient, redisClient)
if err != nil { jokeRouter := routes.Joke(minioClient, redisClient, memory)
log.Panicln(err)
} router := chi.NewRouter()
err = joke.SetTotalJoke(db, setupCtx, memory)
if err != nil { router.Mount("/health", healthRouter)
log.Panicln(err) 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 exitSignal := make(chan os.Signal, 1)
signal.Notify(exitSignal, os.Interrupt)
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{})
go func() { go func() {
sigint := make(chan os.Signal, 1) err := server.ListenAndServe()
signal.Notify(sigint, os.Interrupt) // Catch OS signals. if err != nil && !errors.Is(err, http.ErrServerClosed) {
<-sigint log.Fatalf("listening http server: %v", err)
// Received an interrupt signal, shutdown.
if err := a.Shutdown(); err != nil {
// Error from closing listeners, or context timeout:
log.Printf("Oops... Server is not shutting down! Reason: %v", err)
} }
close(idleConnsClosed)
}() }()
// Run server. <-exitSignal
if err := a.Listen(os.Getenv("HOST") + ":" + os.Getenv("PORT")); err != nil {
log.Printf("Oops... Server is not running! Reason: %v", err)
}
<-idleConnsClosed shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), time.Second*30)
} defer shutdownCancel()
// StartServer func for starting a simple server. err = server.Shutdown(shutdownCtx)
func StartServer(a *fiber.App) { if err != nil {
// Run server. log.Printf("shutting down http server: %v", err)
if err := a.Listen(os.Getenv("HOST") + ":" + os.Getenv("PORT")); err != nil {
log.Printf("Oops... Server is not running! Reason: %v", err)
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -2,18 +2,22 @@ package routes
import ( import (
"jokes-bapak2-api/handler/health" "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 provides route for healthcheck endpoints.
// Health check func Health(bucket *minio.Client, cache *redis.Client) *chi.Mux {
deps := health.Dependencies{ dependency := &health.Dependencies{
DB: d.DB, Bucket: bucket,
Redis: d.Redis, Cache: cache,
} }
d.App.Get("/health", cache.New(cache.Config{Expiration: 30 * time.Minute}), deps.Health) router := chi.NewRouter()
d.App.Get("/v1/health", cache.New(cache.Config{Expiration: 30 * time.Minute}), deps.Health)
router.Get("/", dependency.Health)
return router
} }

View File

@ -2,42 +2,34 @@ package routes
import ( import (
"jokes-bapak2-api/handler/joke" "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() { // Joke provides route for jokes.
deps := joke.Dependencies{ func Joke(bucket *minio.Client, cache *redis.Client, memory *bigcache.BigCache) *chi.Mux {
DB: d.DB, deps := &joke.Dependencies{
Redis: d.Redis, Memory: memory,
Memory: d.Memory, Bucket: bucket,
HTTP: d.HTTP, Redis: cache,
Query: d.Query,
} }
router := chi.NewRouter()
// Single route // Single route
d.App.Get("/", deps.SingleJoke) router.Get("/", deps.SingleJoke)
d.App.Get("/v1", deps.SingleJoke)
// Today's joke // Today's joke
d.App.Get("/today", cache.New(cache.Config{Expiration: 6 * time.Hour}), deps.TodayJoke) router.Get("/today", deps.TodayJoke)
d.App.Get("/v1/today", cache.New(cache.Config{Expiration: 6 * time.Hour}), deps.TodayJoke)
// Joke by ID // Joke by ID
d.App.Get("/id/:id", middleware.OnlyIntegerAsID(), deps.JokeByID) router.Get("/id/{id}", deps.JokeByID)
d.App.Get("/v1/id/:id", middleware.OnlyIntegerAsID(), deps.JokeByID)
// Count total jokes // Count total jokes
d.App.Get("/total", cache.New(cache.Config{Expiration: 15 * time.Minute}), deps.TotalJokes) router.Get("/total", deps.TotalJokes)
d.App.Get("/v1/total", cache.New(cache.Config{Expiration: 15 * time.Minute}), deps.TotalJokes)
// Add new joke return router
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)
} }

View File

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

BIN
api/samples/sample1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

BIN
api/samples/sample2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

BIN
api/samples/sample3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

BIN
api/samples/sample4.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

BIN
api/samples/sample5.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

View File

@ -1,4 +1,4 @@
FROM postgres:13.3-alpine FROM postgres:14.5-alpine
WORKDIR /var/lib/postgresql WORKDIR /var/lib/postgresql

View File

@ -27,10 +27,8 @@ services:
db: db:
build: ./database/postgres/ build: ./database/postgres/
command: > command: >
-c ssl=on -c ssl=on -c ssl_cert_file=/var/lib/postgresql/server.crt -c ssl_key_file=/var/lib/postgresql/server.key
-c ssl_cert_file=/var/lib/postgresql/server.crt restart: unless-stopped
-c ssl_key_file=/var/lib/postgresql/server.key
restart: always
ports: ports:
- 5432:5432 - 5432:5432
environment: environment:
@ -40,18 +38,51 @@ services:
PGDATA: /data/postgres PGDATA: /data/postgres
# I got this key from somewhere. It works when you run it locally. # I got this key from somewhere. It works when you run it locally.
POSTGRES_SSL_CA_CERT: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURjekNDQWx1Z0F3SUJBZ0lVR3lDaElvR3g0 POSTGRES_SSL_CA_CERT: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURjekNDQWx1Z0F3SUJBZ0lVR3lDaElvR3g0
healthcheck:
test: pg_isready -U postgres
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
volumes: volumes:
- ./database/postgres/data:/data/postgres - ./database/postgres/data:/data/postgres
cache: cache:
image: redis:6.2.4-alpine image: redis:6.2.4-alpine
restart: always restart: unless-stopped
ports: ports:
- 6379:6379 - 6379:6379
healthcheck:
test: redis-cli -a foobared ping | grep PONG
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
volumes: volumes:
- ./database/redis/etc:/usr/local/etc/redis - ./database/redis/etc:/usr/local/etc/redis
- ./database/redis/data:/data - ./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: cache-admin:
image: rediscommander/redis-commander:latest image: rediscommander/redis-commander:latest
restart: always restart: always
@ -73,4 +104,4 @@ services:
environment: environment:
DATABASE_URL: postgres://postgres:password@db/jokesbapak2 DATABASE_URL: postgres://postgres:password@db/jokesbapak2
depends_on: depends_on:
- db - db