mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f11caac3ff | |||
| 6c387b420c | |||
| ae84c9d8cc | |||
| 6bddbbf9d9 | |||
| d04b9278d2 | |||
| 1684d69fae | |||
| 3376f538fe | |||
| d6c9747c54 | |||
| e4646faf30 | |||
| 2d49ffe4cd | |||
| e8905be856 | |||
| fac5f382ec | |||
| dbc1f79a36 | |||
| 1053b779e4 | |||
| 94a6d41a61 | |||
| 9444ad56dc | |||
| c136206f2d | |||
| 4d26e6b7b4 | |||
| 10506238ae | |||
| c43544e5e8 |
@@ -0,0 +1,13 @@
|
||||
# .air.toml
|
||||
root = "."
|
||||
tmp_dir = "tmp"
|
||||
|
||||
[build]
|
||||
cmd = "go build -o ./tmp/main ./cmd/api"
|
||||
bin = "tmp/main"
|
||||
full_bin = "APP_ENV=dev ./tmp/main"
|
||||
include_ext = ["go", "tpl", "tmpl", "html"]
|
||||
exclude_dir = ["vendor", "tmp"]
|
||||
|
||||
[log]
|
||||
time = true
|
||||
@@ -0,0 +1,40 @@
|
||||
# Git & CI
|
||||
.git
|
||||
.gitignore
|
||||
.github
|
||||
|
||||
# Build artifacts
|
||||
/tmp
|
||||
/bin
|
||||
/out
|
||||
*.exe
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
*.test
|
||||
*.out
|
||||
|
||||
# Go specific
|
||||
*.exe
|
||||
*.test
|
||||
*.prof
|
||||
*.log
|
||||
|
||||
# Dependencies cache (biar tidak kebawa)
|
||||
vendor/
|
||||
go-build-cache/
|
||||
go-mod-cache/
|
||||
|
||||
# Editor/IDE
|
||||
.vscode
|
||||
.idea
|
||||
*.swp
|
||||
|
||||
# Env & secrets
|
||||
.env
|
||||
.env.*
|
||||
!/.env.example
|
||||
|
||||
# Docker sendiri
|
||||
Dockerfile*
|
||||
docker-compose*.yml
|
||||
@@ -0,0 +1,54 @@
|
||||
# server configuration
|
||||
# Env value : prod || dev
|
||||
VERSION=0.0.1
|
||||
APP_ENV=dev
|
||||
APP_HOST=0.0.0.0
|
||||
APP_PORT=8081
|
||||
APP_URL=http://localhost:8081
|
||||
|
||||
# database configuration
|
||||
DB_HOST=postgresdb
|
||||
DB_USER=postgres
|
||||
DB_PASSWORD=changeme
|
||||
DB_NAME=db_lti_erp
|
||||
DB_PORT=5432
|
||||
DB_PORT_HOST=5542
|
||||
|
||||
# JWT
|
||||
JWT_SECRET=changeme
|
||||
JWT_ACCESS_EXP_MINUTES=30
|
||||
JWT_REFRESH_EXP_DAYS=30
|
||||
JWT_RESET_PASSWORD_EXP_MINUTES=10
|
||||
JWT_VERIFY_EMAIL_EXP_MINUTES=10
|
||||
|
||||
# CORS
|
||||
CORS_ALLOW_ORIGINS=changeme
|
||||
CORS_ALLOW_METHODS=GET,POST,PUT,PATCH,DELETE,OPTIONS
|
||||
CORS_ALLOW_HEADERS=Authorization,Content-Type,X-Requested-With
|
||||
CORS_EXPOSE_HEADERS=Link,Location
|
||||
CORS_ALLOW_CREDENTIALS=true
|
||||
CORS_MAX_AGE=600
|
||||
|
||||
# Redis
|
||||
REDIS_URL=redis://redis:6379/0
|
||||
REDIS_PORT_HOST=6381
|
||||
|
||||
# SSO Integration
|
||||
SSO_ISSUER=http://localhost:8080/api
|
||||
SSO_JWKS_URL=http://localhost:8080/api/.well-known/jwks.json
|
||||
SSO_ALLOWED_AUDIENCES=client:lti-api
|
||||
SSO_AUTHORIZE_URL=http://localhost:8080/sso/authorize
|
||||
SSO_TOKEN_URL=http://localhost:8080/sso/token
|
||||
SSO_GETME_URL=http://localhost:8080/api/auth/get-me
|
||||
SSO_ACCESS_COOKIE_NAME=sso_access
|
||||
SSO_REFRESH_COOKIE_NAME=sso_refresh
|
||||
SSO_COOKIE_DOMAIN=
|
||||
SSO_COOKIE_SECURE=false
|
||||
SSO_COOKIE_SAMESITE=Lax
|
||||
SSO_PKCE_TTL_SECONDS=300
|
||||
# Security window and payload limits for SSO user sync webhook
|
||||
SSO_USER_SYNC_SIGNATURE_DRIFT_SECONDS=120
|
||||
SSO_USER_SYNC_NONCE_TTL_SECONDS=600
|
||||
SSO_USER_SYNC_MAX_BODY_BYTES=32768
|
||||
# Example JSON (single-line) of client configs (each client requires a unique sync_secret)
|
||||
SSO_CLIENTS={"lti":{"public_id":"client:lti","redirect_uri":"http://localhost:8081/api/sso/callback","scope":"openid profile","default_return_uri":"http://localhost:3000","allowed_return_origins":["http://localhost:3000"],"sync_secret":"changeme"}}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
# Environment variables
|
||||
.env
|
||||
|
||||
# Air temp dir
|
||||
tmp/
|
||||
|
||||
# Binaries
|
||||
main
|
||||
bin/
|
||||
*.exe
|
||||
*.out
|
||||
|
||||
# Go build cache
|
||||
.gocache/
|
||||
|
||||
# Logs & reports
|
||||
*.log
|
||||
*.txt
|
||||
coverage/
|
||||
|
||||
# IDE / editor files
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
@@ -0,0 +1,76 @@
|
||||
stages: [notify]
|
||||
|
||||
# --- Notify when MR is opened/updated ---
|
||||
notify_discord_mr:
|
||||
stage: notify
|
||||
image: alpine:3.20
|
||||
rules:
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||
variables:
|
||||
WEBHOOK_URL: $DISCORD_WEBHOOK_URL
|
||||
before_script:
|
||||
- apk add --no-cache curl jq
|
||||
script: |
|
||||
MR_URL="${CI_PROJECT_URL}/-/merge_requests/${CI_MERGE_REQUEST_IID}"
|
||||
|
||||
jq -n \
|
||||
--arg repo "$CI_PROJECT_PATH" \
|
||||
--arg mr "#${CI_MERGE_REQUEST_IID}" \
|
||||
--arg url "$MR_URL" \
|
||||
--arg requestor "${GITLAB_USER_LOGIN:-$GITLAB_USER_NAME}" \
|
||||
--arg source "$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME" \
|
||||
--arg target "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" \
|
||||
--arg title "$CI_MERGE_REQUEST_TITLE" \
|
||||
'{
|
||||
username: "CI Bot - BE",
|
||||
embeds: [{
|
||||
title: "📣 [LTI API] Merge Request Opened/Updated",
|
||||
description: ($mr + " in " + $repo),
|
||||
url: $url,
|
||||
color: 3447003,
|
||||
fields: [
|
||||
{name: "Author", value: $requestor, inline: true},
|
||||
{name: "Source → Target", value: ($source + " → " + $target), inline: true},
|
||||
{name: "Title", value: $title}
|
||||
]
|
||||
}]
|
||||
}' \
|
||||
| curl -sS -H "Content-Type: application/json" -d @- "$WEBHOOK_URL"
|
||||
|
||||
# --- Notify when MR is merged ---
|
||||
notify_discord_merge:
|
||||
stage: notify
|
||||
image: alpine:3.20
|
||||
rules:
|
||||
# Only run for merge request pipelines that are in merged state
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_STATE == "merged"'
|
||||
variables:
|
||||
WEBHOOK_URL: $DISCORD_WEBHOOK_URL
|
||||
before_script:
|
||||
- apk add --no-cache curl jq
|
||||
script: |
|
||||
MR_URL="${CI_PROJECT_URL}/-/merge_requests/${CI_MERGE_REQUEST_IID}"
|
||||
|
||||
jq -n \
|
||||
--arg repo "$CI_PROJECT_PATH" \
|
||||
--arg mr "#${CI_MERGE_REQUEST_IID}" \
|
||||
--arg url "$MR_URL" \
|
||||
--arg requestor "${CI_MERGE_REQUEST_AUTHOR}" \
|
||||
--arg source "$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME" \
|
||||
--arg target "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" \
|
||||
--arg title "$CI_MERGE_REQUEST_TITLE" \
|
||||
'{
|
||||
username: "CI Bot - BE",
|
||||
embeds: [{
|
||||
title: "✅ [LTI API] Merge Request Merged",
|
||||
description: ($mr + " has been merged into " + $repo),
|
||||
url: $url,
|
||||
color: 3066993,
|
||||
fields: [
|
||||
{name: "Author", value: $requestor, inline: true},
|
||||
{name: "Source → Target", value: ($source + " → " + $target), inline: true},
|
||||
{name: "Title", value: $title}
|
||||
]
|
||||
}]
|
||||
}' \
|
||||
| curl -sS -H "Content-Type: application/json" -d @- "$WEBHOOK_URL"
|
||||
+203
@@ -0,0 +1,203 @@
|
||||
## Config for golangci-lint v2 schema
|
||||
version: "2"
|
||||
|
||||
run:
|
||||
timeout: 3m
|
||||
|
||||
linters:
|
||||
default: none
|
||||
enable:
|
||||
## enabled by default
|
||||
- errcheck
|
||||
- govet
|
||||
- ineffassign
|
||||
- staticcheck
|
||||
- unused
|
||||
## disabled by default
|
||||
- asasalint
|
||||
- asciicheck
|
||||
- bidichk
|
||||
- bodyclose
|
||||
- canonicalheader
|
||||
- cyclop
|
||||
- dupl
|
||||
- durationcheck
|
||||
- errname
|
||||
- errorlint
|
||||
- exhaustive
|
||||
- fatcontext
|
||||
- forbidigo
|
||||
- funlen
|
||||
- gocheckcompilerdirectives
|
||||
#- gochecknoglobals
|
||||
#- gochecknoinits
|
||||
- gochecksumtype
|
||||
- gocognit
|
||||
- goconst
|
||||
- gocritic
|
||||
- gocyclo
|
||||
#- godot
|
||||
- gomoddirectives
|
||||
- gomodguard
|
||||
- goprintffuncname
|
||||
- gosec
|
||||
- intrange
|
||||
- lll
|
||||
- loggercheck
|
||||
- makezero
|
||||
- mirror
|
||||
#- mnd
|
||||
- musttag
|
||||
- nakedret
|
||||
- nestif
|
||||
- nilerr
|
||||
- nilnil
|
||||
- noctx
|
||||
- nolintlint
|
||||
- nonamedreturns
|
||||
- nosprintfhostport
|
||||
- perfsprint
|
||||
- predeclared
|
||||
- promlinter
|
||||
- protogetter
|
||||
- reassign
|
||||
- revive
|
||||
- rowserrcheck
|
||||
- sloglint
|
||||
- spancheck
|
||||
- sqlclosecheck
|
||||
- testableexamples
|
||||
#- testifylint
|
||||
- testpackage
|
||||
- tparallel
|
||||
- unconvert
|
||||
- unparam
|
||||
- usestdlibvars
|
||||
- usetesting
|
||||
- wastedassign
|
||||
- whitespace
|
||||
settings:
|
||||
cyclop:
|
||||
max-complexity: 30
|
||||
package-average: 10.0
|
||||
errcheck:
|
||||
check-type-assertions: true
|
||||
exhaustive:
|
||||
check:
|
||||
- switch
|
||||
- map
|
||||
exhaustruct:
|
||||
exclude:
|
||||
- "^net/http.Client$"
|
||||
- "^net/http.Cookie$"
|
||||
- "^net/http.Request$"
|
||||
- "^net/http.Response$"
|
||||
- "^net/http.Server$"
|
||||
- "^net/http.Transport$"
|
||||
- "^net/url.URL$"
|
||||
- "^os/exec.Cmd$"
|
||||
- "^reflect.StructField$"
|
||||
- "^github.com/Shopify/sarama.Config$"
|
||||
- "^github.com/Shopify/sarama.ProducerMessage$"
|
||||
- "^github.com/mitchellh/mapstructure.DecoderConfig$"
|
||||
- "^github.com/prometheus/client_golang/.+Opts$"
|
||||
- "^github.com/spf13/cobra.Command$"
|
||||
- "^github.com/spf13/cobra.CompletionOptions$"
|
||||
- "^github.com/stretchr/testify/mock.Mock$"
|
||||
- "^github.com/testcontainers/testcontainers-go.+Request$"
|
||||
- "^github.com/testcontainers/testcontainers-go.FromDockerfile$"
|
||||
- "^golang.org/x/tools/go/analysis.Analyzer$"
|
||||
- "^google.golang.org/protobuf/.+Options$"
|
||||
- "^gopkg.in/yaml.v3.Node$"
|
||||
funlen:
|
||||
lines: 100
|
||||
statements: 50
|
||||
ignore-comments: true
|
||||
gocognit:
|
||||
min-complexity: 20
|
||||
gocritic:
|
||||
settings:
|
||||
captLocal:
|
||||
paramsOnly: false
|
||||
underef:
|
||||
skipRecvDeref: false
|
||||
gomodguard:
|
||||
blocked:
|
||||
modules:
|
||||
- github.com/golang/protobuf:
|
||||
recommendations:
|
||||
- google.golang.org/protobuf
|
||||
reason: "see https://developers.google.com/protocol-buffers/docs/reference/go/faq#modules"
|
||||
- github.com/satori/go.uuid:
|
||||
recommendations:
|
||||
- github.com/google/uuid
|
||||
reason: "satori's package is not maintained"
|
||||
- github.com/gofrs/uuid:
|
||||
recommendations:
|
||||
- github.com/gofrs/uuid/v5
|
||||
reason: "gofrs' package was not go module before v5"
|
||||
govet:
|
||||
enable-all: true
|
||||
disable:
|
||||
- fieldalignment
|
||||
settings:
|
||||
shadow:
|
||||
strict: true
|
||||
inamedparam:
|
||||
skip-single-param: true
|
||||
mnd:
|
||||
ignored-functions:
|
||||
- args.Error
|
||||
- flag.Arg
|
||||
- flag.Duration.*
|
||||
- flag.Float.*
|
||||
- flag.Int.*
|
||||
- flag.Uint.*
|
||||
- os.Chmod
|
||||
- os.Mkdir.*
|
||||
- os.OpenFile
|
||||
- os.WriteFile
|
||||
- prometheus.ExponentialBuckets.*
|
||||
- prometheus.LinearBuckets
|
||||
nakedret:
|
||||
max-func-lines: 0
|
||||
nolintlint:
|
||||
allow-no-explanation:
|
||||
- funlen
|
||||
- gocognit
|
||||
- lll
|
||||
require-explanation: true
|
||||
require-specific: true
|
||||
perfsprint:
|
||||
strconcat: false
|
||||
rowserrcheck:
|
||||
packages:
|
||||
- github.com/jmoiron/sqlx
|
||||
sloglint:
|
||||
no-global: "all"
|
||||
context: "scope"
|
||||
exclusions:
|
||||
rules:
|
||||
- source: "(noinspection|TODO)"
|
||||
linters:
|
||||
- godot
|
||||
- source: "//noinspection"
|
||||
linters:
|
||||
- gocritic
|
||||
- path: "example\\.go"
|
||||
linters:
|
||||
- lll
|
||||
- path: "_test\\.go"
|
||||
linters:
|
||||
- bodyclose
|
||||
- dupl
|
||||
- funlen
|
||||
- goconst
|
||||
- gosec
|
||||
- noctx
|
||||
- wrapcheck
|
||||
- lll
|
||||
- testpackage
|
||||
|
||||
issues:
|
||||
max-same-issues: 50
|
||||
@@ -0,0 +1,20 @@
|
||||
FROM golang:1.23-alpine
|
||||
|
||||
# Install dependensi dasar
|
||||
RUN apk add --no-cache git curl bash build-base
|
||||
|
||||
# Install Air (pakai repo baru air-verse)
|
||||
RUN go install github.com/air-verse/air@v1.52.3
|
||||
|
||||
WORKDIR /lti-api
|
||||
|
||||
# Cache dependencies
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
EXPOSE 8081
|
||||
|
||||
CMD ["air", "-c", ".air.toml"]
|
||||
@@ -0,0 +1,120 @@
|
||||
# --- Load .env kalau ada, dan export ke shell child ---
|
||||
ifneq (,$(wildcard .env))
|
||||
include .env
|
||||
export
|
||||
endif
|
||||
|
||||
# --- Konfigurasi umum ---
|
||||
COMPOSE ?= docker compose -f docker-compose.local.yml
|
||||
NETWORK ?= lti-api_go-network
|
||||
MIGRATE_IMAGE ?= migrate/migrate
|
||||
MIGRATIONS_DIR := $(PWD)/internal/database/migrations
|
||||
|
||||
# Fallback agar tetap jalan meski .env kosong
|
||||
DB_HOST ?= postgresdb
|
||||
DB_PORT ?= 5432
|
||||
DB_USER ?= postgres
|
||||
DB_PASSWORD ?= postgres
|
||||
DB_NAME ?= db_lti_erp
|
||||
|
||||
DB_URL := postgres://$(DB_USER):$(DB_PASSWORD)@$(DB_HOST):$(DB_PORT)/$(DB_NAME)?sslmode=disable
|
||||
|
||||
# Tunggu DB ready memakai pg_isready dari image postgres
|
||||
WAIT_DB := docker run --rm --network $(NETWORK) postgres:alpine \
|
||||
sh -c 'until pg_isready -h $(DB_HOST) -p $(DB_PORT) -U $(DB_USER) -d $(DB_NAME); do echo "waiting for postgres..."; sleep 1; done'
|
||||
|
||||
# Default target
|
||||
.DEFAULT_GOAL := start
|
||||
|
||||
# --- Daftar phony targets ---
|
||||
.PHONY: start build test lint gen \
|
||||
db-up wait-db \
|
||||
migration-% migrate-up migrate-down migrate-fresh \
|
||||
seed \
|
||||
docker-local docker-down docker-nuke docker-cache psql
|
||||
|
||||
# --- Go workflow ---
|
||||
start:
|
||||
@go run cmd/api/main.go
|
||||
|
||||
build:
|
||||
@go build -o tmp/app ./cmd/api
|
||||
|
||||
test:
|
||||
@go test ./test/...
|
||||
|
||||
lint:
|
||||
@golangci-lint run
|
||||
|
||||
# --- Compose / DB helpers ---
|
||||
db-up:
|
||||
@$(COMPOSE) up -d postgresdb
|
||||
|
||||
wait-db:
|
||||
@$(WAIT_DB)
|
||||
|
||||
# --- Migration (pembuatan file) ---
|
||||
# Contoh: make migration-create_users_table
|
||||
# ":" akan diubah ke "_" (biar aman untuk nama file)
|
||||
migration-%:
|
||||
@migrate create -ext sql -dir internal/database/migrations $(subst :,_,$*)
|
||||
|
||||
# --- Migration (apply via docker image 'migrate') ---
|
||||
migrate-up: db-up wait-db
|
||||
@docker run --rm -v $(MIGRATIONS_DIR):/migrations --network $(NETWORK) \
|
||||
$(MIGRATE_IMAGE) -path=/migrations/ -database "$(DB_URL)" up
|
||||
|
||||
# Contoh:
|
||||
# make migrate-down step=2 → rollback 2 step
|
||||
# make migrate-down → rollback semua
|
||||
|
||||
migrate-down: db-up wait-db
|
||||
@if [ -n "$(step)" ]; then \
|
||||
echo "⬇️ Migrating down $(step) step(s)..."; \
|
||||
docker run --rm -v $(MIGRATIONS_DIR):/migrations --network $(NETWORK) \
|
||||
$(MIGRATE_IMAGE) -path=/migrations/ -database "$(DB_URL)" down $(step); \
|
||||
else \
|
||||
echo "⬇️ Migrating down ALL steps..."; \
|
||||
docker run --rm -v $(MIGRATIONS_DIR):/migrations --network $(NETWORK) \
|
||||
$(MIGRATE_IMAGE) -path=/migrations/ -database "$(DB_URL)" down -all; \
|
||||
fi
|
||||
|
||||
migrate-fresh: migrate-down migrate-up
|
||||
@true
|
||||
|
||||
# Pakai: make migrate-force v=20250917120000
|
||||
migrate-force:
|
||||
@docker run --rm -v $(MIGRATIONS_DIR):/migrations --network $(NETWORK) \
|
||||
$(MIGRATE_IMAGE) -path=/migrations/ -database "$(DB_URL)" force $(v)
|
||||
|
||||
|
||||
# --- Seeder ---
|
||||
seed: db-up wait-db
|
||||
@$(COMPOSE) run --rm app go run cmd/seed/main.go
|
||||
|
||||
# --- Docker orchestration convenience ---
|
||||
docker-local:
|
||||
@$(COMPOSE) up --build -d
|
||||
|
||||
docker-down:
|
||||
@$(COMPOSE) down --remove-orphans
|
||||
|
||||
# ⚠️ Akan menghapus container, images dan volumes.
|
||||
docker-nuke:
|
||||
@$(COMPOSE) down --rmi all --volumes --remove-orphans
|
||||
|
||||
docker-cache:
|
||||
@docker builder prune -f
|
||||
|
||||
# --- PSQL shell ke DB di container ---
|
||||
psql: db-up
|
||||
@$(COMPOSE) exec -it postgresdb psql -U $(DB_USER) -d $(DB_NAME)
|
||||
|
||||
# Single feature
|
||||
# example: make gen feat=product-category
|
||||
|
||||
# Sub feature
|
||||
# make gen feat=master/area
|
||||
gen:
|
||||
@go run tools/gen.go $(feat)
|
||||
# @goimports -w internal
|
||||
@@ -1,93 +1,113 @@
|
||||
# LTI API
|
||||
# Lumbung Telur Indonesia ERP API (lti-api)
|
||||
|
||||
RESTful API for **Lumbung Telur Indonesia ERP**, built with **Go, Fiber, GORM** and **PostgreSQL**.
|
||||
|
||||
---
|
||||
|
||||
## Getting started
|
||||
## 📦 Tech Stack
|
||||
|
||||
To make it easy for you to get started with GitLab, here's a list of recommended next steps.
|
||||
- **Go + Fiber** — Web framework
|
||||
- **GORM** — ORM for PostgreSQL
|
||||
- **PostgreSQL** — Relational database
|
||||
- **go-playground/validator** — Input validation
|
||||
- **JWT** — Authentication
|
||||
- **Logrus** — Logging
|
||||
- **Fiber middleware** — Rate limiting, CORS, recovery, logger
|
||||
- **Air** — Hot reload for development
|
||||
- **Docker + Docker Compose** — Containerization
|
||||
- **golang-migrate** — Database migration tool
|
||||
|
||||
Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)!
|
||||
---
|
||||
|
||||
## Add your files
|
||||
## 🚀 Getting Started
|
||||
|
||||
- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files
|
||||
- [ ] [Add files using the command line](https://docs.gitlab.com/topics/git/add_files/#add-files-to-a-git-repository) or push an existing Git repository with the following command:
|
||||
### 1. Clone Project
|
||||
|
||||
```
|
||||
cd existing_repo
|
||||
git remote add origin https://gitlab.com/mbugroup/lti-api.git
|
||||
git branch -M main
|
||||
git push -uf origin main
|
||||
```bash
|
||||
git clone https://gitlab.com/mbugroup/lti-api.git
|
||||
cd lti-api
|
||||
```
|
||||
|
||||
## Integrate with your tools
|
||||
### 2. Install Dependencies
|
||||
|
||||
- [ ] [Set up project integrations](https://gitlab.com/mbugroup/lti-api/-/settings/integrations)
|
||||
```bash
|
||||
go mod tidy
|
||||
```
|
||||
|
||||
## Collaborate with your team
|
||||
### 3. Configure Environment
|
||||
|
||||
- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/)
|
||||
- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html)
|
||||
- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically)
|
||||
- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/)
|
||||
- [ ] [Set auto-merge](https://docs.gitlab.com/user/project/merge_requests/auto_merge/)
|
||||
Copy .env.example to .env and adjust the variables (e.g. DATABASE_URL, JWT secrets, etc).
|
||||
|
||||
## Test and Deploy
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Use the built-in continuous integration in GitLab.
|
||||
### 5. Setup Docker
|
||||
|
||||
- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/)
|
||||
- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing (SAST)](https://docs.gitlab.com/ee/user/application_security/sast/)
|
||||
- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html)
|
||||
- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/)
|
||||
- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html)
|
||||
Run initial docker.
|
||||
|
||||
***
|
||||
```bash
|
||||
make docker-local
|
||||
```
|
||||
|
||||
# Editing this README
|
||||
### 4. Migrate Database
|
||||
|
||||
When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thanks to [makeareadme.com](https://www.makeareadme.com/) for this template.
|
||||
Run initial migrations and generate views.
|
||||
|
||||
## Suggestions for a good README
|
||||
```bash
|
||||
make migrate-up
|
||||
```
|
||||
|
||||
Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information.
|
||||
### 5. Run App
|
||||
|
||||
## Name
|
||||
Choose a self-explaining name for your project.
|
||||
Run project via Docker
|
||||
|
||||
## Description
|
||||
Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors.
|
||||
### 6. Create New Module
|
||||
|
||||
## Badges
|
||||
On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge.
|
||||
```bash
|
||||
make gen feat=user
|
||||
```
|
||||
|
||||
## Visuals
|
||||
Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method.
|
||||
output:
|
||||
|
||||
## Installation
|
||||
Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection.
|
||||
```bash
|
||||
cmd/
|
||||
├── api/
|
||||
│ └── main.go # Application entrypoint (initialize Fiber, load config, connect DB, register route)
|
||||
|
||||
## Usage
|
||||
Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README.
|
||||
internal/
|
||||
├── config/ # App config (env loader, logger, app settings)
|
||||
│
|
||||
├── database/ # Database connection + migration setup
|
||||
│
|
||||
├── middleware/ # Global Fiber middleware (auth, logger, recovery, rate limiting)
|
||||
│
|
||||
├── modules/ # Feature modules (users, products, suppliers, etc.)
|
||||
│ ├── <module>/
|
||||
│ │ ├── controllers/ # HTTP handler layer (receive request, call service, return response)
|
||||
│ │ ├── dto/ # Data Transfer Objects (request & response payloads, separate from models)
|
||||
│ │ ├── models/ # GORM models (represent database tables/entities)
|
||||
│ │ ├── repositories/ # Data access layer (queries to DB, CRUD abstraction)
|
||||
│ │ ├── services/ # Business logic layer (process rules, orchestrate repository calls)
|
||||
│ │ ├── validation/ # Request validation (custom rules per module)
|
||||
│ │ ├── module.go # Module bootstrapper (wire controller, service, repository together)
|
||||
│ │ └── route.go # Module route (register module routes into Fiber app)
|
||||
│
|
||||
├── repository/ # Shared repositories (reusable DB access layer across multiple modules)
|
||||
│
|
||||
├── response/ # Standardized API responses (success, error, pagination)
|
||||
│
|
||||
├── utils/ # Helper functions (JWT, hashing, constants, enums, etc.)
|
||||
│
|
||||
├── validation/ # Shared request validation structs & rules
|
||||
│
|
||||
└── route/ # Central route aggregator (load all module routes into main app)
|
||||
```
|
||||
|
||||
## Support
|
||||
Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.
|
||||
## ✨ Author
|
||||
|
||||
## Roadmap
|
||||
If you have ideas for releases in the future, it is a good idea to list them in the README.
|
||||
IT Development PT Mitra Berlian Unggas Group
|
||||
|
||||
## Contributing
|
||||
State if you are open to contributions and what your requirements are for accepting them.
|
||||
## 📃 License
|
||||
|
||||
For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self.
|
||||
|
||||
You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser.
|
||||
|
||||
## Authors and acknowledgment
|
||||
Show your appreciation to those who have contributed to the project.
|
||||
|
||||
## License
|
||||
For open source projects, say how it is licensed.
|
||||
|
||||
## Project status
|
||||
If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.
|
||||
This project is private. All rights reserved.
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "VERCEL_GIT_COMMIT_REF: $VERCEL_GIT_COMMIT_REF"
|
||||
|
||||
if [[ "$VERCEL_GIT_COMMIT_REF" == "master" || "$VERCEL_GIT_COMMIT_REF" == "development" ]]; then
|
||||
echo "✅ - Build can proceed"
|
||||
exit 1
|
||||
else
|
||||
echo "🛑 - Build cancelled"
|
||||
exit 0
|
||||
fi
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/cache"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/config"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/database"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/route"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/sso"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/compress"
|
||||
"github.com/gofiber/fiber/v2/middleware/cors"
|
||||
"github.com/gofiber/fiber/v2/middleware/helmet"
|
||||
"github.com/redis/go-redis/v9"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
app := setupFiberApp()
|
||||
db := setupDatabase()
|
||||
defer closeDatabase(db)
|
||||
rdb := setupRedis()
|
||||
defer rdb.Close()
|
||||
setupSSO(ctx)
|
||||
setupRoutes(app, db, rdb)
|
||||
|
||||
address := fmt.Sprintf("%s:%d", config.AppHost, config.AppPort)
|
||||
|
||||
// Start server and handle graceful shutdown
|
||||
serverErrors := make(chan error, 1)
|
||||
go startServer(app, address, serverErrors)
|
||||
handleGracefulShutdown(ctx, app, serverErrors)
|
||||
}
|
||||
|
||||
func setupRedis() *redis.Client {
|
||||
opt, err := redis.ParseURL(config.RedisURL)
|
||||
if err != nil {
|
||||
utils.Log.Fatalf("Redis URL parse error: %v", err)
|
||||
}
|
||||
rdb := redis.NewClient(opt)
|
||||
if err := rdb.Ping(context.Background()).Err(); err != nil {
|
||||
utils.Log.Fatalf("Redis ping failed: %v", err)
|
||||
}
|
||||
cache.SetRedis(rdb)
|
||||
utils.Log.Infof("Redis connected: %s", config.RedisURL)
|
||||
return rdb
|
||||
}
|
||||
|
||||
func setupSSO(ctx context.Context) {
|
||||
if err := sso.Init(ctx, config.SSOJWKSURL, config.SSOIssuer, config.SSOAllowedAudiences); err != nil {
|
||||
utils.Log.Fatalf("SSO initialization failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func setupFiberApp() *fiber.App {
|
||||
app := fiber.New(config.FiberConfig())
|
||||
|
||||
// Middleware setup
|
||||
app.Use(middleware.LoggerConfig())
|
||||
app.Use(helmet.New())
|
||||
app.Use(compress.New())
|
||||
app.Use(middleware.RecoverConfig())
|
||||
|
||||
origins := "*"
|
||||
if len(config.CORSAllowOrigins) > 0 {
|
||||
origins = strings.Join(config.CORSAllowOrigins, ",")
|
||||
}
|
||||
if config.CORSAllowCredentials && (origins == "" || origins == "*") {
|
||||
origins = "http://localhost:3000"
|
||||
}
|
||||
app.Use(cors.New(cors.Config{
|
||||
AllowOrigins: origins,
|
||||
AllowMethods: strings.Join(config.CORSAllowMethods, ","),
|
||||
AllowHeaders: strings.Join(config.CORSAllowHeaders, ","),
|
||||
ExposeHeaders: strings.Join(config.CORSExposeHeaders, ","),
|
||||
AllowCredentials: config.CORSAllowCredentials,
|
||||
MaxAge: config.CORSMaxAge,
|
||||
}))
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
func setupDatabase() *gorm.DB {
|
||||
db := database.Connect(config.DBHost, config.DBName)
|
||||
return db
|
||||
}
|
||||
|
||||
func setupRoutes(app *fiber.App, db *gorm.DB, rdb *redis.Client) {
|
||||
|
||||
// route.Routes(app, db)
|
||||
// app.Use(utils.NotFoundHandler)
|
||||
app.Get("/healthz", func(c *fiber.Ctx) error {
|
||||
return c.Status(fiber.StatusOK).JSON(fiber.Map{
|
||||
"status": "ok",
|
||||
"service": "api",
|
||||
"version": config.Version,
|
||||
})
|
||||
})
|
||||
|
||||
app.Get("/readyz", func(c *fiber.Ctx) error {
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||
"status": "error", "db": "unavailable", "redis": "unknown",
|
||||
})
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(c.Context(), 2*time.Second)
|
||||
defer cancel()
|
||||
|
||||
dbOK := sqlDB.PingContext(ctx) == nil
|
||||
redisOK := rdb.Ping(ctx).Err() == nil
|
||||
|
||||
status := fiber.StatusOK
|
||||
statusText := "ok"
|
||||
if !dbOK || !redisOK {
|
||||
status = fiber.StatusServiceUnavailable
|
||||
statusText = "degraded"
|
||||
}
|
||||
body := fiber.Map{
|
||||
"status": statusText,
|
||||
"db": map[bool]string{true: "up", false: "down"}[dbOK],
|
||||
"redis": map[bool]string{true: "up", false: "down"}[redisOK],
|
||||
}
|
||||
return c.Status(status).JSON(body)
|
||||
})
|
||||
|
||||
route.Routes(app, db)
|
||||
app.Use(utils.NotFoundHandler)
|
||||
}
|
||||
|
||||
func startServer(app *fiber.App, address string, errs chan<- error) {
|
||||
if err := app.Listen(address); err != nil {
|
||||
errs <- fmt.Errorf("error starting server: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
func closeDatabase(db *gorm.DB) {
|
||||
sqlDB, errDB := db.DB()
|
||||
if errDB != nil {
|
||||
utils.Log.Errorf("Error getting database instance: %v", errDB)
|
||||
return
|
||||
}
|
||||
|
||||
if err := sqlDB.Close(); err != nil {
|
||||
utils.Log.Errorf("Error closing database connection: %v", err)
|
||||
} else {
|
||||
utils.Log.Info("Database connection closed successfully")
|
||||
}
|
||||
}
|
||||
|
||||
func handleGracefulShutdown(ctx context.Context, app *fiber.App, serverErrors <-chan error) {
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
|
||||
|
||||
select {
|
||||
case err := <-serverErrors:
|
||||
utils.Log.Fatalf("Server error: %v", err)
|
||||
case <-quit:
|
||||
utils.Log.Info("Shutting down server...")
|
||||
if err := app.Shutdown(); err != nil {
|
||||
utils.Log.Fatalf("Error during server shutdown: %v", err)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
utils.Log.Info("Server exiting due to context cancellation")
|
||||
}
|
||||
|
||||
utils.Log.Info("Server exited")
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/config"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/database"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/database/seed"
|
||||
)
|
||||
|
||||
func main() {
|
||||
db := database.Connect(config.DBHost, config.DBName)
|
||||
|
||||
if err := seed.Run(db); err != nil {
|
||||
log.Fatalf("❌ Failed run seeder: %v", err)
|
||||
}
|
||||
|
||||
log.Println("✅ Seed Successfully")
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
services:
|
||||
postgresdb:
|
||||
image: postgres:alpine
|
||||
restart: always
|
||||
ports:
|
||||
- "${DB_PORT_HOST:-5542}:5432"
|
||||
environment:
|
||||
POSTGRES_USER: ${DB_USER:-postgres}
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres}
|
||||
POSTGRES_DB: ${DB_NAME:-db_lti_erp}
|
||||
volumes:
|
||||
- dbdata:/var/lib/postgresql/data
|
||||
- ./internal/database/init:/docker-entrypoint-initdb.d
|
||||
networks: [go-network]
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD-SHELL",
|
||||
"pg_isready -U ${DB_USER:-postgres} -d ${DB_NAME:-db_lti_erp}",
|
||||
]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${REDIS_PORT_HOST:-6381}:6379"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
networks: [go-network]
|
||||
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.local
|
||||
image: cosmtrek/air:v1.52.3
|
||||
working_dir: /lti-api
|
||||
volumes:
|
||||
- .:/lti-api
|
||||
- ./internal/config/jwtRS256.key:/run/keys/jwtRS256.key
|
||||
- ./internal/config/jwtRS256.key.pub:/run/keys/jwtRS256.key.pub
|
||||
command: air -c .air.toml
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
DB_HOST: postgresdb
|
||||
DB_PORT: 5432
|
||||
DB_USER: ${DB_USER:-postgres}
|
||||
DB_PASSWORD: ${DB_PASSWORD:-postgres}
|
||||
DB_NAME: ${DB_NAME:-db_lti_erp}
|
||||
REDIS_URL: ${REDIS_URL:-redis://redis:6379/0}
|
||||
ports:
|
||||
- "${APP_PORT:-8081}:8081"
|
||||
depends_on:
|
||||
postgresdb:
|
||||
condition: service_healthy
|
||||
networks: [go-network]
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -qO- http://localhost:8081/healthz || exit 1"]
|
||||
interval: 10s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
start_period: 10s
|
||||
|
||||
volumes:
|
||||
dbdata:
|
||||
go-mod-cache:
|
||||
go-build-cache:
|
||||
|
||||
networks:
|
||||
go-network:
|
||||
name: lti-api_go-network
|
||||
driver: bridge
|
||||
@@ -0,0 +1,84 @@
|
||||
module gitlab.com/mbugroup/lti-api.git
|
||||
|
||||
go 1.23
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic v1.12.1
|
||||
github.com/go-playground/validator/v10 v10.27.0
|
||||
github.com/gofiber/contrib/jwt v1.0.10
|
||||
github.com/gofiber/fiber/v2 v2.52.5
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||
github.com/redis/go-redis/v9 v9.14.0
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/viper v1.19.0
|
||||
golang.org/x/crypto v0.33.0
|
||||
gorm.io/driver/postgres v1.5.9
|
||||
gorm.io/gorm v1.25.11
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/MicahParks/keyfunc/v2 v2.1.0 // indirect
|
||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.2.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||
github.com/glebarez/go-sqlite v1.21.2 // indirect
|
||||
github.com/glebarez/sqlite v1.11.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||
github.com/jackc/pgx/v5 v5.5.5 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/klauspost/compress v1.17.9 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.3 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.17 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||
github.com/philhofer/fwd v1.1.2 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/rogpeppe/go-internal v1.11.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.11.0 // indirect
|
||||
github.com/spf13/cast v1.6.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/tinylib/msgp v1.1.8 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.55.0 // indirect
|
||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.9.0 // indirect
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
golang.org/x/sync v0.11.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gorm.io/driver/sqlite v1.5.5 // indirect
|
||||
modernc.org/libc v1.22.5 // indirect
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
modernc.org/memory v1.5.0 // indirect
|
||||
modernc.org/sqlite v1.23.1 // indirect
|
||||
)
|
||||
@@ -0,0 +1,226 @@
|
||||
github.com/MicahParks/keyfunc/v2 v2.1.0 h1:6ZXKb9Rp6qp1bDbJefnG7cTH8yMN1IC/4nf+GVjO99k=
|
||||
github.com/MicahParks/keyfunc/v2 v2.1.0/go.mod h1:rW42fi+xgLJ2FRRXAfNx9ZA8WpD4OeE/yHVMteCkw9k=
|
||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/bytedance/sonic v1.12.1 h1:jWl5Qz1fy7X1ioY74WqO0KjAMtAGQs4sYnjiEBiyX24=
|
||||
github.com/bytedance/sonic v1.12.1/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM=
|
||||
github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
|
||||
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
|
||||
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
||||
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
|
||||
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||
github.com/gofiber/contrib/jwt v1.0.10 h1:/ilGepl6i0Bntl0Zcd+lAzagY8BiS1+fEiAj32HMApk=
|
||||
github.com/gofiber/contrib/jwt v1.0.10/go.mod h1:1qBENE6sZ6PPT4xIpBzx1VxeyROQO7sj48OlM1I9qdU=
|
||||
github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo=
|
||||
github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
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/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
|
||||
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
|
||||
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
||||
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
|
||||
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw=
|
||||
github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/redis/go-redis/v9 v9.14.0 h1:u4tNCjXOyzfgeLN+vAZaW1xUooqWDqVEsZN0U01jfAE=
|
||||
github.com/redis/go-redis/v9 v9.14.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
||||
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
||||
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
||||
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
||||
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
|
||||
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0=
|
||||
github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.55.0 h1:Zkefzgt6a7+bVKHnu/YaYSOPfNYNisSVBo/unVCf8k8=
|
||||
github.com/valyala/fasthttp v1.55.0/go.mod h1:NkY9JtkrpPKmgwV3HTaS2HWaJss9RSIsRVfcxxoHiOM=
|
||||
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
|
||||
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8=
|
||||
gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
|
||||
gorm.io/driver/sqlite v1.5.5 h1:7MDMtUZhV065SilG62E0MquljeArQZNfJnjd9i9gx3E=
|
||||
gorm.io/driver/sqlite v1.5.5/go.mod h1:6NgQ7sQWAIFsPrJJl1lSNSu2TABh0ZZ/zm5fosATavE=
|
||||
gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg=
|
||||
gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
|
||||
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
|
||||
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
|
||||
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
|
||||
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
|
||||
modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM=
|
||||
modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
Vendored
BIN
Binary file not shown.
Vendored
+38
@@ -0,0 +1,38 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
var (
|
||||
redisClient *redis.Client
|
||||
mu sync.RWMutex
|
||||
)
|
||||
|
||||
// SetRedis assigns the global redis client used across the application.
|
||||
func SetRedis(client *redis.Client) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
redisClient = client
|
||||
}
|
||||
|
||||
// Redis returns the configured redis client. It may be nil if not yet initialised.
|
||||
func Redis() *redis.Client {
|
||||
mu.RLock()
|
||||
defer mu.RUnlock()
|
||||
return redisClient
|
||||
}
|
||||
|
||||
// MustRedis returns the redis client or panics if it has not been set.
|
||||
func MustRedis() *redis.Client {
|
||||
mu.RLock()
|
||||
client := redisClient
|
||||
mu.RUnlock()
|
||||
if client == nil {
|
||||
panic(errors.New("redis client not initialised"))
|
||||
}
|
||||
return client
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Exists reports whether a record with the given ID exists for type T.
|
||||
func Exists[T any](ctx context.Context, db *gorm.DB, id uint) (bool, error) {
|
||||
var count int64
|
||||
if err := db.WithContext(ctx).
|
||||
Model(new(T)).
|
||||
Where("id = ?", id).
|
||||
Count(&count).Error; err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
func ExistsByName[T any](ctx context.Context, db *gorm.DB, name string, excludeID *uint) (bool, error) {
|
||||
var count int64
|
||||
q := db.WithContext(ctx).
|
||||
Model(new(T)).
|
||||
Where("name = ?", name).
|
||||
Where("deleted_at IS NULL")
|
||||
if excludeID != nil {
|
||||
q = q.Where("id <> ?", *excludeID)
|
||||
}
|
||||
if err := q.Count(&count).Error; err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type BaseRepository[T any] interface {
|
||||
GetAll(ctx context.Context, offset, limit int, modifier func(*gorm.DB) *gorm.DB) ([]T, int64, error)
|
||||
GetByID(ctx context.Context, id uint, modifier func(*gorm.DB) *gorm.DB) (*T, error)
|
||||
GetByIDs(ctx context.Context, ids []uint, modifier func(*gorm.DB) *gorm.DB) ([]T, error)
|
||||
First(ctx context.Context, modifier func(*gorm.DB) *gorm.DB) (*T, error)
|
||||
|
||||
CreateOne(ctx context.Context, entity *T, modifier func(*gorm.DB) *gorm.DB) error
|
||||
CreateMany(ctx context.Context, entities []*T, modifier func(*gorm.DB) *gorm.DB) error
|
||||
|
||||
UpdateOne(ctx context.Context, id uint, entity *T, modifier func(*gorm.DB) *gorm.DB) error
|
||||
UpdateMany(ctx context.Context, entities []*T, modifier func(*gorm.DB) *gorm.DB) error
|
||||
PatchOne(ctx context.Context, id uint, updates map[string]any, modifier func(*gorm.DB) *gorm.DB) error
|
||||
|
||||
DeleteOne(ctx context.Context, id uint) error
|
||||
DeleteMany(ctx context.Context, modifier func(*gorm.DB) *gorm.DB) error
|
||||
|
||||
Upsert(ctx context.Context, entity *T, conflictColumns []clause.Column, modifier func(*gorm.DB) *gorm.DB) error
|
||||
|
||||
WithTx(tx *gorm.DB) BaseRepository[T]
|
||||
DB() *gorm.DB
|
||||
}
|
||||
|
||||
type BaseRepositoryImpl[T any] struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewBaseRepository[T any](db *gorm.DB) *BaseRepositoryImpl[T] {
|
||||
return &BaseRepositoryImpl[T]{db: db}
|
||||
}
|
||||
|
||||
func (r *BaseRepositoryImpl[T]) GetAll(
|
||||
ctx context.Context,
|
||||
offset, limit int,
|
||||
modifier func(*gorm.DB) *gorm.DB,
|
||||
) ([]T, int64, error) {
|
||||
var entities []T
|
||||
var total int64
|
||||
|
||||
q := r.db.WithContext(ctx).Model(new(T))
|
||||
if modifier != nil {
|
||||
q = modifier(q)
|
||||
}
|
||||
|
||||
if err := q.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if err := q.Offset(offset).Limit(limit).Find(&entities).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return entities, total, nil
|
||||
}
|
||||
|
||||
func (r *BaseRepositoryImpl[T]) GetByID(
|
||||
ctx context.Context,
|
||||
id uint,
|
||||
modifier func(*gorm.DB) *gorm.DB,
|
||||
) (*T, error) {
|
||||
entity := new(T)
|
||||
q := r.db.WithContext(ctx)
|
||||
if modifier != nil {
|
||||
q = modifier(q)
|
||||
}
|
||||
if err := q.First(entity, id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return entity, nil
|
||||
}
|
||||
|
||||
func (r *BaseRepositoryImpl[T]) GetByIDs(
|
||||
ctx context.Context,
|
||||
ids []uint,
|
||||
modifier func(*gorm.DB) *gorm.DB,
|
||||
) ([]T, error) {
|
||||
var entities []T
|
||||
q := r.db.WithContext(ctx).Model(new(T))
|
||||
if modifier != nil {
|
||||
q = modifier(q)
|
||||
}
|
||||
if err := q.Where("id IN ?", ids).Find(&entities).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(entities) == 0 {
|
||||
return nil, gorm.ErrRecordNotFound
|
||||
}
|
||||
|
||||
return entities, nil
|
||||
}
|
||||
|
||||
func (r *BaseRepositoryImpl[T]) First(
|
||||
ctx context.Context,
|
||||
modifier func(*gorm.DB) *gorm.DB,
|
||||
) (*T, error) {
|
||||
entity := new(T)
|
||||
q := r.db.WithContext(ctx)
|
||||
if modifier != nil {
|
||||
q = modifier(q)
|
||||
}
|
||||
if err := q.First(entity).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return entity, nil
|
||||
}
|
||||
|
||||
// ---- CREATE ----
|
||||
func (r *BaseRepositoryImpl[T]) CreateOne(
|
||||
ctx context.Context,
|
||||
entity *T,
|
||||
modifier func(*gorm.DB) *gorm.DB,
|
||||
) error {
|
||||
q := r.db.WithContext(ctx)
|
||||
if modifier != nil {
|
||||
q = modifier(q)
|
||||
}
|
||||
return q.Create(entity).Error
|
||||
}
|
||||
|
||||
func (r *BaseRepositoryImpl[T]) CreateMany(
|
||||
ctx context.Context,
|
||||
entities []*T,
|
||||
modifier func(*gorm.DB) *gorm.DB,
|
||||
) error {
|
||||
q := r.db.WithContext(ctx)
|
||||
if modifier != nil {
|
||||
q = modifier(q)
|
||||
}
|
||||
return q.Create(&entities).Error
|
||||
}
|
||||
|
||||
// ---- UPDATE ----
|
||||
func (r *BaseRepositoryImpl[T]) UpdateOne(
|
||||
ctx context.Context,
|
||||
id uint,
|
||||
entity *T,
|
||||
modifier func(*gorm.DB) *gorm.DB,
|
||||
) error {
|
||||
q := r.db.WithContext(ctx).Model(new(T)).Where("id = ?", id)
|
||||
if modifier != nil {
|
||||
q = modifier(q)
|
||||
}
|
||||
|
||||
result := q.Updates(entity)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *BaseRepositoryImpl[T]) UpdateMany(
|
||||
ctx context.Context,
|
||||
entities []*T,
|
||||
modifier func(*gorm.DB) *gorm.DB,
|
||||
) error {
|
||||
q := r.db.WithContext(ctx)
|
||||
if modifier != nil {
|
||||
q = modifier(q)
|
||||
}
|
||||
|
||||
result := q.Save(&entities)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *BaseRepositoryImpl[T]) PatchOne(
|
||||
ctx context.Context,
|
||||
id uint,
|
||||
updates map[string]any,
|
||||
modifier func(*gorm.DB) *gorm.DB,
|
||||
) error {
|
||||
q := r.db.WithContext(ctx).Model(new(T)).Where("id = ?", id)
|
||||
if modifier != nil {
|
||||
q = modifier(q)
|
||||
}
|
||||
|
||||
result := q.Updates(updates)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ---- DELETE ----
|
||||
func (r *BaseRepositoryImpl[T]) DeleteOne(ctx context.Context, id uint) error {
|
||||
result := r.db.WithContext(ctx).Delete(new(T), id)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *BaseRepositoryImpl[T]) DeleteMany(ctx context.Context, modifier func(*gorm.DB) *gorm.DB) error {
|
||||
q := r.db.WithContext(ctx).Model(new(T))
|
||||
if modifier != nil {
|
||||
q = modifier(q)
|
||||
}
|
||||
|
||||
result := q.Delete(new(T))
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ---- UPSERT ----
|
||||
func (r *BaseRepositoryImpl[T]) Upsert(
|
||||
ctx context.Context,
|
||||
entity *T,
|
||||
conflictColumns []clause.Column,
|
||||
modifier func(*gorm.DB) *gorm.DB,
|
||||
) error {
|
||||
q := r.db.WithContext(ctx).Clauses(clause.OnConflict{
|
||||
Columns: conflictColumns,
|
||||
UpdateAll: true,
|
||||
})
|
||||
if modifier != nil {
|
||||
q = modifier(q)
|
||||
}
|
||||
return q.Create(entity).Error
|
||||
}
|
||||
|
||||
func (r *BaseRepositoryImpl[T]) WithTx(tx *gorm.DB) BaseRepository[T] {
|
||||
return &BaseRepositoryImpl[T]{db: tx}
|
||||
}
|
||||
|
||||
func (r *BaseRepositoryImpl[T]) DB() *gorm.DB {
|
||||
return r.db
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
// RelationCheck describes a foreign-key style dependency that must exist before processing.
|
||||
type RelationCheck struct {
|
||||
Name string
|
||||
ID *uint
|
||||
Exists func(context.Context, uint) (bool, error)
|
||||
}
|
||||
|
||||
// EnsureRelations validates that each RelationCheck is satisfied, returning consistent Fiber errors.
|
||||
func EnsureRelations(ctx context.Context, checks ...RelationCheck) error {
|
||||
for _, check := range checks {
|
||||
if check.ID == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
exists, err := check.Exists(ctx, *check.ID)
|
||||
if err != nil {
|
||||
return fiber.NewError(
|
||||
fiber.StatusInternalServerError,
|
||||
fmt.Sprintf("Failed to check %s", strings.ToLower(check.Name)),
|
||||
)
|
||||
}
|
||||
if !exists {
|
||||
return fiber.NewError(
|
||||
fiber.StatusNotFound,
|
||||
fmt.Sprintf("%s with id %d not found", check.Name, *check.ID),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package validation
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
var (
|
||||
reUpper = regexp.MustCompile(`[A-Z]`)
|
||||
reLower = regexp.MustCompile(`[a-z]`)
|
||||
reDigit = regexp.MustCompile(`[0-9]`)
|
||||
reSym = regexp.MustCompile(`[^A-Za-z0-9]`)
|
||||
)
|
||||
|
||||
func Password(fl validator.FieldLevel) bool {
|
||||
pw := fl.Field().String()
|
||||
pw = strings.TrimSpace(pw)
|
||||
|
||||
if len(pw) < 8 {
|
||||
return false
|
||||
}
|
||||
if !reUpper.MatchString(pw) {
|
||||
return false
|
||||
}
|
||||
if !reLower.MatchString(pw) {
|
||||
return false
|
||||
}
|
||||
if !reDigit.MatchString(pw) {
|
||||
return false
|
||||
}
|
||||
if !reSym.MatchString(pw) {
|
||||
return false
|
||||
}
|
||||
if strings.Contains(pw, " ") {
|
||||
return false
|
||||
}
|
||||
|
||||
parent := fl.Parent()
|
||||
if parent.IsValid() && parent.Kind() == reflect.Struct {
|
||||
emailField := parent.FieldByName("Email")
|
||||
if emailField.IsValid() && emailField.Kind() == reflect.String {
|
||||
if email := emailField.String(); email != "" {
|
||||
if i := strings.IndexByte(email, '@'); i > 0 {
|
||||
local := strings.ToLower(email[:i])
|
||||
if local != "" && strings.Contains(strings.ToLower(pw), local) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func RequiredStrict(fl validator.FieldLevel) bool {
|
||||
field := fl.Field()
|
||||
|
||||
switch field.Kind() {
|
||||
case reflect.String:
|
||||
return field.String() != ""
|
||||
case reflect.Ptr:
|
||||
return !field.IsNil()
|
||||
}
|
||||
|
||||
return field.IsValid() && !field.IsZero()
|
||||
}
|
||||
|
||||
func OmitemptyStrict(fl validator.FieldLevel) bool {
|
||||
field := fl.Field()
|
||||
|
||||
if !field.IsValid() || field.IsZero() {
|
||||
return true
|
||||
}
|
||||
|
||||
if field.Kind() == reflect.String {
|
||||
return field.String() != ""
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package validation
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
var customMessages = map[string]string{
|
||||
"required": "Field %s is required",
|
||||
"required_strict": "Field %s is required and cannot be null or empty",
|
||||
"omitempty_strict": "Field %s cannot be null or empty when provided",
|
||||
|
||||
"email": "Invalid email address for field %s",
|
||||
"min": "Field %s must have a minimum length of %s characters",
|
||||
"max": "Field %s must have a maximum length of %s characters",
|
||||
"len": "Field %s must be exactly %s characters long",
|
||||
"number": "Field %s must be a number",
|
||||
"positive": "Field %s must be a positive number",
|
||||
"alphanum": "Field %s must contain only alphanumeric characters",
|
||||
"oneof": "Invalid value for field %s",
|
||||
"password": "Field %s must be at least 8 characters, contain uppercase, lowercase, number, and special character",
|
||||
}
|
||||
|
||||
func CustomErrorMessages(err error) map[string]string {
|
||||
var validationErrors validator.ValidationErrors
|
||||
if errors.As(err, &validationErrors) {
|
||||
return generateErrorMessages(validationErrors)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateErrorMessages(validationErrors validator.ValidationErrors) map[string]string {
|
||||
errorsMap := make(map[string]string)
|
||||
for _, err := range validationErrors {
|
||||
fieldName := err.StructNamespace()
|
||||
tag := err.Tag()
|
||||
|
||||
customMessage := customMessages[tag]
|
||||
if customMessage != "" {
|
||||
errorsMap[fieldName] = formatErrorMessage(customMessage, err, tag)
|
||||
} else {
|
||||
errorsMap[fieldName] = defaultErrorMessage(err)
|
||||
}
|
||||
}
|
||||
return errorsMap
|
||||
}
|
||||
|
||||
func formatErrorMessage(customMessage string, err validator.FieldError, tag string) string {
|
||||
if tag == "min" || tag == "max" || tag == "len" {
|
||||
return fmt.Sprintf(customMessage, err.Field(), err.Param())
|
||||
}
|
||||
return fmt.Sprintf(customMessage, err.Field())
|
||||
}
|
||||
|
||||
func defaultErrorMessage(err validator.FieldError) string {
|
||||
return fmt.Sprintf("Field validation for '%s' failed on the '%s' tag", err.Field(), err.Tag())
|
||||
}
|
||||
|
||||
func Validator() *validator.Validate {
|
||||
validate := validator.New()
|
||||
|
||||
if err := validate.RegisterValidation("password", Password); err != nil {
|
||||
return nil
|
||||
}
|
||||
if err := validate.RegisterValidation("required_strict", RequiredStrict); err != nil {
|
||||
return nil
|
||||
}
|
||||
if err := validate.RegisterValidation("omitempty_strict", OmitemptyStrict); err != nil {
|
||||
return nil
|
||||
}
|
||||
return validate
|
||||
}
|
||||
Vendored
BIN
Binary file not shown.
@@ -0,0 +1,251 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type SSOClientConfig struct {
|
||||
PublicID string `json:"public_id"`
|
||||
RedirectURI string `json:"redirect_uri"`
|
||||
Scope string `json:"scope"`
|
||||
Prompt string `json:"prompt"`
|
||||
DefaultReturnURI string `json:"default_return_uri"`
|
||||
AllowedReturnOrigins []string `json:"allowed_return_origins"`
|
||||
SyncSecret string `json:"sync_secret"`
|
||||
}
|
||||
|
||||
var (
|
||||
IsProd bool
|
||||
AppHost string
|
||||
Version string
|
||||
LogLevel string
|
||||
AppPort int
|
||||
DBHost string
|
||||
DBUser string
|
||||
DBPassword string
|
||||
DBName string
|
||||
DBPort int
|
||||
JWTSecret string
|
||||
JWTAccessExp int
|
||||
JWTRefreshExp int
|
||||
JWTResetPasswordExp int
|
||||
JWTVerifyEmailExp int
|
||||
RedisURL string
|
||||
CORSAllowOrigins []string
|
||||
CORSAllowMethods []string
|
||||
CORSAllowHeaders []string
|
||||
CORSExposeHeaders []string
|
||||
CORSAllowCredentials bool
|
||||
CORSMaxAge int
|
||||
SSOIssuer string
|
||||
SSOJWKSURL string
|
||||
SSOAllowedAudiences []string
|
||||
SSOAuthorizeURL string
|
||||
SSOTokenURL string
|
||||
SSOGetMeURL string
|
||||
SSOClients map[string]SSOClientConfig
|
||||
SSOAccessCookieName string
|
||||
SSORefreshCookieName string
|
||||
SSOCookieDomain string
|
||||
SSOCookieSecure bool
|
||||
SSOCookieSameSite string
|
||||
SSOPKCETTL time.Duration
|
||||
SSOUserSyncDrift time.Duration
|
||||
SSOUserSyncNonceTTL time.Duration
|
||||
SSOUserSyncMaxBodyBytes int
|
||||
)
|
||||
|
||||
func init() {
|
||||
loadConfig()
|
||||
|
||||
// server configuration
|
||||
IsProd = viper.GetString("APP_ENV") == "prod"
|
||||
AppHost = viper.GetString("APP_HOST")
|
||||
AppPort = viper.GetInt("APP_PORT")
|
||||
Version = viper.GetString("VERSION")
|
||||
LogLevel = viper.GetString("LOG_LEVEL")
|
||||
|
||||
// database configuration
|
||||
DBHost = viper.GetString("DB_HOST")
|
||||
DBUser = viper.GetString("DB_USER")
|
||||
DBPassword = viper.GetString("DB_PASSWORD")
|
||||
DBName = viper.GetString("DB_NAME")
|
||||
DBPort = viper.GetInt("DB_PORT")
|
||||
|
||||
// jwt configuration
|
||||
JWTSecret = viper.GetString("JWT_SECRET")
|
||||
JWTAccessExp = viper.GetInt("JWT_ACCESS_EXP_MINUTES")
|
||||
JWTRefreshExp = viper.GetInt("JWT_REFRESH_EXP_DAYS")
|
||||
JWTResetPasswordExp = viper.GetInt("JWT_RESET_PASSWORD_EXP_MINUTES")
|
||||
JWTVerifyEmailExp = viper.GetInt("JWT_VERIFY_EMAIL_EXP_MINUTES")
|
||||
|
||||
//Cors
|
||||
CORSAllowOrigins = parseList("CORS_ALLOW_ORIGINS")
|
||||
CORSAllowMethods = parseListWithDefault("CORS_ALLOW_METHODS", "GET,POST,PUT,PATCH,DELETE,OPTIONS")
|
||||
CORSAllowHeaders = parseListWithDefault("CORS_ALLOW_HEADERS", "Content-Type,Authorization,X-Requested-With")
|
||||
CORSExposeHeaders = parseList("CORS_EXPOSE_HEADERS")
|
||||
CORSAllowCredentials = viper.GetBool("CORS_ALLOW_CREDENTIALS")
|
||||
CORSMaxAge = viper.GetInt("CORS_MAX_AGE")
|
||||
|
||||
// Redis
|
||||
RedisURL = viper.GetString("REDIS_URL")
|
||||
|
||||
// SSO integration
|
||||
SSOIssuer = viper.GetString("SSO_ISSUER")
|
||||
SSOJWKSURL = viper.GetString("SSO_JWKS_URL")
|
||||
SSOAllowedAudiences = parseList("SSO_ALLOWED_AUDIENCES")
|
||||
SSOAuthorizeURL = viper.GetString("SSO_AUTHORIZE_URL")
|
||||
SSOTokenURL = viper.GetString("SSO_TOKEN_URL")
|
||||
SSOGetMeURL = viper.GetString("SSO_GETME_URL")
|
||||
SSOAccessCookieName = defaultString(viper.GetString("SSO_ACCESS_COOKIE_NAME"), "sso_access")
|
||||
SSORefreshCookieName = defaultString(viper.GetString("SSO_REFRESH_COOKIE_NAME"), "sso_refresh")
|
||||
SSOCookieDomain = viper.GetString("SSO_COOKIE_DOMAIN")
|
||||
SSOCookieSecure = viper.GetBool("SSO_COOKIE_SECURE")
|
||||
SSOCookieSameSite = defaultString(viper.GetString("SSO_COOKIE_SAMESITE"), "Lax")
|
||||
if ttl := viper.GetInt("SSO_PKCE_TTL_SECONDS"); ttl > 0 {
|
||||
SSOPKCETTL = time.Duration(ttl) * time.Second
|
||||
} else {
|
||||
SSOPKCETTL = 5 * time.Minute
|
||||
}
|
||||
SSOClients = loadSSOClients("SSO_CLIENTS")
|
||||
if drift := viper.GetInt("SSO_USER_SYNC_SIGNATURE_DRIFT_SECONDS"); drift > 0 {
|
||||
SSOUserSyncDrift = time.Duration(drift) * time.Second
|
||||
} else {
|
||||
SSOUserSyncDrift = 2 * time.Minute
|
||||
}
|
||||
if ttl := viper.GetInt("SSO_USER_SYNC_NONCE_TTL_SECONDS"); ttl > 0 {
|
||||
SSOUserSyncNonceTTL = time.Duration(ttl) * time.Second
|
||||
} else {
|
||||
SSOUserSyncNonceTTL = 10 * time.Minute
|
||||
}
|
||||
SSOUserSyncMaxBodyBytes = viper.GetInt("SSO_USER_SYNC_MAX_BODY_BYTES")
|
||||
if SSOUserSyncMaxBodyBytes <= 0 {
|
||||
SSOUserSyncMaxBodyBytes = 32 * 1024
|
||||
}
|
||||
|
||||
if IsProd {
|
||||
ensureProdConfig()
|
||||
}
|
||||
}
|
||||
|
||||
func loadConfig() {
|
||||
viper.AutomaticEnv()
|
||||
|
||||
viper.SetConfigFile(".env")
|
||||
if err := viper.ReadInConfig(); err == nil {
|
||||
utils.Log.Info("Config file loaded from .env")
|
||||
} else {
|
||||
utils.Log.Warn("No .env file found, using environment variables only")
|
||||
}
|
||||
}
|
||||
|
||||
func parseList(key string) []string {
|
||||
raw := strings.TrimSpace(viper.GetString(key))
|
||||
if raw == "" {
|
||||
return nil
|
||||
}
|
||||
if strings.HasPrefix(raw, "[") {
|
||||
var arr []string
|
||||
if json.Unmarshal([]byte(raw), &arr) == nil {
|
||||
for i := range arr {
|
||||
arr[i] = strings.TrimSpace(arr[i])
|
||||
}
|
||||
return arr
|
||||
}
|
||||
}
|
||||
parts := strings.Split(raw, ",")
|
||||
out := make([]string, 0, len(parts))
|
||||
for _, p := range parts {
|
||||
p = strings.TrimSpace(p)
|
||||
if p != "" {
|
||||
out = append(out, p)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func parseListWithDefault(key, def string) []string {
|
||||
if v := parseList(key); len(v) > 0 {
|
||||
return v
|
||||
}
|
||||
// fallback ke default CSV
|
||||
parts := strings.Split(def, ",")
|
||||
for i := range parts {
|
||||
parts[i] = strings.TrimSpace(parts[i])
|
||||
}
|
||||
return parts
|
||||
}
|
||||
|
||||
func loadSSOClients(key string) map[string]SSOClientConfig {
|
||||
clients := make(map[string]SSOClientConfig)
|
||||
raw := strings.TrimSpace(viper.GetString(key))
|
||||
if raw == "" {
|
||||
return clients
|
||||
}
|
||||
if err := json.Unmarshal([]byte(raw), &clients); err != nil {
|
||||
utils.Log.Errorf("Failed to parse %s: %v", key, err)
|
||||
return make(map[string]SSOClientConfig)
|
||||
}
|
||||
result := make(map[string]SSOClientConfig, len(clients))
|
||||
for alias, cfg := range clients {
|
||||
alias = strings.ToLower(strings.TrimSpace(alias))
|
||||
for i, origin := range cfg.AllowedReturnOrigins {
|
||||
cfg.AllowedReturnOrigins[i] = strings.TrimSpace(origin)
|
||||
}
|
||||
cfg.SyncSecret = strings.TrimSpace(cfg.SyncSecret)
|
||||
result[alias] = cfg
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func defaultString(v, def string) string {
|
||||
if strings.TrimSpace(v) == "" {
|
||||
return def
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func ensureProdConfig() {
|
||||
if SSOAuthorizeURL == "" || !strings.HasPrefix(SSOAuthorizeURL, "https://") {
|
||||
panic("SSO_AUTHORIZE_URL must be https in production")
|
||||
}
|
||||
if SSOTokenURL == "" || !strings.HasPrefix(SSOTokenURL, "https://") {
|
||||
panic("SSO_TOKEN_URL must be https in production")
|
||||
}
|
||||
if SSOGetMeURL == "" || !strings.HasPrefix(SSOGetMeURL, "https://") {
|
||||
panic("SSO_GETME_URL must be https in production")
|
||||
}
|
||||
if !SSOCookieSecure {
|
||||
panic("SSO_COOKIE_SECURE must be true in production")
|
||||
}
|
||||
if SSOCookieDomain == "" {
|
||||
panic("SSO_COOKIE_DOMAIN must be configured in production")
|
||||
}
|
||||
if len(SSOAllowedAudiences) == 0 {
|
||||
panic("SSO_ALLOWED_AUDIENCES must contain at least one audience in production")
|
||||
}
|
||||
for alias, cfg := range SSOClients {
|
||||
if strings.TrimSpace(cfg.SyncSecret) == "" {
|
||||
panic(fmt.Sprintf("SSO_CLIENTS[%s].sync_secret must be configured in production", alias))
|
||||
}
|
||||
if len(cfg.SyncSecret) < 16 {
|
||||
panic(fmt.Sprintf("SSO_CLIENTS[%s].sync_secret must be at least 16 characters", alias))
|
||||
}
|
||||
}
|
||||
if SSOUserSyncDrift <= 0 {
|
||||
panic("SSO_USER_SYNC_SIGNATURE_DRIFT_SECONDS must be greater than zero in production")
|
||||
}
|
||||
if SSOUserSyncNonceTTL <= 0 {
|
||||
panic("SSO_USER_SYNC_NONCE_TTL_SECONDS must be greater than zero in production")
|
||||
}
|
||||
if SSOUserSyncMaxBodyBytes <= 0 {
|
||||
panic("SSO_USER_SYNC_MAX_BODY_BYTES must be greater than zero in production")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func FiberConfig() fiber.Config {
|
||||
return fiber.Config{
|
||||
Prefork: IsProd,
|
||||
CaseSensitive: true,
|
||||
ServerHeader: "Fiber",
|
||||
AppName: "Fiber API",
|
||||
ErrorHandler: utils.ErrorHandler,
|
||||
JSONEncoder: sonic.Marshal,
|
||||
JSONDecoder: sonic.Unmarshal,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEArf9cLsf3m4TituVqDwvM
|
||||
yaUwQ0rzDfOcmF/N+rHvgMMv1yyR4FcozoGk1NFfL/4jDIVm9FLUS68foPDo0iu5
|
||||
shNY0pwSsps9lcyWxQVhUVJzh489S53hU799PiDrUPBxYTcpy3EO/jX0HOZJs5dl
|
||||
N/4C54LYrVdXyleG82NLNjcMnNGr3VGc6zE7B3YYd9/daPyr+QBpeUL5BIzUZbeu
|
||||
sI0NMIxucaqxMKWF62CDWTrwfSSoFOubI9FZ9tkkWro01wVFK35GseQCsDtEmJ9v
|
||||
kb81LvfM2AcPLr+g1kN8dVeZLNNQTMrmxaWXFiwwEgayJ8q01pHfgAxg42ariKEK
|
||||
fX9kFx/3Rs80qsXhQNEkoCOwQBRNwrRxRzNfVkvuE0aRVoO6PVFE1gDOLUV2fJJs
|
||||
QUpAWMzZ/+e/N+1gKMtbaCbz2dLqnA6KkdMdHe79dMFVGx2ZnRFbyALzM3S5XgNV
|
||||
QtVvTri2PW/6ZH41T6MpLUANzuwaIEys1Az+8VLxOgBugb63xoORB2JDsebxEfsS
|
||||
HBllECnBJVuBndkJRSnbqGjCKq4sl2xXo83nZ+2eNmZO/vkTxREl8aVp3DgaHWxp
|
||||
OQIlZwbP9lsruTqSnQfH3/hLemrOhSh/hXfFguw3oOQjfeFwJBD8u7vGOl2vBi3C
|
||||
hvb8hFdjzoUXAJLxWPl5+E0CAwEAAQ==
|
||||
-----END PUBLIC KEY-----
|
||||
@@ -0,0 +1,17 @@
|
||||
package config
|
||||
|
||||
var allRoles = map[string][]string{
|
||||
"user": {},
|
||||
"admin": {"getUsers", "manageUsers"},
|
||||
}
|
||||
|
||||
var Roles = getKeys(allRoles)
|
||||
var RoleRights = allRoles
|
||||
|
||||
func getKeys(m map[string][]string) []string {
|
||||
keys := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package config
|
||||
|
||||
const (
|
||||
TokenTypeAccess = "access"
|
||||
TokenTypeRefresh = "refresh"
|
||||
TokenTypeResetPassword = "resetPassword"
|
||||
TokenTypeVerifyEmail = "verifyEmail"
|
||||
)
|
||||
@@ -0,0 +1,42 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/config"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
func Connect(dbHost, dbName string) *gorm.DB {
|
||||
dsn := fmt.Sprintf(
|
||||
"host=%s user=%s password=%s dbname=%s port=%d sslmode=disable TimeZone=Asia/Shanghai",
|
||||
dbHost, config.DBUser, config.DBPassword, dbName, config.DBPort,
|
||||
)
|
||||
|
||||
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Info),
|
||||
SkipDefaultTransaction: true,
|
||||
PrepareStmt: true,
|
||||
TranslateError: true,
|
||||
})
|
||||
if err != nil {
|
||||
utils.Log.Errorf("Failed to connect to database: %+v", err)
|
||||
}
|
||||
|
||||
sqlDB, errDB := db.DB()
|
||||
if errDB != nil {
|
||||
utils.Log.Errorf("Failed to connect to database: %+v", errDB)
|
||||
}
|
||||
|
||||
// Config connection pooling
|
||||
sqlDB.SetMaxIdleConns(10)
|
||||
sqlDB.SetMaxOpenConns(100)
|
||||
sqlDB.SetConnMaxLifetime(60 * time.Minute)
|
||||
|
||||
return db
|
||||
}
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
CREATE DATABASE IF NOT EXISTS db_lti_erp;
|
||||
@@ -0,0 +1,38 @@
|
||||
|
||||
DROP TABLE IF EXISTS fcr_standards;
|
||||
DROP INDEX IF EXISTS suppliers_name_unique;
|
||||
DROP TABLE IF EXISTS product_suppliers;
|
||||
DROP INDEX IF EXISTS products_sku_unique;
|
||||
DROP INDEX IF EXISTS products_name_unique;
|
||||
DROP TABLE IF EXISTS products;
|
||||
DROP INDEX IF EXISTS flags_flagable_lookup;
|
||||
DROP INDEX IF EXISTS flags_unique_flagable;
|
||||
DROP TABLE IF EXISTS flags;
|
||||
DROP INDEX IF EXISTS customers_name_unique;
|
||||
DROP INDEX IF EXISTS customers_email_unique;
|
||||
DROP TABLE IF EXISTS customers;
|
||||
DROP INDEX IF EXISTS warehouses_name_unique;
|
||||
DROP INDEX IF EXISTS product_categories_code_unique;
|
||||
DROP INDEX IF EXISTS product_categories_name_unique;
|
||||
DROP TABLE IF EXISTS product_categories;
|
||||
DROP INDEX IF EXISTS nonstocks_name_unique;
|
||||
DROP TABLE IF EXISTS nonstock_suppliers;
|
||||
DROP TABLE IF EXISTS nonstocks;
|
||||
DROP INDEX IF EXISTS banks_name_unique;
|
||||
DROP TABLE IF EXISTS banks;
|
||||
DROP INDEX IF EXISTS kandangs_name_unique;
|
||||
DROP TABLE IF EXISTS warehouses;
|
||||
DROP TABLE IF EXISTS kandangs;
|
||||
DROP INDEX IF EXISTS locations_name_unique;
|
||||
DROP TABLE IF EXISTS locations;
|
||||
DROP INDEX IF EXISTS areas_name_unique;
|
||||
DROP TABLE IF EXISTS areas;
|
||||
DROP INDEX IF EXISTS uoms_name_unique;
|
||||
DROP TABLE IF EXISTS uoms;
|
||||
DROP TABLE IF EXISTS suppliers;
|
||||
DROP INDEX IF EXISTS fcrs_name_unique;
|
||||
DROP TABLE IF EXISTS fcrs;
|
||||
DROP TABLE IF EXISTS projects;
|
||||
DROP INDEX IF EXISTS users_id_user_unique;
|
||||
DROP INDEX IF EXISTS users_email_unique;
|
||||
DROP TABLE IF EXISTS users;
|
||||
@@ -0,0 +1,235 @@
|
||||
-- USERS
|
||||
CREATE TABLE users (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
id_user BIGINT NOT NULL,
|
||||
name VARCHAR NOT NULL,
|
||||
email VARCHAR NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX users_id_user_unique ON users (id_user) WHERE deleted_at IS NULL;
|
||||
|
||||
CREATE UNIQUE INDEX users_email_unique ON users (email) WHERE deleted_at IS NULL;
|
||||
|
||||
-- FLAGS
|
||||
CREATE TABLE flags (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR NOT NULL,
|
||||
flagable_id BIGINT NOT NULL,
|
||||
flagable_type VARCHAR(50) NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX flags_unique_flagable ON flags (name, flagable_id, flagable_type);
|
||||
CREATE INDEX flags_flagable_lookup ON flags (flagable_type, flagable_id);
|
||||
|
||||
-- PRODUCT CATEGORIES
|
||||
CREATE TABLE product_categories (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR NOT NULL,
|
||||
code VARCHAR(10) NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
CREATE UNIQUE INDEX product_categories_name_unique ON product_categories (name) WHERE deleted_at IS NULL;
|
||||
CREATE UNIQUE INDEX product_categories_code_unique ON product_categories (code) WHERE deleted_at IS NULL;
|
||||
|
||||
-- UOM
|
||||
CREATE TABLE uoms (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
CREATE UNIQUE INDEX uoms_name_unique ON uoms (name) WHERE deleted_at IS NULL;
|
||||
|
||||
-- BANKS
|
||||
CREATE TABLE banks (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR NOT NULL,
|
||||
alias VARCHAR(5) NOT NULL,
|
||||
owner VARCHAR,
|
||||
account_number VARCHAR(50) NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
CREATE UNIQUE INDEX banks_name_unique ON banks (name) WHERE deleted_at IS NULL;
|
||||
|
||||
-- AREAS
|
||||
CREATE TABLE areas (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
CREATE UNIQUE INDEX areas_name_unique ON areas (name) WHERE deleted_at IS NULL;
|
||||
|
||||
-- LOCATIONS
|
||||
CREATE TABLE locations (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR NOT NULL,
|
||||
address TEXT NOT NULL,
|
||||
area_id BIGINT NOT NULL REFERENCES areas(id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
CREATE UNIQUE INDEX locations_name_unique ON locations (name) WHERE deleted_at IS NULL;
|
||||
|
||||
-- KANDANG
|
||||
CREATE TABLE kandangs (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR NOT NULL,
|
||||
location_id BIGINT NOT NULL REFERENCES locations(id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
pic_id BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
CREATE UNIQUE INDEX kandangs_name_unique ON kandangs (name) WHERE deleted_at IS NULL;
|
||||
|
||||
-- WAREHOUSES
|
||||
CREATE TABLE warehouses (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR NOT NULL,
|
||||
type VARCHAR(50) NOT NULL,
|
||||
area_id BIGINT NOT NULL REFERENCES areas(id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
location_id BIGINT REFERENCES locations(id) ON DELETE SET NULL ON UPDATE CASCADE,
|
||||
kandang_id BIGINT REFERENCES kandangs(id) ON DELETE SET NULL ON UPDATE CASCADE,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
CREATE UNIQUE INDEX warehouses_name_unique ON warehouses (name) WHERE deleted_at IS NULL;
|
||||
|
||||
-- CUSTOMERS
|
||||
CREATE TABLE customers (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR NOT NULL,
|
||||
pic_id BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE,
|
||||
type VARCHAR(50) NOT NULL,
|
||||
address TEXT NOT NULL,
|
||||
phone VARCHAR(20) NOT NULL,
|
||||
email VARCHAR NOT NULL,
|
||||
account_number VARCHAR(50) NOT NULL,
|
||||
balance NUMERIC(15,3) DEFAULT 0,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
CREATE UNIQUE INDEX customers_name_unique ON customers (name) WHERE deleted_at IS NULL;
|
||||
|
||||
-- NONSTOCK
|
||||
CREATE TABLE nonstocks (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR NOT NULL,
|
||||
uom_id BIGINT NOT NULL REFERENCES uoms(id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
CREATE UNIQUE INDEX nonstocks_name_unique ON nonstocks (name) WHERE deleted_at IS NULL;
|
||||
|
||||
-- FCR
|
||||
CREATE TABLE fcrs (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
CREATE UNIQUE INDEX fcrs_name_unique ON fcrs (name) WHERE deleted_at IS NULL;
|
||||
|
||||
CREATE TABLE fcr_standards (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
fcr_id BIGINT NOT NULL REFERENCES fcrs(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
weight NUMERIC(15,3) NOT NULL,
|
||||
fcr_number NUMERIC(15,3) NOT NULL,
|
||||
mortality NUMERIC(15,3) NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
-- SUPPLIERS
|
||||
CREATE TABLE suppliers (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR NOT NULL,
|
||||
alias VARCHAR(5) NOT NULL,
|
||||
pic VARCHAR NOT NULL,
|
||||
type VARCHAR(50) NOT NULL,
|
||||
category VARCHAR(20) NOT NULL,
|
||||
hatchery VARCHAR,
|
||||
phone VARCHAR(20) NOT NULL,
|
||||
email VARCHAR NOT NULL,
|
||||
address TEXT NOT NULL,
|
||||
npwp VARCHAR(50),
|
||||
account_number VARCHAR(50),
|
||||
balance NUMERIC(15,3) DEFAULT 0,
|
||||
due_date INT NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
CREATE UNIQUE INDEX suppliers_name_unique ON suppliers (name) WHERE deleted_at IS NULL;
|
||||
|
||||
CREATE TABLE nonstock_suppliers (
|
||||
nonstock_id BIGINT NOT NULL REFERENCES nonstocks(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
supplier_id BIGINT NOT NULL REFERENCES suppliers(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
PRIMARY KEY (nonstock_id, supplier_id)
|
||||
);
|
||||
|
||||
-- PRODUCTS
|
||||
CREATE TABLE products (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR NOT NULL,
|
||||
brand VARCHAR NOT NULL,
|
||||
sku VARCHAR(100),
|
||||
uom_id BIGINT NOT NULL REFERENCES uoms(id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
product_category_id BIGINT NOT NULL REFERENCES product_categories(id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
product_price NUMERIC(15,3) NOT NULL,
|
||||
selling_price NUMERIC(15,3),
|
||||
tax NUMERIC(15,3),
|
||||
expiry_period INT,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
CREATE UNIQUE INDEX products_name_unique ON products (name) WHERE deleted_at IS NULL;
|
||||
CREATE UNIQUE INDEX products_sku_unique ON products (sku) WHERE deleted_at IS NULL;
|
||||
|
||||
CREATE TABLE product_suppliers (
|
||||
product_id BIGINT NOT NULL REFERENCES products(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
supplier_id BIGINT NOT NULL REFERENCES suppliers(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
PRIMARY KEY (product_id, supplier_id)
|
||||
);
|
||||
|
||||
-- PROJECTS
|
||||
CREATE TABLE projects (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
@@ -0,0 +1,777 @@
|
||||
package seed
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func Run(db *gorm.DB) error {
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
users, err := seedUsers(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
adminID := users["admin"]
|
||||
|
||||
uoms, err := seedUoms(tx, adminID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
areas, err := seedAreas(tx, adminID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
locations, err := seedLocations(tx, adminID, areas)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
kandangs, err := seedKandangs(tx, adminID, locations, users)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := seedWarehouses(tx, adminID, areas, locations, kandangs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
productCategories, err := seedProductCategories(tx, adminID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
suppliers, err := seedSuppliers(tx, adminID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := seedCustomers(tx, adminID, users); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := seedFcr(tx, adminID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := seedProducts(tx, adminID, uoms, productCategories, suppliers); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := seedNonstocks(tx, adminID, uoms, suppliers); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := seedBanks(tx, adminID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("✅ Master data seeding completed")
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func seedUsers(tx *gorm.DB) (map[string]uint, error) {
|
||||
seeds := []struct {
|
||||
Key string
|
||||
Data entity.User
|
||||
}{
|
||||
{
|
||||
Key: "admin",
|
||||
Data: entity.User{Email: "admin@mbugroup.id", IdUser: 1, Name: "Super Admin"},
|
||||
},
|
||||
}
|
||||
|
||||
result := make(map[string]uint, len(seeds))
|
||||
|
||||
for _, seed := range seeds {
|
||||
var user entity.User
|
||||
err := tx.Where("email = ?", seed.Data.Email).First(&user).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
user = seed.Data
|
||||
if err := tx.Create(&user).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result[seed.Key] = user.Id
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func seedUoms(tx *gorm.DB, createdBy uint) (map[string]uint, error) {
|
||||
names := []string{"Kilogram", "Gram", "Liter", "Unit", "Ekor"}
|
||||
result := make(map[string]uint, len(names))
|
||||
|
||||
for _, name := range names {
|
||||
var uom entity.Uom
|
||||
err := tx.Where("name = ?", name).First(&uom).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
uom = entity.Uom{Name: name, CreatedBy: createdBy}
|
||||
if err := tx.Create(&uom).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result[name] = uom.Id
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func seedAreas(tx *gorm.DB, createdBy uint) (map[string]uint, error) {
|
||||
names := []string{"Priangan", "Banten"}
|
||||
result := make(map[string]uint, len(names))
|
||||
|
||||
for _, name := range names {
|
||||
var area entity.Area
|
||||
err := tx.Where("name = ?", name).First(&area).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
area = entity.Area{Name: name, CreatedBy: createdBy}
|
||||
if err := tx.Create(&area).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result[name] = area.Id
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func seedLocations(tx *gorm.DB, createdBy uint, areas map[string]uint) (map[string]uint, error) {
|
||||
seeds := []struct {
|
||||
Name string
|
||||
Address string
|
||||
Area string
|
||||
}{
|
||||
{"Singaparna", "Tasik", "Priangan"},
|
||||
{"Cikaum", "Cikaum", "Banten"},
|
||||
}
|
||||
|
||||
result := make(map[string]uint, len(seeds))
|
||||
|
||||
for _, seed := range seeds {
|
||||
areaID, ok := areas[seed.Area]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("area %s not seeded", seed.Area)
|
||||
}
|
||||
|
||||
var loc entity.Location
|
||||
err := tx.Where("name = ?", seed.Name).First(&loc).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
loc = entity.Location{
|
||||
Name: seed.Name,
|
||||
Address: seed.Address,
|
||||
AreaId: areaID,
|
||||
CreatedBy: createdBy,
|
||||
}
|
||||
if err := tx.Create(&loc).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result[seed.Name] = loc.Id
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users map[string]uint) (map[string]uint, error) {
|
||||
seeds := []struct {
|
||||
Name string
|
||||
Location string
|
||||
PicKey string
|
||||
}{
|
||||
{"Singaparna 1", "Singaparna", "admin"},
|
||||
{"Singaparna 2", "Singaparna", "admin"},
|
||||
{"Cikaum 1", "Cikaum", "admin"},
|
||||
{"Cikaum 2", "Cikaum", "admin"},
|
||||
}
|
||||
|
||||
result := make(map[string]uint, len(seeds))
|
||||
|
||||
for _, seed := range seeds {
|
||||
locID, ok := locations[seed.Location]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("location %s not seeded", seed.Location)
|
||||
}
|
||||
picID, ok := users[seed.PicKey]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("user %s not seeded", seed.PicKey)
|
||||
}
|
||||
|
||||
var kandang entity.Kandang
|
||||
err := tx.Where("name = ?", seed.Name).First(&kandang).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
kandang = entity.Kandang{
|
||||
Name: seed.Name,
|
||||
LocationId: locID,
|
||||
PicId: picID,
|
||||
CreatedBy: createdBy,
|
||||
}
|
||||
if err := tx.Create(&kandang).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result[seed.Name] = kandang.Id
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func seedWarehouses(tx *gorm.DB, createdBy uint, areas map[string]uint, locations map[string]uint, kandangs map[string]uint) error {
|
||||
seeds := []struct {
|
||||
Name string
|
||||
Type string
|
||||
Area string
|
||||
Location *string
|
||||
Kandang *string
|
||||
}{
|
||||
{Name: "Gudang Priangan", Type: string(utils.WarehouseTypeArea), Area: "Priangan"},
|
||||
{Name: "Gudang Singaparna", Type: string(utils.WarehouseTypeLokasi), Area: "Priangan", Location: strPtr("Singaparna")},
|
||||
{Name: "Gudang Singaparna 1", Type: string(utils.WarehouseTypeKandang), Area: "Priangan", Location: strPtr("Singaparna"), Kandang: strPtr("Singaparna 1")},
|
||||
{Name: "Gudang Singaparna 2", Type: string(utils.WarehouseTypeKandang), Area: "Priangan", Location: strPtr("Singaparna"), Kandang: strPtr("Singaparna 2")},
|
||||
{Name: "Gudang Banten", Type: string(utils.WarehouseTypeArea), Area: "Banten"},
|
||||
{Name: "Gudang Cikaum", Type: string(utils.WarehouseTypeLokasi), Area: "Banten", Location: strPtr("Cikaum")},
|
||||
{Name: "Gudang Cikaum 1", Type: string(utils.WarehouseTypeKandang), Area: "Banten", Location: strPtr("Cikaum"), Kandang: strPtr("Cikaum 1")},
|
||||
{Name: "Gudang Cikaum 2", Type: string(utils.WarehouseTypeKandang), Area: "Banten", Location: strPtr("Cikaum"), Kandang: strPtr("Cikaum 2")},
|
||||
}
|
||||
|
||||
for _, seed := range seeds {
|
||||
areaID, ok := areas[seed.Area]
|
||||
if !ok {
|
||||
return fmt.Errorf("area %s not seeded", seed.Area)
|
||||
}
|
||||
|
||||
var warehouse entity.Warehouse
|
||||
err := tx.Where("name = ?", seed.Name).First(&warehouse).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
warehouse = entity.Warehouse{
|
||||
Name: seed.Name,
|
||||
Type: seed.Type,
|
||||
AreaId: areaID,
|
||||
CreatedBy: createdBy,
|
||||
}
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if seed.Location != nil {
|
||||
locID, ok := locations[*seed.Location]
|
||||
if !ok {
|
||||
return fmt.Errorf("location %s not seeded", *seed.Location)
|
||||
}
|
||||
warehouse.LocationId = uintPtr(locID)
|
||||
}
|
||||
if seed.Kandang != nil {
|
||||
kandangID, ok := kandangs[*seed.Kandang]
|
||||
if !ok {
|
||||
return fmt.Errorf("kandang %s not seeded", *seed.Kandang)
|
||||
}
|
||||
warehouse.KandangId = uintPtr(kandangID)
|
||||
}
|
||||
|
||||
if warehouse.Id == 0 {
|
||||
if err := tx.Create(&warehouse).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := tx.Model(&entity.Warehouse{}).Where("id = ?", warehouse.Id).Updates(map[string]any{
|
||||
"type": warehouse.Type,
|
||||
"area_id": warehouse.AreaId,
|
||||
"location_id": warehouse.LocationId,
|
||||
"kandang_id": warehouse.KandangId,
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func seedProductCategories(tx *gorm.DB, createdBy uint) (map[string]uint, error) {
|
||||
seeds := []struct {
|
||||
Name string
|
||||
Code string
|
||||
}{
|
||||
{"Bahan Baku", "RAW"},
|
||||
{"Day Old Chick", "DOC"},
|
||||
}
|
||||
|
||||
result := make(map[string]uint, len(seeds))
|
||||
|
||||
for _, seed := range seeds {
|
||||
var category entity.ProductCategory
|
||||
err := tx.Where("name = ?", seed.Name).First(&category).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
category = entity.ProductCategory{Name: seed.Name, Code: seed.Code, CreatedBy: createdBy}
|
||||
if err := tx.Create(&category).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
if err := tx.Model(&entity.ProductCategory{}).Where("id = ?", category.Id).Updates(map[string]any{
|
||||
"code": seed.Code,
|
||||
}).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
result[seed.Name] = category.Id
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func seedSuppliers(tx *gorm.DB, createdBy uint) (map[string]uint, error) {
|
||||
seeds := []struct {
|
||||
Name string
|
||||
Alias string
|
||||
Category string
|
||||
Email string
|
||||
Phone string
|
||||
Address string
|
||||
}{
|
||||
{"PT CHAROEN POKPHAND INDONESIA Tbk", "CPI", string(utils.SupplierCategorySapronak), "cpi@gmail.com", "081200000001", "Jl. Pakan 1, Bekasi"},
|
||||
{"BOP Vendor", "BOP", string(utils.SupplierCategoryBOP), "bop@gmail.com", "081200000002", "Jl. Veteriner 3, Bogor"},
|
||||
{"Ekspedisi", "EKS", string(utils.SupplierCategoryBOP), "bop@gmail.com", "081200000002", "Jl. Veteriner 3, Bogor"},
|
||||
}
|
||||
|
||||
result := make(map[string]uint, len(seeds))
|
||||
|
||||
for idx, seed := range seeds {
|
||||
var supplier entity.Supplier
|
||||
err := tx.Where("name = ?", seed.Name).First(&supplier).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
supplier = entity.Supplier{
|
||||
Name: seed.Name,
|
||||
Alias: seed.Alias,
|
||||
Pic: "John Doe",
|
||||
Type: string(utils.CustomerSupplierTypeBisnis),
|
||||
Category: seed.Category,
|
||||
Phone: seed.Phone,
|
||||
Email: seed.Email,
|
||||
Address: seed.Address,
|
||||
DueDate: 30,
|
||||
CreatedBy: createdBy,
|
||||
AccountNumber: strPtr(fmt.Sprintf("%03d", idx+1)),
|
||||
}
|
||||
if err := tx.Create(&supplier).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result[seed.Name] = supplier.Id
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func seedCustomers(tx *gorm.DB, createdBy uint, users map[string]uint) error {
|
||||
seeds := []struct {
|
||||
Name string
|
||||
PicKey string
|
||||
Address string
|
||||
Phone string
|
||||
Email string
|
||||
}{
|
||||
{"Abdul Azis", "admin", "Jl. Raya Utama 1, Bekasi", "082100000001", "abdul.azis@gmail.com"},
|
||||
}
|
||||
|
||||
for idx, seed := range seeds {
|
||||
picID, ok := users[seed.PicKey]
|
||||
if !ok {
|
||||
return fmt.Errorf("user %s not seeded", seed.PicKey)
|
||||
}
|
||||
|
||||
var customer entity.Customer
|
||||
err := tx.Where("name = ?", seed.Name).First(&customer).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
customer = entity.Customer{
|
||||
Name: seed.Name,
|
||||
PicId: picID,
|
||||
Type: string(utils.CustomerSupplierTypeBisnis),
|
||||
Address: seed.Address,
|
||||
Phone: seed.Phone,
|
||||
Email: seed.Email,
|
||||
AccountNumber: *strPtr(fmt.Sprintf("%03d", idx+1)),
|
||||
CreatedBy: createdBy,
|
||||
}
|
||||
if err := tx.Create(&customer).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func seedFcr(tx *gorm.DB, createdBy uint) error {
|
||||
seeds := []struct {
|
||||
Name string
|
||||
Standards []struct {
|
||||
Weight float64
|
||||
FcrNumber float64
|
||||
Mortality float64
|
||||
}
|
||||
}{
|
||||
{
|
||||
Name: "FCR Layer",
|
||||
Standards: []struct {
|
||||
Weight float64
|
||||
FcrNumber float64
|
||||
Mortality float64
|
||||
}{
|
||||
{Weight: 0.8, FcrNumber: 1.60, Mortality: 2.0},
|
||||
{Weight: 1.5, FcrNumber: 1.75, Mortality: 3.5},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, seed := range seeds {
|
||||
var fcr entity.Fcr
|
||||
err := tx.Where("name = ?", seed.Name).First(&fcr).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
fcr = entity.Fcr{Name: seed.Name, CreatedBy: createdBy}
|
||||
if err := tx.Create(&fcr).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, std := range seed.Standards {
|
||||
var standard entity.FcrStandard
|
||||
err := tx.Where("fcr_id = ? AND weight = ?", fcr.Id, std.Weight).First(&standard).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
standard = entity.FcrStandard{
|
||||
FcrID: fcr.Id,
|
||||
Weight: std.Weight,
|
||||
FcrNumber: std.FcrNumber,
|
||||
Mortality: std.Mortality,
|
||||
}
|
||||
if err := tx.Create(&standard).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err != nil {
|
||||
return err
|
||||
} else {
|
||||
if err := tx.Model(&entity.FcrStandard{}).Where("id = ?", standard.Id).Updates(map[string]any{
|
||||
"fcr_number": std.FcrNumber,
|
||||
"mortality": std.Mortality,
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories map[string]uint, suppliers map[string]uint) error {
|
||||
seeds := []struct {
|
||||
Name string
|
||||
Brand string
|
||||
Sku string
|
||||
Uom string
|
||||
Category string
|
||||
Price float64
|
||||
Selling *float64
|
||||
Tax *float64
|
||||
Expiry *int
|
||||
Suppliers []string
|
||||
Flags []utils.FlagType
|
||||
}{
|
||||
{
|
||||
Name: "DOC Broiler",
|
||||
Brand: "MBU Broiler",
|
||||
Sku: "BRO0001",
|
||||
Uom: "Ekor",
|
||||
Category: "Day Old Chick",
|
||||
Price: 7500,
|
||||
Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"},
|
||||
Flags: []utils.FlagType{utils.FlagDOC},
|
||||
},
|
||||
{
|
||||
Name: "281 SPECIAL STARTER",
|
||||
Brand: "281 STARTER",
|
||||
Sku: "281",
|
||||
Uom: "Kilogram",
|
||||
Category: "Bahan Baku",
|
||||
Price: 7850,
|
||||
Expiry: intPtr(60),
|
||||
Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"},
|
||||
Flags: []utils.FlagType{utils.FlagPakan, utils.FlagStarter},
|
||||
},
|
||||
}
|
||||
|
||||
for _, seed := range seeds {
|
||||
uomID, ok := uoms[seed.Uom]
|
||||
if !ok {
|
||||
return fmt.Errorf("uom %s not seeded", seed.Uom)
|
||||
}
|
||||
categoryID, ok := categories[seed.Category]
|
||||
if !ok {
|
||||
return fmt.Errorf("product category %s not seeded", seed.Category)
|
||||
}
|
||||
|
||||
var product entity.Product
|
||||
err := tx.Where("name = ?", seed.Name).First(&product).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
selling := seed.Selling
|
||||
tax := seed.Tax
|
||||
product = entity.Product{
|
||||
Name: seed.Name,
|
||||
Brand: seed.Brand,
|
||||
Sku: &seed.Sku,
|
||||
UomId: uomID,
|
||||
ProductCategoryId: categoryID,
|
||||
ProductPrice: seed.Price,
|
||||
SellingPrice: selling,
|
||||
Tax: tax,
|
||||
ExpiryPeriod: seed.Expiry,
|
||||
CreatedBy: createdBy,
|
||||
}
|
||||
if err := tx.Create(&product).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err != nil {
|
||||
return err
|
||||
} else {
|
||||
updates := map[string]any{
|
||||
"brand": seed.Brand,
|
||||
"uom_id": uomID,
|
||||
"product_category_id": categoryID,
|
||||
"product_price": seed.Price,
|
||||
"selling_price": seed.Selling,
|
||||
"tax": seed.Tax,
|
||||
"expiry_period": seed.Expiry,
|
||||
}
|
||||
if seed.Sku != "" {
|
||||
updates["sku"] = seed.Sku
|
||||
}
|
||||
if err := tx.Model(&entity.Product{}).Where("id = ?", product.Id).Updates(updates).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, supplierName := range seed.Suppliers {
|
||||
supplierID, ok := suppliers[supplierName]
|
||||
if !ok {
|
||||
return fmt.Errorf("supplier %s not seeded", supplierName)
|
||||
}
|
||||
var existing entity.ProductSupplier
|
||||
err := tx.Where("product_id = ? AND supplier_id = ?", product.Id, supplierID).First(&existing).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
link := entity.ProductSupplier{ProductID: product.Id, SupplierID: supplierID}
|
||||
if err := tx.Create(&link).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := seedFlags(tx, product.Id, entity.FlagableTypeProduct, seed.Flags); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func seedNonstocks(tx *gorm.DB, createdBy uint, uoms map[string]uint, suppliers map[string]uint) error {
|
||||
seeds := []struct {
|
||||
Name string
|
||||
Uom string
|
||||
Suppliers []string
|
||||
Flags []utils.FlagType
|
||||
}{
|
||||
{
|
||||
Name: "Expedisi DOC",
|
||||
Uom: "Ekor",
|
||||
Suppliers: []string{"Ekspedisi"},
|
||||
Flags: []utils.FlagType{utils.FlagEkspedisi},
|
||||
},
|
||||
{
|
||||
Name: "Solar",
|
||||
Uom: "Liter",
|
||||
Suppliers: []string{"BOP Vendor"},
|
||||
Flags: []utils.FlagType{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, seed := range seeds {
|
||||
uomID, ok := uoms[seed.Uom]
|
||||
if !ok {
|
||||
return fmt.Errorf("uom %s not seeded", seed.Uom)
|
||||
}
|
||||
|
||||
var nonstock entity.Nonstock
|
||||
err := tx.Where("name = ?", seed.Name).First(&nonstock).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
nonstock = entity.Nonstock{
|
||||
Name: seed.Name,
|
||||
UomId: uomID,
|
||||
CreatedBy: createdBy,
|
||||
}
|
||||
if err := tx.Create(&nonstock).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err != nil {
|
||||
return err
|
||||
} else {
|
||||
if err := tx.Model(&entity.Nonstock{}).Where("id = ?", nonstock.Id).Updates(map[string]any{
|
||||
"uom_id": uomID,
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, supplierName := range seed.Suppliers {
|
||||
supplierID, ok := suppliers[supplierName]
|
||||
if !ok {
|
||||
return fmt.Errorf("supplier %s not seeded", supplierName)
|
||||
}
|
||||
var existing entity.NonstockSupplier
|
||||
err := tx.Where("nonstock_id = ? AND supplier_id = ?", nonstock.Id, supplierID).First(&existing).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
link := entity.NonstockSupplier{NonstockID: nonstock.Id, SupplierID: supplierID}
|
||||
if err := tx.Create(&link).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := seedFlags(tx, nonstock.Id, entity.FlagableTypeNonstock, seed.Flags); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func seedFlags(tx *gorm.DB, flagableID uint, flagableType string, flags []utils.FlagType) error {
|
||||
if len(flags) == 0 {
|
||||
return nil
|
||||
}
|
||||
for _, flag := range flags {
|
||||
name := strings.ToUpper(string(flag))
|
||||
var existing entity.Flag
|
||||
err := tx.Where("name = ? AND flagable_id = ? AND flagable_type = ?", name, flagableID, flagableType).First(&existing).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
record := entity.Flag{
|
||||
Name: name,
|
||||
FlagableID: flagableID,
|
||||
FlagableType: flagableType,
|
||||
}
|
||||
if err := tx.Create(&record).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func seedBanks(tx *gorm.DB, createdBy uint) error {
|
||||
seeds := []struct {
|
||||
Name string
|
||||
Alias string
|
||||
Owner *string
|
||||
AccountNumber string
|
||||
}{
|
||||
{
|
||||
Name: "Bank Central Asia",
|
||||
Alias: "BCA",
|
||||
AccountNumber: "1234567890",
|
||||
Owner: ptr("PT MBU Group"),
|
||||
},
|
||||
{
|
||||
Name: "Bank Rakyat Indonesia",
|
||||
Alias: "BRI",
|
||||
AccountNumber: "9876543210",
|
||||
Owner: ptr("PT MBU Group"),
|
||||
},
|
||||
{
|
||||
Name: "Bank Mandiri",
|
||||
Alias: "MAND",
|
||||
AccountNumber: "1122334455",
|
||||
Owner: ptr("PT MBU Group"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, seed := range seeds {
|
||||
var bank entity.Bank
|
||||
err := tx.Where("name = ?", seed.Name).First(&bank).Error
|
||||
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
bank = entity.Bank{
|
||||
Name: seed.Name,
|
||||
Alias: seed.Alias,
|
||||
Owner: seed.Owner,
|
||||
AccountNumber: seed.AccountNumber,
|
||||
CreatedBy: createdBy,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
if err := tx.Create(&bank).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err != nil {
|
||||
return err
|
||||
} else {
|
||||
// update data jika sudah ada
|
||||
if err := tx.Model(&entity.Bank{}).Where("id = ?", bank.Id).Updates(map[string]any{
|
||||
"alias": seed.Alias,
|
||||
"owner": seed.Owner,
|
||||
"account_number": seed.AccountNumber,
|
||||
"updated_at": time.Now(),
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ptr[T any](v T) *T {
|
||||
return &v
|
||||
}
|
||||
|
||||
func strPtr(s string) *string {
|
||||
return &s
|
||||
}
|
||||
|
||||
func intPtr(v int) *int {
|
||||
return &v
|
||||
}
|
||||
|
||||
func uintPtr(v uint) *uint {
|
||||
return &v
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Area struct {
|
||||
Id uint `gorm:"primaryKey"`
|
||||
Name string `gorm:"not null;uniqueIndex:areas_name_unique,where:deleted_at IS NULL"`
|
||||
CreatedBy uint `gorm:"not null"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||
Locations []Location `gorm:"foreignKey:AreaId;references:Id"`
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Bank struct {
|
||||
Id uint `gorm:"primaryKey"`
|
||||
Name string `gorm:"not null;uniqueIndex:banks_name_unique,where:deleted_at IS NULL"`
|
||||
Alias string `gorm:"not null;size:5"`
|
||||
Owner *string `gorm:""`
|
||||
AccountNumber string `gorm:"not null;size:50"`
|
||||
CreatedBy uint `gorm:"not null"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Constant struct {
|
||||
Id uint `gorm:"primaryKey"`
|
||||
Name string `gorm:"not null;uniqueIndex:idx_suppliers_name,where:deleted_at IS NULL"`
|
||||
CreatedBy uint `gorm:"not null"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Customer struct {
|
||||
Id uint `gorm:"primaryKey"`
|
||||
Name string `gorm:"not null;uniqueIndex:customers_name_unique,where:deleted_at IS NULL"`
|
||||
PicId uint `gorm:"not null"`
|
||||
Type string `gorm:"not null;size:50"`
|
||||
Address string `gorm:"not null"`
|
||||
Phone string `gorm:"not null;size:20"`
|
||||
Email string `gorm:"not null"`
|
||||
AccountNumber string `gorm:"not null;size:50"`
|
||||
Balance float64 `gorm:"default:0"`
|
||||
CreatedBy uint `gorm:"not null"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||
Pic User `gorm:"foreignKey:PicId;references:Id"`
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Fcr struct {
|
||||
Id uint `gorm:"primaryKey"`
|
||||
Name string `gorm:"not null;uniqueIndex:idx_suppliers_name,where:deleted_at IS NULL"`
|
||||
CreatedBy uint `gorm:"not null"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||
Standards []FcrStandard `gorm:"foreignKey:FcrID;references:Id"`
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type FcrStandard struct {
|
||||
Id uint `gorm:"primaryKey"`
|
||||
FcrID uint `gorm:"not null;index"`
|
||||
Weight float64 `gorm:"type:numeric(15,3);not null"`
|
||||
FcrNumber float64 `gorm:"type:numeric(15,3);not null"`
|
||||
Mortality float64 `gorm:"type:numeric(15,3);not null"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
Fcr Fcr `gorm:"foreignKey:FcrID;references:Id"`
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package entities
|
||||
|
||||
import "time"
|
||||
|
||||
const (
|
||||
FlagableTypeProduct = "products"
|
||||
FlagableTypeNonstock = "nonstocks"
|
||||
)
|
||||
|
||||
type Flag struct {
|
||||
Id uint `gorm:"primaryKey"`
|
||||
Name string `gorm:"not null;uniqueIndex:flags_unique_flagable"`
|
||||
FlagableID uint `gorm:"not null;uniqueIndex:flags_unique_flagable;index:flags_flagable_lookup,priority:2"`
|
||||
FlagableType string `gorm:"size:50;not null;uniqueIndex:flags_unique_flagable;index:flags_flagable_lookup,priority:1"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Kandang struct {
|
||||
Id uint `gorm:"primaryKey"`
|
||||
Name string `gorm:"not null;uniqueIndex:kandangs_name_unique,where:deleted_at IS NULL"`
|
||||
LocationId uint `gorm:"not null"`
|
||||
PicId uint `gorm:"not null"`
|
||||
CreatedBy uint `gorm:"not null"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||
Location Location `gorm:"foreignKey:LocationId;references:Id"`
|
||||
Pic User `gorm:"foreignKey:PicId;references:Id"`
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Location struct {
|
||||
Id uint `gorm:"primaryKey"`
|
||||
Name string `gorm:"not null;uniqueIndex:locations_name_unique,where:deleted_at IS NULL"`
|
||||
Address string `gorm:"not null"`
|
||||
AreaId uint `gorm:"not null"`
|
||||
CreatedBy uint `gorm:"not null"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||
Area Area `gorm:"foreignKey:AreaId;references:Id"`
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Nonstock struct {
|
||||
Id uint `gorm:"primaryKey"`
|
||||
Name string `gorm:"not null;uniqueIndex:nonstocks_name_unique,where:deleted_at IS NULL"`
|
||||
UomId uint `gorm:"not null"`
|
||||
CreatedBy uint `gorm:"not null"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||
Uom Uom `gorm:"foreignKey:UomId;references:Id"`
|
||||
Suppliers []Supplier `gorm:"many2many:nonstock_suppliers;joinForeignKey:NonstockID;joinReferences:SupplierID"`
|
||||
Flags []Flag `gorm:"polymorphic:Flagable;polymorphicValue:nonstocks"`
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package entities
|
||||
|
||||
import "time"
|
||||
|
||||
type NonstockSupplier struct {
|
||||
NonstockID uint `gorm:"primaryKey"`
|
||||
SupplierID uint `gorm:"primaryKey"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ProductCategory struct {
|
||||
Id uint `gorm:"primaryKey"`
|
||||
Name string `gorm:"not null;uniqueIndex:product_categories_name_unique,where:deleted_at IS NULL"`
|
||||
Code string `gorm:"not null;size:10;uniqueIndex:product_categories_code_unique,where:deleted_at IS NULL"`
|
||||
CreatedBy uint `gorm:"not null"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Product struct {
|
||||
Id uint `gorm:"primaryKey"`
|
||||
Name string `gorm:"not null;uniqueIndex:products_name_unique,where:deleted_at IS NULL"`
|
||||
Brand string `gorm:"not null"`
|
||||
Sku *string `gorm:"size:100;uniqueIndex:products_sku_unique,where:deleted_at IS NULL"`
|
||||
UomId uint `gorm:"not null"`
|
||||
ProductCategoryId uint `gorm:"not null"`
|
||||
ProductPrice float64 `gorm:"type:numeric(15,3);not null"`
|
||||
SellingPrice *float64 `gorm:"type:numeric(15,3)"`
|
||||
Tax *float64 `gorm:"type:numeric(15,3)"`
|
||||
ExpiryPeriod *int `gorm:""`
|
||||
CreatedBy uint `gorm:"not null"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||
Uom Uom `gorm:"foreignKey:UomId;references:Id"`
|
||||
ProductCategory ProductCategory `gorm:"foreignKey:ProductCategoryId;references:Id"`
|
||||
Suppliers []Supplier `gorm:"many2many:product_suppliers;joinForeignKey:ProductID;joinReferences:SupplierID"`
|
||||
Flags []Flag `gorm:"polymorphic:Flagable;polymorphicValue:products"`
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package entities
|
||||
|
||||
import "time"
|
||||
|
||||
type ProductSupplier struct {
|
||||
ProductID uint `gorm:"primaryKey"`
|
||||
SupplierID uint `gorm:"primaryKey"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Supplier struct {
|
||||
Id uint `gorm:"primaryKey"`
|
||||
Name string `gorm:"not null;uniqueIndex:suppliers_name_unique,where:deleted_at IS NULL"`
|
||||
Alias string `gorm:"not null;size:5"`
|
||||
Pic string `gorm:"not null"`
|
||||
Type string `gorm:"not null;size:50"`
|
||||
Category string `gorm:"not null;size:20"`
|
||||
Hatchery *string `gorm:"size:255"`
|
||||
Phone string `gorm:"not null;size:20"`
|
||||
Email string `gorm:"not null"`
|
||||
Address string `gorm:"not null"`
|
||||
Npwp *string `gorm:"size:50"`
|
||||
AccountNumber *string `gorm:"size:50"`
|
||||
Balance float64 `gorm:"type:numeric(15,3);default:0"`
|
||||
DueDate int `gorm:"not null"`
|
||||
CreatedBy uint `gorm:"not null"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Uom struct {
|
||||
Id uint `gorm:"primaryKey"`
|
||||
Name string `gorm:"not null;uniqueIndex:uoms_name_unique,where:deleted_at IS NULL"`
|
||||
CreatedBy uint `gorm:"not null"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
Id uint `gorm:"primaryKey"`
|
||||
IdUser int64 `gorm:"uniqueIndex"`
|
||||
Email string `gorm:"uniqueIndex"`
|
||||
Name string `gorm:"not null"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Warehouse struct {
|
||||
Id uint `gorm:"primaryKey"`
|
||||
Name string `gorm:"not null"`
|
||||
Type string `gorm:"not null"`
|
||||
AreaId uint `gorm:"not null"`
|
||||
LocationId *uint
|
||||
KandangId *uint
|
||||
CreatedBy uint `gorm:"not null"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||
Area Area `gorm:"foreignKey:AreaId;references:Id"`
|
||||
Location *Location `gorm:"foreignKey:LocationId;references:Id"`
|
||||
Kandang *Kandang `gorm:"foreignKey:KandangId;references:Id"`
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/config"
|
||||
service "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/sso"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func Auth(userService service.UserService, requiredRights ...string) fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
authHeader := c.Get("Authorization")
|
||||
token := strings.TrimSpace(strings.TrimPrefix(authHeader, "Bearer "))
|
||||
|
||||
if token == "" {
|
||||
cookieName := config.SSOAccessCookieName
|
||||
if cookieName == "" {
|
||||
cookieName = "access"
|
||||
}
|
||||
token = strings.TrimSpace(c.Cookies(cookieName))
|
||||
}
|
||||
|
||||
if token == "" {
|
||||
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
||||
}
|
||||
|
||||
verification, err := sso.VerifyAccessToken(token)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
||||
}
|
||||
|
||||
if len(config.SSOAllowedAudiences) > 0 {
|
||||
allowed := make(map[string]struct{}, len(config.SSOAllowedAudiences))
|
||||
for _, aud := range config.SSOAllowedAudiences {
|
||||
aud = strings.TrimSpace(aud)
|
||||
if aud != "" {
|
||||
allowed[aud] = struct{}{}
|
||||
}
|
||||
}
|
||||
audienceValid := false
|
||||
for _, aud := range verification.Claims.Audience {
|
||||
if _, ok := allowed[aud]; ok {
|
||||
audienceValid = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !audienceValid {
|
||||
return fiber.NewError(fiber.StatusUnauthorized, "invalid audience")
|
||||
}
|
||||
}
|
||||
|
||||
user, err := userService.GetBySSOUserID(c, verification.UserID)
|
||||
if err != nil || user == nil {
|
||||
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
||||
}
|
||||
|
||||
c.Locals("user", user)
|
||||
c.Locals("token_claims", verification.Claims)
|
||||
|
||||
// if len(requiredRights) > 0 {
|
||||
// userRights, hasRights := config.RoleRights[user.Role]
|
||||
// if (!hasRights || !hasAllRights(userRights, requiredRights)) && c.Params("userId") != userID {
|
||||
// return fiber.NewError(fiber.StatusForbidden, "You don't have permission to access this resource")
|
||||
// }
|
||||
// }
|
||||
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// func hasAllRights(userRights, requiredRights []string) bool {
|
||||
// rightSet := make(map[string]struct{}, len(userRights))
|
||||
// for _, right := range userRights {
|
||||
// rightSet[right] = struct{}{}
|
||||
// }
|
||||
|
||||
// for _, right := range requiredRights {
|
||||
// if _, exists := rightSet[right]; !exists {
|
||||
// return false
|
||||
// }
|
||||
// }
|
||||
// return true
|
||||
// }
|
||||
@@ -0,0 +1,12 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
jwtware "github.com/gofiber/contrib/jwt"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func JwtConfig() fiber.Handler {
|
||||
return jwtware.New(jwtware.Config{
|
||||
SigningKey: jwtware.SigningKey{Key: []byte("secret")},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/response"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/limiter"
|
||||
)
|
||||
|
||||
func LimiterConfig() fiber.Handler {
|
||||
return limiter.New(limiter.Config{
|
||||
Max: 20,
|
||||
Expiration: 15 * time.Minute,
|
||||
LimitReached: func(c *fiber.Ctx) error {
|
||||
return c.Status(fiber.StatusTooManyRequests).
|
||||
JSON(response.Common{
|
||||
Code: fiber.StatusTooManyRequests,
|
||||
Status: "error",
|
||||
Message: "Too many requests, please try again later",
|
||||
})
|
||||
},
|
||||
SkipSuccessfulRequests: true,
|
||||
})
|
||||
}
|
||||
|
||||
func NewLimiter(max int, expiration time.Duration) fiber.Handler {
|
||||
if max <= 0 {
|
||||
max = 10
|
||||
}
|
||||
if expiration <= 0 {
|
||||
expiration = time.Minute
|
||||
}
|
||||
return limiter.New(limiter.Config{
|
||||
Max: max,
|
||||
Expiration: expiration,
|
||||
LimitReached: func(c *fiber.Ctx) error {
|
||||
return c.Status(fiber.StatusTooManyRequests).
|
||||
JSON(response.Common{
|
||||
Code: fiber.StatusTooManyRequests,
|
||||
Status: "error",
|
||||
Message: "Too many requests, please try again later",
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/logger"
|
||||
)
|
||||
|
||||
func LoggerConfig() fiber.Handler {
|
||||
return logger.New(logger.Config{
|
||||
Format: "${time} ${method} ${status} ${path} in ${latency}\n",
|
||||
TimeFormat: "15:04:05.00",
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/recover"
|
||||
)
|
||||
|
||||
func RecoverConfig() fiber.Handler {
|
||||
return recover.New(recover.Config{
|
||||
EnableStackTrace: true,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package trim
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
// JSONBody trims whitespace from string fields in JSON request bodies.
|
||||
func JSONBody() fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
contentType := c.Get(fiber.HeaderContentType)
|
||||
if !strings.Contains(contentType, fiber.MIMEApplicationJSON) {
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
body := c.Body()
|
||||
if len(body) == 0 {
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
var payload any
|
||||
if err := json.Unmarshal(body, &payload); err != nil {
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
trimStrings(payload)
|
||||
|
||||
buf := bytes.Buffer{}
|
||||
encoder := json.NewEncoder(&buf)
|
||||
encoder.SetEscapeHTML(false)
|
||||
if err := encoder.Encode(payload); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
trimmedBody := bytes.TrimSpace(buf.Bytes())
|
||||
c.Request().SetBody(trimmedBody)
|
||||
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func trimStrings(value any) {
|
||||
switch v := value.(type) {
|
||||
case map[string]any:
|
||||
for key, val := range v {
|
||||
if str, ok := val.(string); ok {
|
||||
v[key] = strings.TrimSpace(str)
|
||||
continue
|
||||
}
|
||||
trimStrings(val)
|
||||
}
|
||||
case []any:
|
||||
for i, elem := range v {
|
||||
if str, ok := elem.(string); ok {
|
||||
v[i] = strings.TrimSpace(str)
|
||||
continue
|
||||
}
|
||||
trimStrings(elem)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
service "gitlab.com/mbugroup/lti-api.git/internal/modules/constants/services"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type ConstantController struct {
|
||||
ConstantService service.ConstantService
|
||||
}
|
||||
|
||||
func NewConstantController(constantService service.ConstantService) *ConstantController {
|
||||
return &ConstantController{
|
||||
ConstantService: constantService,
|
||||
}
|
||||
}
|
||||
|
||||
func (ctrl *ConstantController) GetAll(c *fiber.Ctx) error {
|
||||
data, err := ctrl.ConstantService.GetAll(c)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
return c.Status(fiber.StatusOK).JSON(data)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package constants
|
||||
|
||||
import (
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"gorm.io/gorm"
|
||||
|
||||
rConstant "gitlab.com/mbugroup/lti-api.git/internal/modules/constants/repositories"
|
||||
sConstant "gitlab.com/mbugroup/lti-api.git/internal/modules/constants/services"
|
||||
)
|
||||
|
||||
type ConstantModule struct{}
|
||||
|
||||
func (ConstantModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||
constantRepo := rConstant.NewConstantRepository(db)
|
||||
|
||||
constantService := sConstant.NewConstantService(constantRepo, validate)
|
||||
|
||||
ConstantRoutes(router, constantService)
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ConstantRepository interface {
|
||||
GetConstants() map[string]interface{}
|
||||
}
|
||||
|
||||
type ConstantRepositoryImpl struct {
|
||||
*repository.BaseRepositoryImpl[entity.Constant]
|
||||
}
|
||||
|
||||
func NewConstantRepository(db *gorm.DB) ConstantRepository {
|
||||
return &ConstantRepositoryImpl{
|
||||
BaseRepositoryImpl: repository.NewBaseRepository[entity.Constant](db),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ConstantRepositoryImpl) GetConstants() map[string]interface{} {
|
||||
flagList := make([]string, 0)
|
||||
for f := range utils.AllFlagTypes() {
|
||||
flagList = append(flagList, string(f))
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"flags": flagList,
|
||||
"warehouse_types": []string{
|
||||
"AREA",
|
||||
"LOKASI",
|
||||
"KANDANG",
|
||||
},
|
||||
"supplier_categories": []string{
|
||||
"BOP",
|
||||
"SAPRONAK",
|
||||
},
|
||||
"customer_supplier_types": []string{
|
||||
"BISNIS",
|
||||
"INDIVIDUAL",
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package constants
|
||||
|
||||
import (
|
||||
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/constants/controllers"
|
||||
constant "gitlab.com/mbugroup/lti-api.git/internal/modules/constants/services"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func ConstantRoutes(v1 fiber.Router, s constant.ConstantService) {
|
||||
ctrl := controller.NewConstantController(s)
|
||||
|
||||
route := v1.Group("/constants")
|
||||
|
||||
route.Get("/", ctrl.GetAll)
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/constants/repositories"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type ConstantService interface {
|
||||
GetAll(ctx *fiber.Ctx) (map[string]interface{}, error)
|
||||
}
|
||||
|
||||
type constantService struct {
|
||||
Repository repository.ConstantRepository
|
||||
}
|
||||
|
||||
func NewConstantService(repo repository.ConstantRepository, validate *validator.Validate) ConstantService {
|
||||
return &constantService{
|
||||
Repository: repo,
|
||||
}
|
||||
}
|
||||
|
||||
func (s constantService) GetAll(c *fiber.Ctx) (map[string]interface{}, error) {
|
||||
return s.Repository.GetConstants(), nil
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strconv"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/dto"
|
||||
service "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/services"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/validations"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/response"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type AreaController struct {
|
||||
AreaService service.AreaService
|
||||
}
|
||||
|
||||
func NewAreaController(areaService service.AreaService) *AreaController {
|
||||
return &AreaController{
|
||||
AreaService: areaService,
|
||||
}
|
||||
}
|
||||
|
||||
func (u *AreaController) GetAll(c *fiber.Ctx) error {
|
||||
query := &validation.Query{
|
||||
Page: c.QueryInt("page", 1),
|
||||
Limit: c.QueryInt("limit", 10),
|
||||
Search: c.Query("search", ""),
|
||||
}
|
||||
|
||||
result, totalResults, err := u.AreaService.GetAll(c, query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.SuccessWithPaginate[dto.AreaListDTO]{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Get all areas successfully",
|
||||
Meta: response.Meta{
|
||||
Page: query.Page,
|
||||
Limit: query.Limit,
|
||||
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
|
||||
TotalResults: totalResults,
|
||||
},
|
||||
Data: dto.ToAreaListDTOs(result),
|
||||
})
|
||||
}
|
||||
|
||||
func (u *AreaController) GetOne(c *fiber.Ctx) error {
|
||||
param := c.Params("id")
|
||||
|
||||
id, err := strconv.Atoi(param)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||
}
|
||||
|
||||
result, err := u.AreaService.GetOne(c, uint(id))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.Success{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Get area successfully",
|
||||
Data: dto.ToAreaListDTO(*result),
|
||||
})
|
||||
}
|
||||
|
||||
func (u *AreaController) CreateOne(c *fiber.Ctx) error {
|
||||
req := new(validation.Create)
|
||||
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||
}
|
||||
|
||||
result, err := u.AreaService.CreateOne(c, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusCreated).
|
||||
JSON(response.Success{
|
||||
Code: fiber.StatusCreated,
|
||||
Status: "success",
|
||||
Message: "Create area successfully",
|
||||
Data: dto.ToAreaListDTO(*result),
|
||||
})
|
||||
}
|
||||
|
||||
func (u *AreaController) UpdateOne(c *fiber.Ctx) error {
|
||||
req := new(validation.Update)
|
||||
param := c.Params("id")
|
||||
|
||||
id, err := strconv.Atoi(param)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||
}
|
||||
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||
}
|
||||
|
||||
result, err := u.AreaService.UpdateOne(c, req, uint(id))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.Success{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Update area successfully",
|
||||
Data: dto.ToAreaListDTO(*result),
|
||||
})
|
||||
}
|
||||
|
||||
func (u *AreaController) DeleteOne(c *fiber.Ctx) error {
|
||||
param := c.Params("id")
|
||||
|
||||
id, err := strconv.Atoi(param)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||
}
|
||||
|
||||
if err := u.AreaService.DeleteOne(c, uint(id)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.Common{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Delete area successfully",
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
||||
)
|
||||
|
||||
// === DTO Structs ===
|
||||
|
||||
type AreaBaseDTO struct {
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type AreaListDTO struct {
|
||||
AreaBaseDTO
|
||||
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type AreaDetailDTO struct {
|
||||
AreaListDTO
|
||||
}
|
||||
|
||||
// === Mapper Functions ===
|
||||
|
||||
func ToAreaBaseDTO(e entity.Area) AreaBaseDTO {
|
||||
return AreaBaseDTO{
|
||||
Id: e.Id,
|
||||
Name: e.Name,
|
||||
}
|
||||
}
|
||||
|
||||
func ToAreaListDTO(e entity.Area) AreaListDTO {
|
||||
var createdUser *userDTO.UserBaseDTO
|
||||
if e.CreatedUser.Id != 0 {
|
||||
mapped := userDTO.ToUserBaseDTO(e.CreatedUser)
|
||||
createdUser = &mapped
|
||||
}
|
||||
|
||||
return AreaListDTO{
|
||||
AreaBaseDTO: ToAreaBaseDTO(e),
|
||||
CreatedUser: createdUser,
|
||||
CreatedAt: e.CreatedAt,
|
||||
UpdatedAt: e.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func ToAreaListDTOs(e []entity.Area) []AreaListDTO {
|
||||
result := make([]AreaListDTO, len(e))
|
||||
for i, r := range e {
|
||||
result[i] = ToAreaListDTO(r)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func ToAreaDetailDTO(e entity.Area) AreaDetailDTO {
|
||||
return AreaDetailDTO{
|
||||
AreaListDTO: ToAreaListDTO(e),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package areas
|
||||
|
||||
import (
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"gorm.io/gorm"
|
||||
|
||||
rArea "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/repositories"
|
||||
sArea "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/services"
|
||||
|
||||
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||
)
|
||||
|
||||
type AreaModule struct{}
|
||||
|
||||
func (AreaModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||
areaRepo := rArea.NewAreaRepository(db)
|
||||
userRepo := rUser.NewUserRepository(db)
|
||||
|
||||
areaService := sArea.NewAreaService(areaRepo, validate)
|
||||
userService := sUser.NewUserService(userRepo, validate)
|
||||
|
||||
AreaRoutes(router, userService, areaService)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type AreaRepository interface {
|
||||
repository.BaseRepository[entity.Area]
|
||||
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
|
||||
}
|
||||
|
||||
type AreaRepositoryImpl struct {
|
||||
*repository.BaseRepositoryImpl[entity.Area]
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewAreaRepository(db *gorm.DB) AreaRepository {
|
||||
return &AreaRepositoryImpl{
|
||||
BaseRepositoryImpl: repository.NewBaseRepository[entity.Area](db),
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *AreaRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) {
|
||||
return repository.ExistsByName[entity.Area](ctx, r.db, name, excludeID)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package areas
|
||||
|
||||
import (
|
||||
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/controllers"
|
||||
area "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/services"
|
||||
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func AreaRoutes(v1 fiber.Router, u user.UserService, s area.AreaService) {
|
||||
ctrl := controller.NewAreaController(s)
|
||||
|
||||
route := v1.Group("/areas")
|
||||
|
||||
// route.Get("/", m.Auth(u), ctrl.GetAll)
|
||||
// route.Post("/", m.Auth(u), ctrl.CreateOne)
|
||||
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
|
||||
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
|
||||
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
|
||||
|
||||
route.Get("/", ctrl.GetAll)
|
||||
route.Post("/", ctrl.CreateOne)
|
||||
route.Get("/:id", ctrl.GetOne)
|
||||
route.Patch("/:id", ctrl.UpdateOne)
|
||||
route.Delete("/:id", ctrl.DeleteOne)
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/repositories"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/validations"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type AreaService interface {
|
||||
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.Area, int64, error)
|
||||
GetOne(ctx *fiber.Ctx, id uint) (*entity.Area, error)
|
||||
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Area, error)
|
||||
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Area, error)
|
||||
DeleteOne(ctx *fiber.Ctx, id uint) error
|
||||
}
|
||||
|
||||
type areaService struct {
|
||||
Log *logrus.Logger
|
||||
Validate *validator.Validate
|
||||
Repository repository.AreaRepository
|
||||
}
|
||||
|
||||
func NewAreaService(repo repository.AreaRepository, validate *validator.Validate) AreaService {
|
||||
return &areaService{
|
||||
Log: utils.Log,
|
||||
Validate: validate,
|
||||
Repository: repo,
|
||||
}
|
||||
}
|
||||
|
||||
func (s areaService) withRelations(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("CreatedUser")
|
||||
}
|
||||
|
||||
func (s areaService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Area, int64, error) {
|
||||
if err := s.Validate.Struct(params); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
offset := (params.Page - 1) * params.Limit
|
||||
|
||||
areas, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
||||
db = s.withRelations(db)
|
||||
if params.Search != "" {
|
||||
return db.Where("name LIKE ?", "%"+params.Search+"%")
|
||||
}
|
||||
return db.Order("created_at DESC").Order("updated_at DESC")
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to get areas: %+v", err)
|
||||
return nil, 0, err
|
||||
}
|
||||
return areas, total, nil
|
||||
}
|
||||
|
||||
func (s areaService) GetOne(c *fiber.Ctx, id uint) (*entity.Area, error) {
|
||||
area, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Area not found")
|
||||
}
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed get area by id: %+v", err)
|
||||
return nil, err
|
||||
}
|
||||
return area, nil
|
||||
}
|
||||
|
||||
func (s *areaService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Area, error) {
|
||||
if err := s.Validate.Struct(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if exists, err := s.Repository.NameExists(c.Context(), req.Name, nil); err != nil {
|
||||
s.Log.Errorf("Failed to check area name: %+v", err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check area name")
|
||||
} else if exists {
|
||||
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Area with name %s already exists", req.Name))
|
||||
}
|
||||
|
||||
//TODO: created by dummy
|
||||
createBody := &entity.Area{
|
||||
Name: req.Name,
|
||||
CreatedBy: 1,
|
||||
}
|
||||
|
||||
if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil {
|
||||
s.Log.Errorf("Failed to create area: %+v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.GetOne(c, createBody.Id)
|
||||
}
|
||||
|
||||
func (s areaService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Area, error) {
|
||||
if err := s.Validate.Struct(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
updateBody := make(map[string]any)
|
||||
|
||||
if req.Name != nil {
|
||||
if exists, err := s.Repository.NameExists(c.Context(), *req.Name, &id); err != nil {
|
||||
s.Log.Errorf("Failed to check area name: %+v", err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check area name")
|
||||
} else if exists {
|
||||
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Area with name %s already exists", *req.Name))
|
||||
}
|
||||
updateBody["name"] = *req.Name
|
||||
}
|
||||
|
||||
if len(updateBody) == 0 {
|
||||
return s.GetOne(c, id)
|
||||
}
|
||||
|
||||
if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Area not found")
|
||||
}
|
||||
s.Log.Errorf("Failed to update area: %+v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.GetOne(c, id)
|
||||
}
|
||||
|
||||
func (s areaService) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||
if err := s.Repository.DeleteOne(c.Context(), id); err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(fiber.StatusNotFound, "Area not found")
|
||||
}
|
||||
s.Log.Errorf("Failed to delete area: %+v", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package validation
|
||||
|
||||
type Create struct {
|
||||
Name string `json:"name" validate:"required_strict,min=3"`
|
||||
}
|
||||
|
||||
type Update struct {
|
||||
Name *string `json:"name,omitempty" validate:"omitempty"`
|
||||
}
|
||||
|
||||
type Query struct {
|
||||
Page int `query:"page" validate:"omitempty,number,min=1"`
|
||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
|
||||
Search string `query:"search" validate:"omitempty,max=50"`
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strconv"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/master/banks/dto"
|
||||
service "gitlab.com/mbugroup/lti-api.git/internal/modules/master/banks/services"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/banks/validations"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/response"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type BankController struct {
|
||||
BankService service.BankService
|
||||
}
|
||||
|
||||
func NewBankController(bankService service.BankService) *BankController {
|
||||
return &BankController{
|
||||
BankService: bankService,
|
||||
}
|
||||
}
|
||||
|
||||
func (u *BankController) GetAll(c *fiber.Ctx) error {
|
||||
query := &validation.Query{
|
||||
Page: c.QueryInt("page", 1),
|
||||
Limit: c.QueryInt("limit", 10),
|
||||
Search: c.Query("search", ""),
|
||||
}
|
||||
|
||||
result, totalResults, err := u.BankService.GetAll(c, query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.SuccessWithPaginate[dto.BankListDTO]{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Get all banks successfully",
|
||||
Meta: response.Meta{
|
||||
Page: query.Page,
|
||||
Limit: query.Limit,
|
||||
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
|
||||
TotalResults: totalResults,
|
||||
},
|
||||
Data: dto.ToBankListDTOs(result),
|
||||
})
|
||||
}
|
||||
|
||||
func (u *BankController) GetOne(c *fiber.Ctx) error {
|
||||
param := c.Params("id")
|
||||
|
||||
id, err := strconv.Atoi(param)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||
}
|
||||
|
||||
result, err := u.BankService.GetOne(c, uint(id))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.Success{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Get bank successfully",
|
||||
Data: dto.ToBankListDTO(*result),
|
||||
})
|
||||
}
|
||||
|
||||
func (u *BankController) CreateOne(c *fiber.Ctx) error {
|
||||
req := new(validation.Create)
|
||||
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||
}
|
||||
|
||||
result, err := u.BankService.CreateOne(c, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusCreated).
|
||||
JSON(response.Success{
|
||||
Code: fiber.StatusCreated,
|
||||
Status: "success",
|
||||
Message: "Create bank successfully",
|
||||
Data: dto.ToBankListDTO(*result),
|
||||
})
|
||||
}
|
||||
|
||||
func (u *BankController) UpdateOne(c *fiber.Ctx) error {
|
||||
req := new(validation.Update)
|
||||
param := c.Params("id")
|
||||
|
||||
id, err := strconv.Atoi(param)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||
}
|
||||
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||
}
|
||||
|
||||
result, err := u.BankService.UpdateOne(c, req, uint(id))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.Success{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Update bank successfully",
|
||||
Data: dto.ToBankListDTO(*result),
|
||||
})
|
||||
}
|
||||
|
||||
func (u *BankController) DeleteOne(c *fiber.Ctx) error {
|
||||
param := c.Params("id")
|
||||
|
||||
id, err := strconv.Atoi(param)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||
}
|
||||
|
||||
if err := u.BankService.DeleteOne(c, uint(id)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.Common{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Delete bank successfully",
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
||||
)
|
||||
|
||||
// === DTO Structs ===
|
||||
|
||||
type BankBaseDTO struct {
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Alias string `json:"alias"`
|
||||
Owner *string `json:"owner"`
|
||||
AccountNumber string `json:"account_number"`
|
||||
}
|
||||
|
||||
type BankListDTO struct {
|
||||
BankBaseDTO
|
||||
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type BankDetailDTO struct {
|
||||
BankListDTO
|
||||
}
|
||||
|
||||
// === Mapper Functions ===
|
||||
|
||||
func ToBankBaseDTO(e entity.Bank) BankBaseDTO {
|
||||
return BankBaseDTO{
|
||||
Id: e.Id,
|
||||
Name: e.Name,
|
||||
Alias: e.Alias,
|
||||
Owner: e.Owner,
|
||||
AccountNumber: e.AccountNumber,
|
||||
}
|
||||
}
|
||||
|
||||
func ToBankListDTO(e entity.Bank) BankListDTO {
|
||||
var createdUser *userDTO.UserBaseDTO
|
||||
if e.CreatedUser.Id != 0 {
|
||||
mapped := userDTO.ToUserBaseDTO(e.CreatedUser)
|
||||
createdUser = &mapped
|
||||
}
|
||||
|
||||
return BankListDTO{
|
||||
BankBaseDTO: ToBankBaseDTO(e),
|
||||
CreatedAt: e.CreatedAt,
|
||||
UpdatedAt: e.UpdatedAt,
|
||||
CreatedUser: createdUser,
|
||||
}
|
||||
}
|
||||
|
||||
func ToBankListDTOs(e []entity.Bank) []BankListDTO {
|
||||
result := make([]BankListDTO, len(e))
|
||||
for i, r := range e {
|
||||
result[i] = ToBankListDTO(r)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func ToBankDetailDTO(e entity.Bank) BankDetailDTO {
|
||||
return BankDetailDTO{
|
||||
BankListDTO: ToBankListDTO(e),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package banks
|
||||
|
||||
import (
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"gorm.io/gorm"
|
||||
|
||||
rBank "gitlab.com/mbugroup/lti-api.git/internal/modules/master/banks/repositories"
|
||||
sBank "gitlab.com/mbugroup/lti-api.git/internal/modules/master/banks/services"
|
||||
|
||||
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||
)
|
||||
|
||||
type BankModule struct{}
|
||||
|
||||
func (BankModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||
bankRepo := rBank.NewBankRepository(db)
|
||||
userRepo := rUser.NewUserRepository(db)
|
||||
|
||||
bankService := sBank.NewBankService(bankRepo, validate)
|
||||
userService := sUser.NewUserService(userRepo, validate)
|
||||
|
||||
BankRoutes(router, userService, bankService)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type BankRepository interface {
|
||||
repository.BaseRepository[entity.Bank]
|
||||
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
|
||||
}
|
||||
|
||||
type BankRepositoryImpl struct {
|
||||
*repository.BaseRepositoryImpl[entity.Bank]
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewBankRepository(db *gorm.DB) BankRepository {
|
||||
return &BankRepositoryImpl{
|
||||
BaseRepositoryImpl: repository.NewBaseRepository[entity.Bank](db),
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *BankRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) {
|
||||
return repository.ExistsByName[entity.Bank](ctx, r.db, name, excludeID)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package banks
|
||||
|
||||
import (
|
||||
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/banks/controllers"
|
||||
bank "gitlab.com/mbugroup/lti-api.git/internal/modules/master/banks/services"
|
||||
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func BankRoutes(v1 fiber.Router, u user.UserService, s bank.BankService) {
|
||||
ctrl := controller.NewBankController(s)
|
||||
|
||||
route := v1.Group("/banks")
|
||||
|
||||
// route.Get("/", m.Auth(u), ctrl.GetAll)
|
||||
// route.Post("/", m.Auth(u), ctrl.CreateOne)
|
||||
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
|
||||
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
|
||||
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
|
||||
|
||||
route.Get("/", ctrl.GetAll)
|
||||
route.Post("/", ctrl.CreateOne)
|
||||
route.Get("/:id", ctrl.GetOne)
|
||||
route.Patch("/:id", ctrl.UpdateOne)
|
||||
route.Delete("/:id", ctrl.DeleteOne)
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/banks/repositories"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/banks/validations"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type BankService interface {
|
||||
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.Bank, int64, error)
|
||||
GetOne(ctx *fiber.Ctx, id uint) (*entity.Bank, error)
|
||||
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Bank, error)
|
||||
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Bank, error)
|
||||
DeleteOne(ctx *fiber.Ctx, id uint) error
|
||||
}
|
||||
|
||||
type bankService struct {
|
||||
Log *logrus.Logger
|
||||
Validate *validator.Validate
|
||||
Repository repository.BankRepository
|
||||
}
|
||||
|
||||
func NewBankService(repo repository.BankRepository, validate *validator.Validate) BankService {
|
||||
return &bankService{
|
||||
Log: utils.Log,
|
||||
Validate: validate,
|
||||
Repository: repo,
|
||||
}
|
||||
}
|
||||
|
||||
func (s bankService) withRelations(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("CreatedUser")
|
||||
}
|
||||
|
||||
func (s bankService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Bank, int64, error) {
|
||||
if err := s.Validate.Struct(params); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
offset := (params.Page - 1) * params.Limit
|
||||
|
||||
banks, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
||||
db = s.withRelations(db)
|
||||
if params.Search != "" {
|
||||
return db.Where("name LIKE ?", "%"+params.Search+"%")
|
||||
}
|
||||
return db.Order("created_at DESC").Order("updated_at DESC")
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to get banks: %+v", err)
|
||||
return nil, 0, err
|
||||
}
|
||||
return banks, total, nil
|
||||
}
|
||||
|
||||
func (s bankService) GetOne(c *fiber.Ctx, id uint) (*entity.Bank, error) {
|
||||
bank, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Bank not found")
|
||||
}
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed get bank by id: %+v", err)
|
||||
return nil, err
|
||||
}
|
||||
return bank, nil
|
||||
}
|
||||
|
||||
func (s *bankService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Bank, error) {
|
||||
if err := s.Validate.Struct(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if exists, err := s.Repository.NameExists(c.Context(), req.Name, nil); err != nil {
|
||||
s.Log.Errorf("Failed to check bank name: %+v", err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check bank name")
|
||||
} else if exists {
|
||||
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Bank with name %s already exists", req.Name))
|
||||
}
|
||||
|
||||
createBody := &entity.Bank{
|
||||
Name: req.Name,
|
||||
Alias: req.Alias,
|
||||
Owner: req.Owner,
|
||||
AccountNumber: req.AccountNumber,
|
||||
CreatedBy: 1,
|
||||
}
|
||||
|
||||
if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil {
|
||||
s.Log.Errorf("Failed to create bank: %+v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.GetOne(c, createBody.Id)
|
||||
}
|
||||
|
||||
func (s bankService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Bank, error) {
|
||||
if err := s.Validate.Struct(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
updateBody := make(map[string]any)
|
||||
|
||||
if req.Name != nil {
|
||||
if exists, err := s.Repository.NameExists(c.Context(), *req.Name, &id); err != nil {
|
||||
s.Log.Errorf("Failed to check bank name: %+v", err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check bank name")
|
||||
} else if exists {
|
||||
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Bank with name %s already exists", *req.Name))
|
||||
}
|
||||
updateBody["name"] = *req.Name
|
||||
}
|
||||
if req.Alias != nil {
|
||||
updateBody["alias"] = *req.Alias
|
||||
}
|
||||
if req.Owner != nil {
|
||||
updateBody["owner"] = *req.Owner
|
||||
}
|
||||
if req.AccountNumber != nil {
|
||||
updateBody["account_number"] = *req.AccountNumber
|
||||
}
|
||||
|
||||
if len(updateBody) == 0 {
|
||||
return s.GetOne(c, id)
|
||||
}
|
||||
|
||||
if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Bank not found")
|
||||
}
|
||||
s.Log.Errorf("Failed to update bank: %+v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.GetOne(c, id)
|
||||
}
|
||||
|
||||
func (s bankService) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||
if err := s.Repository.DeleteOne(c.Context(), id); err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(fiber.StatusNotFound, "Bank not found")
|
||||
}
|
||||
s.Log.Errorf("Failed to delete bank: %+v", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package validation
|
||||
|
||||
type Create struct {
|
||||
Name string `json:"name" validate:"required_strict,min=3"`
|
||||
Alias string `json:"alias" validate:"required_strict"`
|
||||
Owner *string `json:"owner,omitempty" validate:"omitempty"`
|
||||
AccountNumber string `json:"account_number" validate:"required_strict,max=50"`
|
||||
}
|
||||
|
||||
type Update struct {
|
||||
Name *string `json:"name,omitempty" validate:"omitempty"`
|
||||
Alias *string `json:"alias,omitempty" validate:"omitempty"`
|
||||
Owner *string `json:"owner,omitempty" validate:"omitempty"`
|
||||
AccountNumber *string `json:"account_number,omitempty" validate:"omitempty,max=50"`
|
||||
}
|
||||
|
||||
type Query struct {
|
||||
Page int `query:"page" validate:"omitempty,number,min=1"`
|
||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
|
||||
Search string `query:"search" validate:"omitempty,max=50"`
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strconv"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto"
|
||||
service "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/services"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/validations"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/response"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type CustomerController struct {
|
||||
CustomerService service.CustomerService
|
||||
}
|
||||
|
||||
func NewCustomerController(customerService service.CustomerService) *CustomerController {
|
||||
return &CustomerController{
|
||||
CustomerService: customerService,
|
||||
}
|
||||
}
|
||||
|
||||
func (u *CustomerController) GetAll(c *fiber.Ctx) error {
|
||||
query := &validation.Query{
|
||||
Page: c.QueryInt("page", 1),
|
||||
Limit: c.QueryInt("limit", 10),
|
||||
Search: c.Query("search", ""),
|
||||
}
|
||||
|
||||
result, totalResults, err := u.CustomerService.GetAll(c, query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.SuccessWithPaginate[dto.CustomerListDTO]{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Get all customers successfully",
|
||||
Meta: response.Meta{
|
||||
Page: query.Page,
|
||||
Limit: query.Limit,
|
||||
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
|
||||
TotalResults: totalResults,
|
||||
},
|
||||
Data: dto.ToCustomerListDTOs(result),
|
||||
})
|
||||
}
|
||||
|
||||
func (u *CustomerController) GetOne(c *fiber.Ctx) error {
|
||||
param := c.Params("id")
|
||||
|
||||
id, err := strconv.Atoi(param)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||
}
|
||||
|
||||
result, err := u.CustomerService.GetOne(c, uint(id))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.Success{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Get customer successfully",
|
||||
Data: dto.ToCustomerListDTO(*result),
|
||||
})
|
||||
}
|
||||
|
||||
func (u *CustomerController) CreateOne(c *fiber.Ctx) error {
|
||||
req := new(validation.Create)
|
||||
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||
}
|
||||
|
||||
result, err := u.CustomerService.CreateOne(c, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusCreated).
|
||||
JSON(response.Success{
|
||||
Code: fiber.StatusCreated,
|
||||
Status: "success",
|
||||
Message: "Create customer successfully",
|
||||
Data: dto.ToCustomerListDTO(*result),
|
||||
})
|
||||
}
|
||||
|
||||
func (u *CustomerController) UpdateOne(c *fiber.Ctx) error {
|
||||
req := new(validation.Update)
|
||||
param := c.Params("id")
|
||||
|
||||
id, err := strconv.Atoi(param)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||
}
|
||||
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||
}
|
||||
|
||||
result, err := u.CustomerService.UpdateOne(c, req, uint(id))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.Success{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Update customer successfully",
|
||||
Data: dto.ToCustomerListDTO(*result),
|
||||
})
|
||||
}
|
||||
|
||||
func (u *CustomerController) DeleteOne(c *fiber.Ctx) error {
|
||||
param := c.Params("id")
|
||||
|
||||
id, err := strconv.Atoi(param)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||
}
|
||||
|
||||
if err := u.CustomerService.DeleteOne(c, uint(id)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.Common{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Delete customer successfully",
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
||||
)
|
||||
|
||||
// === DTO Structs ===
|
||||
|
||||
type CustomerBaseDTO struct {
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
PicId uint `json:"pic_id"`
|
||||
Type string `json:"type"`
|
||||
Address string `json:"address"`
|
||||
Phone string `json:"phone"`
|
||||
Email string `json:"email"`
|
||||
AccountNumber string `json:"account_number"`
|
||||
Balance float64 `json:"balance"`
|
||||
|
||||
Pic *userDTO.UserBaseDTO `json:"pic"`
|
||||
}
|
||||
|
||||
type CustomerListDTO struct {
|
||||
CustomerBaseDTO
|
||||
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type CustomerDetailDTO struct {
|
||||
CustomerListDTO
|
||||
}
|
||||
|
||||
// === Mapper Functions ===
|
||||
|
||||
func ToCustomerBaseDTO(e entity.Customer) CustomerBaseDTO {
|
||||
var pic *userDTO.UserBaseDTO
|
||||
if e.Pic.Id != 0 {
|
||||
mapped := userDTO.ToUserBaseDTO(e.Pic)
|
||||
pic = &mapped
|
||||
}
|
||||
|
||||
return CustomerBaseDTO{
|
||||
Id: e.Id,
|
||||
Name: e.Name,
|
||||
PicId: e.PicId,
|
||||
Type: e.Type,
|
||||
Address: e.Address,
|
||||
Phone: e.Phone,
|
||||
Email: e.Email,
|
||||
AccountNumber: e.AccountNumber,
|
||||
Pic: pic,
|
||||
}
|
||||
}
|
||||
|
||||
func ToCustomerListDTO(e entity.Customer) CustomerListDTO {
|
||||
var createdUser *userDTO.UserBaseDTO
|
||||
if e.CreatedUser.Id != 0 {
|
||||
mapped := userDTO.ToUserBaseDTO(e.CreatedUser)
|
||||
createdUser = &mapped
|
||||
}
|
||||
|
||||
return CustomerListDTO{
|
||||
CustomerBaseDTO: ToCustomerBaseDTO(e),
|
||||
CreatedAt: e.CreatedAt,
|
||||
UpdatedAt: e.UpdatedAt,
|
||||
CreatedUser: createdUser,
|
||||
}
|
||||
}
|
||||
|
||||
func ToCustomerListDTOs(e []entity.Customer) []CustomerListDTO {
|
||||
result := make([]CustomerListDTO, len(e))
|
||||
for i, r := range e {
|
||||
result[i] = ToCustomerListDTO(r)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func ToCustomerDetailDTO(e entity.Customer) CustomerDetailDTO {
|
||||
return CustomerDetailDTO{
|
||||
CustomerListDTO: ToCustomerListDTO(e),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package customers
|
||||
|
||||
import (
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"gorm.io/gorm"
|
||||
|
||||
rCustomer "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/repositories"
|
||||
sCustomer "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/services"
|
||||
|
||||
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||
)
|
||||
|
||||
type CustomerModule struct{}
|
||||
|
||||
func (CustomerModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||
customerRepo := rCustomer.NewCustomerRepository(db)
|
||||
userRepo := rUser.NewUserRepository(db)
|
||||
|
||||
customerService := sCustomer.NewCustomerService(customerRepo, validate)
|
||||
userService := sUser.NewUserService(userRepo, validate)
|
||||
|
||||
CustomerRoutes(router, userService, customerService)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type CustomerRepository interface {
|
||||
repository.BaseRepository[entity.Customer]
|
||||
PicExists(ctx context.Context, areaId uint) (bool, error)
|
||||
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
|
||||
}
|
||||
|
||||
type CustomerRepositoryImpl struct {
|
||||
*repository.BaseRepositoryImpl[entity.Customer]
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewCustomerRepository(db *gorm.DB) CustomerRepository {
|
||||
return &CustomerRepositoryImpl{
|
||||
BaseRepositoryImpl: repository.NewBaseRepository[entity.Customer](db),
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *CustomerRepositoryImpl) PicExists(ctx context.Context, picId uint) (bool, error) {
|
||||
return repository.Exists[entity.User](ctx, r.db, picId)
|
||||
}
|
||||
|
||||
func (r *CustomerRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) {
|
||||
return repository.ExistsByName[entity.Customer](ctx, r.db, name, excludeID)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package customers
|
||||
|
||||
import (
|
||||
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/controllers"
|
||||
customer "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/services"
|
||||
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func CustomerRoutes(v1 fiber.Router, u user.UserService, s customer.CustomerService) {
|
||||
ctrl := controller.NewCustomerController(s)
|
||||
|
||||
route := v1.Group("/customers")
|
||||
|
||||
// route.Get("/", m.Auth(u), ctrl.GetAll)
|
||||
// route.Post("/", m.Auth(u), ctrl.CreateOne)
|
||||
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
|
||||
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
|
||||
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
|
||||
|
||||
route.Get("/", ctrl.GetAll)
|
||||
route.Post("/", ctrl.CreateOne)
|
||||
route.Get("/:id", ctrl.GetOne)
|
||||
route.Patch("/:id", ctrl.UpdateOne)
|
||||
route.Delete("/:id", ctrl.DeleteOne)
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/repositories"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/validations"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type CustomerService interface {
|
||||
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.Customer, int64, error)
|
||||
GetOne(ctx *fiber.Ctx, id uint) (*entity.Customer, error)
|
||||
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Customer, error)
|
||||
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Customer, error)
|
||||
DeleteOne(ctx *fiber.Ctx, id uint) error
|
||||
}
|
||||
|
||||
type customerService struct {
|
||||
Log *logrus.Logger
|
||||
Validate *validator.Validate
|
||||
Repository repository.CustomerRepository
|
||||
}
|
||||
|
||||
func NewCustomerService(repo repository.CustomerRepository, validate *validator.Validate) CustomerService {
|
||||
return &customerService{
|
||||
Log: utils.Log,
|
||||
Validate: validate,
|
||||
Repository: repo,
|
||||
}
|
||||
}
|
||||
|
||||
func (s customerService) withRelations(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("CreatedUser").Preload("Pic")
|
||||
}
|
||||
|
||||
func (s customerService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Customer, int64, error) {
|
||||
if err := s.Validate.Struct(params); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
offset := (params.Page - 1) * params.Limit
|
||||
|
||||
customers, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
||||
db = s.withRelations(db)
|
||||
if params.Search != "" {
|
||||
return db.Where("name LIKE ?", "%"+params.Search+"%")
|
||||
}
|
||||
return db.Order("created_at DESC").Order("updated_at DESC")
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to get customers: %+v", err)
|
||||
return nil, 0, err
|
||||
}
|
||||
return customers, total, nil
|
||||
}
|
||||
|
||||
func (s customerService) GetOne(c *fiber.Ctx, id uint) (*entity.Customer, error) {
|
||||
customer, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Customer not found")
|
||||
}
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed get customer by id: %+v", err)
|
||||
return nil, err
|
||||
}
|
||||
return customer, nil
|
||||
}
|
||||
|
||||
func (s *customerService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Customer, error) {
|
||||
if err := s.Validate.Struct(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if exists, err := s.Repository.NameExists(c.Context(), req.Name, nil); err != nil {
|
||||
s.Log.Errorf("Failed to check customer name: %+v", err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check customer name")
|
||||
} else if exists {
|
||||
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Customer with name %s already exists", req.Name))
|
||||
}
|
||||
|
||||
typ := strings.ToUpper(req.Type)
|
||||
if !utils.IsValidCustomerSupplierType(typ) {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid customer type")
|
||||
}
|
||||
|
||||
if err := common.EnsureRelations(c.Context(),
|
||||
common.RelationCheck{Name: "Pic", ID: &req.PicId, Exists: s.Repository.PicExists},
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//TODO: created by dummy
|
||||
createBody := &entity.Customer{
|
||||
Name: req.Name,
|
||||
PicId: req.PicId,
|
||||
Type: typ,
|
||||
Address: req.Address,
|
||||
Phone: req.Phone,
|
||||
Email: req.Email,
|
||||
AccountNumber: req.AccountNumber,
|
||||
CreatedBy: 1,
|
||||
}
|
||||
|
||||
if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil {
|
||||
s.Log.Errorf("Failed to create customer: %+v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.GetOne(c, createBody.Id)
|
||||
}
|
||||
|
||||
func (s customerService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Customer, error) {
|
||||
if err := s.Validate.Struct(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
updateBody := make(map[string]any)
|
||||
|
||||
if req.Name != nil {
|
||||
if exists, err := s.Repository.NameExists(c.Context(), *req.Name, &id); err != nil {
|
||||
s.Log.Errorf("Failed to check customer name: %+v", err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check customer name")
|
||||
} else if exists {
|
||||
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Customer with name %s already exists", *req.Name))
|
||||
}
|
||||
updateBody["name"] = *req.Name
|
||||
}
|
||||
|
||||
if err := common.EnsureRelations(c.Context(), common.RelationCheck{Name: "Pic", ID: req.PicId, Exists: s.Repository.PicExists}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if req.PicId != nil {
|
||||
updateBody["pic_id"] = *req.PicId
|
||||
}
|
||||
|
||||
if req.Type != nil {
|
||||
typ := strings.ToUpper(*req.Type)
|
||||
if !utils.IsValidCustomerSupplierType(typ) {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid customer type")
|
||||
}
|
||||
updateBody["type"] = typ
|
||||
}
|
||||
|
||||
if len(updateBody) == 0 {
|
||||
return s.GetOne(c, id)
|
||||
}
|
||||
|
||||
if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Customer not found")
|
||||
}
|
||||
s.Log.Errorf("Failed to update customer: %+v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.GetOne(c, id)
|
||||
}
|
||||
|
||||
func (s customerService) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||
if err := s.Repository.DeleteOne(c.Context(), id); err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(fiber.StatusNotFound, "Customer not found")
|
||||
}
|
||||
s.Log.Errorf("Failed to delete customer: %+v", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package validation
|
||||
|
||||
type Create struct {
|
||||
Name string `json:"name" validate:"required_strict,min=3"`
|
||||
PicId uint `json:"pic_id" validate:"required_strict,number,gt=0"`
|
||||
Type string `json:"type" validate:"required_strict"`
|
||||
Address string `json:"address" validate:"required_strict"`
|
||||
Phone string `json:"phone" validate:"required_strict,max=20"`
|
||||
Email string `json:"email" validate:"required_strict,email"`
|
||||
AccountNumber string `json:"account_number" validate:"required_strict"`
|
||||
}
|
||||
|
||||
type Update struct {
|
||||
Name *string `json:"name,omitempty" validate:"omitempty"`
|
||||
PicId *uint `json:"pic_id,omitempty" validate:"omitempty,number,gt=0"`
|
||||
Type *string `json:"type,omitempty" validate:"omitempty"`
|
||||
Address *string `json:"address,omitempty" validate:"omitempty"`
|
||||
Phone *string `json:"phone,omitempty" validate:"omitempty"`
|
||||
Email *string `json:"email,omitempty" validate:"omitempty"`
|
||||
AccountNumber *string `json:"account_number,omitempty" validate:"omitempty"`
|
||||
}
|
||||
|
||||
type Query struct {
|
||||
Page int `query:"page" validate:"omitempty,number,min=1"`
|
||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
|
||||
Search string `query:"search" validate:"omitempty,max=50"`
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strconv"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/dto"
|
||||
service "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/services"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/validations"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/response"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type FcrController struct {
|
||||
FcrService service.FcrService
|
||||
}
|
||||
|
||||
func NewFcrController(fcrService service.FcrService) *FcrController {
|
||||
return &FcrController{
|
||||
FcrService: fcrService,
|
||||
}
|
||||
}
|
||||
|
||||
func (u *FcrController) GetAll(c *fiber.Ctx) error {
|
||||
query := &validation.Query{
|
||||
Page: c.QueryInt("page", 1),
|
||||
Limit: c.QueryInt("limit", 10),
|
||||
Search: c.Query("search", ""),
|
||||
}
|
||||
|
||||
result, totalResults, err := u.FcrService.GetAll(c, query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.SuccessWithPaginate[dto.FcrListDTO]{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Get all fcrs successfully",
|
||||
Meta: response.Meta{
|
||||
Page: query.Page,
|
||||
Limit: query.Limit,
|
||||
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
|
||||
TotalResults: totalResults,
|
||||
},
|
||||
Data: dto.ToFcrListDTOs(result),
|
||||
})
|
||||
}
|
||||
|
||||
func (u *FcrController) GetOne(c *fiber.Ctx) error {
|
||||
param := c.Params("id")
|
||||
|
||||
id, err := strconv.Atoi(param)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||
}
|
||||
|
||||
result, err := u.FcrService.GetOne(c, uint(id))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.Success{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Get fcr successfully",
|
||||
Data: dto.ToFcrDetailDTO(*result),
|
||||
})
|
||||
}
|
||||
|
||||
func (u *FcrController) CreateOne(c *fiber.Ctx) error {
|
||||
req := new(validation.Create)
|
||||
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||
}
|
||||
|
||||
result, err := u.FcrService.CreateOne(c, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusCreated).
|
||||
JSON(response.Success{
|
||||
Code: fiber.StatusCreated,
|
||||
Status: "success",
|
||||
Message: "Create fcr successfully",
|
||||
Data: dto.ToFcrDetailDTO(*result),
|
||||
})
|
||||
}
|
||||
|
||||
func (u *FcrController) UpdateOne(c *fiber.Ctx) error {
|
||||
req := new(validation.Update)
|
||||
param := c.Params("id")
|
||||
|
||||
id, err := strconv.Atoi(param)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||
}
|
||||
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||
}
|
||||
|
||||
result, err := u.FcrService.UpdateOne(c, req, uint(id))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.Success{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Update fcr successfully",
|
||||
Data: dto.ToFcrDetailDTO(*result),
|
||||
})
|
||||
}
|
||||
|
||||
func (u *FcrController) DeleteOne(c *fiber.Ctx) error {
|
||||
param := c.Params("id")
|
||||
|
||||
id, err := strconv.Atoi(param)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||
}
|
||||
|
||||
if err := u.FcrService.DeleteOne(c, uint(id)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.Common{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Delete fcr successfully",
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
||||
)
|
||||
|
||||
// === DTO Structs ===
|
||||
|
||||
type FcrBaseDTO struct {
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type FcrStandardDTO struct {
|
||||
Id uint `json:"id"`
|
||||
Weight float64 `json:"weight"`
|
||||
FcrNumber float64 `json:"fcr_number"`
|
||||
Mortality float64 `json:"mortality"`
|
||||
}
|
||||
|
||||
type FcrListDTO struct {
|
||||
FcrBaseDTO
|
||||
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type FcrDetailDTO struct {
|
||||
FcrListDTO
|
||||
Standards []FcrStandardDTO `json:"fcr_standards"`
|
||||
}
|
||||
|
||||
// === Mapper Functions ===
|
||||
|
||||
func ToFcrBaseDTO(e entity.Fcr) FcrBaseDTO {
|
||||
return FcrBaseDTO{
|
||||
Id: e.Id,
|
||||
Name: e.Name,
|
||||
}
|
||||
}
|
||||
|
||||
func ToFcrListDTO(e entity.Fcr) FcrListDTO {
|
||||
var createdUser *userDTO.UserBaseDTO
|
||||
if e.CreatedUser.Id != 0 {
|
||||
mapped := userDTO.ToUserBaseDTO(e.CreatedUser)
|
||||
createdUser = &mapped
|
||||
}
|
||||
|
||||
return FcrListDTO{
|
||||
FcrBaseDTO: ToFcrBaseDTO(e),
|
||||
CreatedAt: e.CreatedAt,
|
||||
UpdatedAt: e.UpdatedAt,
|
||||
CreatedUser: createdUser,
|
||||
}
|
||||
}
|
||||
|
||||
func ToFcrListDTOs(e []entity.Fcr) []FcrListDTO {
|
||||
result := make([]FcrListDTO, len(e))
|
||||
for i, r := range e {
|
||||
result[i] = ToFcrListDTO(r)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func ToFcrDetailDTO(e entity.Fcr) FcrDetailDTO {
|
||||
return FcrDetailDTO{
|
||||
FcrListDTO: ToFcrListDTO(e),
|
||||
Standards: ToFcrStandardDTOs(e.Standards),
|
||||
}
|
||||
}
|
||||
|
||||
func ToFcrStandardDTOs(standards []entity.FcrStandard) []FcrStandardDTO {
|
||||
result := make([]FcrStandardDTO, len(standards))
|
||||
for i, s := range standards {
|
||||
result[i] = FcrStandardDTO{
|
||||
Id: s.Id,
|
||||
Weight: s.Weight,
|
||||
FcrNumber: s.FcrNumber,
|
||||
Mortality: s.Mortality,
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package fcrs
|
||||
|
||||
import (
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"gorm.io/gorm"
|
||||
|
||||
rFcr "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/repositories"
|
||||
sFcr "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/services"
|
||||
|
||||
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||
)
|
||||
|
||||
type FcrModule struct{}
|
||||
|
||||
func (FcrModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||
fcrRepo := rFcr.NewFcrRepository(db)
|
||||
userRepo := rUser.NewUserRepository(db)
|
||||
|
||||
fcrService := sFcr.NewFcrService(fcrRepo, validate)
|
||||
userService := sUser.NewUserService(userRepo, validate)
|
||||
|
||||
FcrRoutes(router, userService, fcrService)
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type FcrRepository interface {
|
||||
repository.BaseRepository[entity.Fcr]
|
||||
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
|
||||
SyncStandardsDiff(ctx context.Context, tx *gorm.DB, fcrID uint, standards []entity.FcrStandard) error
|
||||
}
|
||||
|
||||
type FcrRepositoryImpl struct {
|
||||
*repository.BaseRepositoryImpl[entity.Fcr]
|
||||
}
|
||||
|
||||
func NewFcrRepository(db *gorm.DB) FcrRepository {
|
||||
return &FcrRepositoryImpl{
|
||||
BaseRepositoryImpl: repository.NewBaseRepository[entity.Fcr](db),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *FcrRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) {
|
||||
return repository.ExistsByName[entity.Fcr](ctx, r.DB(), name, excludeID)
|
||||
}
|
||||
|
||||
func (r *FcrRepositoryImpl) SyncStandardsDiff(ctx context.Context, tx *gorm.DB, fcrID uint, standards []entity.FcrStandard) error {
|
||||
db := tx
|
||||
if db == nil {
|
||||
db = r.DB()
|
||||
}
|
||||
|
||||
var existing []entity.FcrStandard
|
||||
if err := db.WithContext(ctx).
|
||||
Where("fcr_id = ?", fcrID).
|
||||
Find(&existing).
|
||||
Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
existingMap := make(map[float64]entity.FcrStandard)
|
||||
for _, st := range existing {
|
||||
existingMap[st.Weight] = st
|
||||
}
|
||||
|
||||
newMap := make(map[float64]entity.FcrStandard)
|
||||
for _, st := range standards {
|
||||
st.FcrID = fcrID
|
||||
newMap[st.Weight] = st
|
||||
}
|
||||
|
||||
baseRepo := repository.NewBaseRepository[entity.FcrStandard](db)
|
||||
|
||||
for weight, newStd := range newMap {
|
||||
if current, ok := existingMap[weight]; ok {
|
||||
if current.FcrNumber != newStd.FcrNumber || current.Mortality != newStd.Mortality {
|
||||
update := map[string]any{
|
||||
"fcr_number": newStd.FcrNumber,
|
||||
"mortality": newStd.Mortality,
|
||||
}
|
||||
if err := baseRepo.PatchOne(ctx, current.Id, update, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
entry := newStd
|
||||
if err := baseRepo.CreateOne(ctx, &entry, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for weight, current := range existingMap {
|
||||
if _, keep := newMap[weight]; !keep {
|
||||
if err := baseRepo.DeleteOne(ctx, current.Id); err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package fcrs
|
||||
|
||||
import (
|
||||
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/controllers"
|
||||
fcr "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/services"
|
||||
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func FcrRoutes(v1 fiber.Router, u user.UserService, s fcr.FcrService) {
|
||||
ctrl := controller.NewFcrController(s)
|
||||
|
||||
route := v1.Group("/fcrs")
|
||||
|
||||
// route.Get("/", m.Auth(u), ctrl.GetAll)
|
||||
// route.Post("/", m.Auth(u), ctrl.CreateOne)
|
||||
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
|
||||
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
|
||||
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
|
||||
|
||||
route.Get("/", ctrl.GetAll)
|
||||
route.Post("/", ctrl.CreateOne)
|
||||
route.Get("/:id", ctrl.GetOne)
|
||||
route.Patch("/:id", ctrl.UpdateOne)
|
||||
route.Delete("/:id", ctrl.DeleteOne)
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/repositories"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/validations"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type FcrService interface {
|
||||
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.Fcr, int64, error)
|
||||
GetOne(ctx *fiber.Ctx, id uint) (*entity.Fcr, error)
|
||||
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Fcr, error)
|
||||
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Fcr, error)
|
||||
DeleteOne(ctx *fiber.Ctx, id uint) error
|
||||
}
|
||||
|
||||
type fcrService struct {
|
||||
Log *logrus.Logger
|
||||
Validate *validator.Validate
|
||||
Repository repository.FcrRepository
|
||||
}
|
||||
|
||||
func NewFcrService(repo repository.FcrRepository, validate *validator.Validate) FcrService {
|
||||
return &fcrService{
|
||||
Log: utils.Log,
|
||||
Validate: validate,
|
||||
Repository: repo,
|
||||
}
|
||||
}
|
||||
|
||||
func (s fcrService) withRelations(db *gorm.DB) *gorm.DB {
|
||||
return db.
|
||||
Preload("CreatedUser").
|
||||
Preload("Standards", func(db *gorm.DB) *gorm.DB {
|
||||
return db.Order("weight ASC")
|
||||
})
|
||||
}
|
||||
|
||||
func (s fcrService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Fcr, int64, error) {
|
||||
if err := s.Validate.Struct(params); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
offset := (params.Page - 1) * params.Limit
|
||||
|
||||
fcrs, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
||||
db = s.withRelations(db)
|
||||
if params.Search != "" {
|
||||
return db.Where("name LIKE ?", "%"+params.Search+"%")
|
||||
}
|
||||
return db.Order("created_at DESC").Order("updated_at DESC")
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to get fcrs: %+v", err)
|
||||
return nil, 0, err
|
||||
}
|
||||
return fcrs, total, nil
|
||||
}
|
||||
|
||||
func (s fcrService) GetOne(c *fiber.Ctx, id uint) (*entity.Fcr, error) {
|
||||
fcr, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Fcr not found")
|
||||
}
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed get fcr by id: %+v", err)
|
||||
return nil, err
|
||||
}
|
||||
return fcr, nil
|
||||
}
|
||||
|
||||
func (s *fcrService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Fcr, error) {
|
||||
if err := s.Validate.Struct(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if exists, err := s.Repository.NameExists(c.Context(), req.Name, nil); err != nil {
|
||||
s.Log.Errorf("Failed to check fcr name: %+v", err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check fcr name")
|
||||
} else if exists {
|
||||
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Fcr with name %s already exists", req.Name))
|
||||
}
|
||||
|
||||
createBody := &entity.Fcr{
|
||||
Name: req.Name,
|
||||
CreatedBy: 1,
|
||||
}
|
||||
|
||||
err := s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
|
||||
repoTx := s.Repository.WithTx(tx)
|
||||
|
||||
if err := repoTx.CreateOne(c.Context(), createBody, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(req.FcrStandards) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
standards := make([]entity.FcrStandard, len(req.FcrStandards))
|
||||
for i, std := range req.FcrStandards {
|
||||
standards[i] = entity.FcrStandard{
|
||||
FcrID: createBody.Id,
|
||||
Weight: std.Weight,
|
||||
FcrNumber: std.FcrNumber,
|
||||
Mortality: std.Mortality,
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.Repository.SyncStandardsDiff(c.Context(), tx, createBody.Id, standards); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to create fcr: %+v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.GetOne(c, createBody.Id)
|
||||
}
|
||||
|
||||
func (s fcrService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Fcr, error) {
|
||||
if err := s.Validate.Struct(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
updateBody := make(map[string]any)
|
||||
|
||||
if req.Name != nil {
|
||||
if exists, err := s.Repository.NameExists(c.Context(), *req.Name, &id); err != nil {
|
||||
s.Log.Errorf("Failed to check fcr name: %+v", err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check fcr name")
|
||||
} else if exists {
|
||||
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Fcr with name %s already exists", *req.Name))
|
||||
}
|
||||
updateBody["name"] = *req.Name
|
||||
}
|
||||
|
||||
if len(updateBody) == 0 && req.FcrStandards == nil {
|
||||
return s.GetOne(c, id)
|
||||
}
|
||||
|
||||
err := s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
|
||||
repoTx := s.Repository.WithTx(tx)
|
||||
|
||||
if len(updateBody) > 0 {
|
||||
if err := repoTx.PatchOne(c.Context(), id, updateBody, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if _, err := repoTx.GetByID(c.Context(), id, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if req.FcrStandards != nil {
|
||||
standards := make([]entity.FcrStandard, len(req.FcrStandards))
|
||||
for i, std := range req.FcrStandards {
|
||||
standards[i] = entity.FcrStandard{
|
||||
FcrID: id,
|
||||
Weight: std.Weight,
|
||||
FcrNumber: std.FcrNumber,
|
||||
Mortality: std.Mortality,
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.Repository.SyncStandardsDiff(c.Context(), tx, id, standards); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Fcr not found")
|
||||
}
|
||||
s.Log.Errorf("Failed to update fcr: %+v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.GetOne(c, id)
|
||||
}
|
||||
|
||||
func (s fcrService) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||
err := s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
|
||||
repoTx := s.Repository.WithTx(tx)
|
||||
|
||||
if err := s.Repository.SyncStandardsDiff(c.Context(), tx, id, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return repoTx.DeleteOne(c.Context(), id)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(fiber.StatusNotFound, "Fcr not found")
|
||||
}
|
||||
s.Log.Errorf("Failed to delete fcr: %+v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package validation
|
||||
|
||||
type FcrStandard struct {
|
||||
Weight float64 `json:"weight" validate:"required,gte=0"`
|
||||
FcrNumber float64 `json:"fcr_number" validate:"required,gte=0"`
|
||||
Mortality float64 `json:"mortality" validate:"required,gte=0"`
|
||||
}
|
||||
|
||||
type Create struct {
|
||||
Name string `json:"name" validate:"required_strict,min=3,max=50"`
|
||||
FcrStandards []FcrStandard `json:"fcr_standards" validate:"required,min=1,dive"`
|
||||
}
|
||||
|
||||
type Update struct {
|
||||
Name *string `json:"name,omitempty" validate:"omitempty_strict,min=3,max=50"`
|
||||
FcrStandards []FcrStandard `json:"fcr_standards,omitempty" validate:"omitempty,min=1,dive"`
|
||||
}
|
||||
|
||||
type Query struct {
|
||||
Page int `query:"page" validate:"omitempty,number,min=1"`
|
||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
|
||||
Search string `query:"search" validate:"omitempty,max=50"`
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strconv"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
|
||||
service "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/services"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/validations"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/response"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type KandangController struct {
|
||||
KandangService service.KandangService
|
||||
}
|
||||
|
||||
func NewKandangController(kandangService service.KandangService) *KandangController {
|
||||
return &KandangController{
|
||||
KandangService: kandangService,
|
||||
}
|
||||
}
|
||||
|
||||
func (u *KandangController) GetAll(c *fiber.Ctx) error {
|
||||
query := &validation.Query{
|
||||
Page: c.QueryInt("page", 1),
|
||||
Limit: c.QueryInt("limit", 10),
|
||||
Search: c.Query("search", ""),
|
||||
}
|
||||
|
||||
result, totalResults, err := u.KandangService.GetAll(c, query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.SuccessWithPaginate[dto.KandangListDTO]{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Get all kandangs successfully",
|
||||
Meta: response.Meta{
|
||||
Page: query.Page,
|
||||
Limit: query.Limit,
|
||||
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
|
||||
TotalResults: totalResults,
|
||||
},
|
||||
Data: dto.ToKandangListDTOs(result),
|
||||
})
|
||||
}
|
||||
|
||||
func (u *KandangController) GetOne(c *fiber.Ctx) error {
|
||||
param := c.Params("id")
|
||||
|
||||
id, err := strconv.Atoi(param)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||
}
|
||||
|
||||
result, err := u.KandangService.GetOne(c, uint(id))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.Success{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Get kandang successfully",
|
||||
Data: dto.ToKandangListDTO(*result),
|
||||
})
|
||||
}
|
||||
|
||||
func (u *KandangController) CreateOne(c *fiber.Ctx) error {
|
||||
req := new(validation.Create)
|
||||
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||
}
|
||||
|
||||
result, err := u.KandangService.CreateOne(c, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusCreated).
|
||||
JSON(response.Success{
|
||||
Code: fiber.StatusCreated,
|
||||
Status: "success",
|
||||
Message: "Create kandang successfully",
|
||||
Data: dto.ToKandangListDTO(*result),
|
||||
})
|
||||
}
|
||||
|
||||
func (u *KandangController) UpdateOne(c *fiber.Ctx) error {
|
||||
req := new(validation.Update)
|
||||
param := c.Params("id")
|
||||
|
||||
id, err := strconv.Atoi(param)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||
}
|
||||
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||
}
|
||||
|
||||
result, err := u.KandangService.UpdateOne(c, req, uint(id))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.Success{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Update kandang successfully",
|
||||
Data: dto.ToKandangListDTO(*result),
|
||||
})
|
||||
}
|
||||
|
||||
func (u *KandangController) DeleteOne(c *fiber.Ctx) error {
|
||||
param := c.Params("id")
|
||||
|
||||
id, err := strconv.Atoi(param)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||
}
|
||||
|
||||
if err := u.KandangService.DeleteOne(c, uint(id)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.Common{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Delete kandang successfully",
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto"
|
||||
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
||||
)
|
||||
|
||||
// === DTO Structs ===
|
||||
|
||||
type KandangBaseDTO struct {
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Location *locationDTO.LocationBaseDTO `json:"location"`
|
||||
Pic *userDTO.UserBaseDTO `json:"pic"`
|
||||
}
|
||||
|
||||
type KandangListDTO struct {
|
||||
KandangBaseDTO
|
||||
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type KandangDetailDTO struct {
|
||||
KandangListDTO
|
||||
}
|
||||
|
||||
// === Mapper Functions ===
|
||||
|
||||
func ToKandangBaseDTO(e entity.Kandang) KandangBaseDTO {
|
||||
var location *locationDTO.LocationBaseDTO
|
||||
if e.Location.Id != 0 {
|
||||
mapped := locationDTO.ToLocationBaseDTO(e.Location)
|
||||
location = &mapped
|
||||
}
|
||||
|
||||
var pic *userDTO.UserBaseDTO
|
||||
if e.Pic.Id != 0 {
|
||||
mapped := userDTO.ToUserBaseDTO(e.Pic)
|
||||
pic = &mapped
|
||||
}
|
||||
|
||||
return KandangBaseDTO{
|
||||
Id: e.Id,
|
||||
Name: e.Name,
|
||||
Location: location,
|
||||
Pic: pic,
|
||||
}
|
||||
}
|
||||
|
||||
func ToKandangListDTO(e entity.Kandang) KandangListDTO {
|
||||
var createdUser *userDTO.UserBaseDTO
|
||||
if e.CreatedUser.Id != 0 {
|
||||
mapped := userDTO.ToUserBaseDTO(e.CreatedUser)
|
||||
createdUser = &mapped
|
||||
}
|
||||
|
||||
return KandangListDTO{
|
||||
KandangBaseDTO: ToKandangBaseDTO(e),
|
||||
CreatedAt: e.CreatedAt,
|
||||
UpdatedAt: e.UpdatedAt,
|
||||
CreatedUser: createdUser,
|
||||
}
|
||||
}
|
||||
|
||||
func ToKandangListDTOs(e []entity.Kandang) []KandangListDTO {
|
||||
result := make([]KandangListDTO, len(e))
|
||||
for i, r := range e {
|
||||
result[i] = ToKandangListDTO(r)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func ToKandangDetailDTO(e entity.Kandang) KandangDetailDTO {
|
||||
return KandangDetailDTO{
|
||||
KandangListDTO: ToKandangListDTO(e),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package kandangs
|
||||
|
||||
import (
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"gorm.io/gorm"
|
||||
|
||||
rKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
|
||||
sKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/services"
|
||||
|
||||
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||
)
|
||||
|
||||
type KandangModule struct{}
|
||||
|
||||
func (KandangModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||
kandangRepo := rKandang.NewKandangRepository(db)
|
||||
userRepo := rUser.NewUserRepository(db)
|
||||
|
||||
kandangService := sKandang.NewKandangService(kandangRepo, validate)
|
||||
userService := sUser.NewUserService(userRepo, validate)
|
||||
|
||||
KandangRoutes(router, userService, kandangService)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type KandangRepository interface {
|
||||
repository.BaseRepository[entity.Kandang]
|
||||
LocationExists(ctx context.Context, areaId uint) (bool, error)
|
||||
PicExists(ctx context.Context, areaId uint) (bool, error)
|
||||
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
|
||||
}
|
||||
|
||||
type KandangRepositoryImpl struct {
|
||||
*repository.BaseRepositoryImpl[entity.Kandang]
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewKandangRepository(db *gorm.DB) KandangRepository {
|
||||
return &KandangRepositoryImpl{
|
||||
BaseRepositoryImpl: repository.NewBaseRepository[entity.Kandang](db),
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *KandangRepositoryImpl) LocationExists(ctx context.Context, locationId uint) (bool, error) {
|
||||
return repository.Exists[entity.Location](ctx, r.db, locationId)
|
||||
}
|
||||
|
||||
func (r *KandangRepositoryImpl) PicExists(ctx context.Context, picId uint) (bool, error) {
|
||||
return repository.Exists[entity.User](ctx, r.db, picId)
|
||||
}
|
||||
|
||||
func (r *KandangRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) {
|
||||
return repository.ExistsByName[entity.Kandang](ctx, r.db, name, excludeID)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package kandangs
|
||||
|
||||
import (
|
||||
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/controllers"
|
||||
kandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/services"
|
||||
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func KandangRoutes(v1 fiber.Router, u user.UserService, s kandang.KandangService) {
|
||||
ctrl := controller.NewKandangController(s)
|
||||
|
||||
route := v1.Group("/kandangs")
|
||||
|
||||
// route.Get("/", m.Auth(u), ctrl.GetAll)
|
||||
// route.Post("/", m.Auth(u), ctrl.CreateOne)
|
||||
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
|
||||
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
|
||||
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
|
||||
|
||||
route.Get("/", ctrl.GetAll)
|
||||
route.Post("/", ctrl.CreateOne)
|
||||
route.Get("/:id", ctrl.GetOne)
|
||||
route.Patch("/:id", ctrl.UpdateOne)
|
||||
route.Delete("/:id", ctrl.DeleteOne)
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/validations"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type KandangService interface {
|
||||
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.Kandang, int64, error)
|
||||
GetOne(ctx *fiber.Ctx, id uint) (*entity.Kandang, error)
|
||||
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Kandang, error)
|
||||
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Kandang, error)
|
||||
DeleteOne(ctx *fiber.Ctx, id uint) error
|
||||
}
|
||||
|
||||
type kandangService struct {
|
||||
Log *logrus.Logger
|
||||
Validate *validator.Validate
|
||||
Repository repository.KandangRepository
|
||||
}
|
||||
|
||||
func NewKandangService(repo repository.KandangRepository, validate *validator.Validate) KandangService {
|
||||
return &kandangService{
|
||||
Log: utils.Log,
|
||||
Validate: validate,
|
||||
Repository: repo,
|
||||
}
|
||||
}
|
||||
|
||||
func (s kandangService) withRelations(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("CreatedUser").Preload("Location").Preload("Pic")
|
||||
}
|
||||
|
||||
func (s kandangService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Kandang, int64, error) {
|
||||
if err := s.Validate.Struct(params); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
offset := (params.Page - 1) * params.Limit
|
||||
|
||||
kandangs, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
||||
db = s.withRelations(db)
|
||||
if params.Search != "" {
|
||||
return db.Where("name LIKE ?", "%"+params.Search+"%")
|
||||
}
|
||||
return db.Order("created_at DESC").Order("updated_at DESC")
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to get kandangs: %+v", err)
|
||||
return nil, 0, err
|
||||
}
|
||||
return kandangs, total, nil
|
||||
}
|
||||
|
||||
func (s kandangService) GetOne(c *fiber.Ctx, id uint) (*entity.Kandang, error) {
|
||||
kandang, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Kandang not found")
|
||||
}
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed get kandang by id: %+v", err)
|
||||
return nil, err
|
||||
}
|
||||
return kandang, nil
|
||||
}
|
||||
|
||||
func (s *kandangService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Kandang, error) {
|
||||
if err := s.Validate.Struct(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if exists, err := s.Repository.NameExists(c.Context(), req.Name, nil); err != nil {
|
||||
s.Log.Errorf("Failed to check kandang name: %+v", err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check kandang name")
|
||||
} else if exists {
|
||||
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Kandang with name %s already exists", req.Name))
|
||||
}
|
||||
|
||||
if err := common.EnsureRelations(c.Context(),
|
||||
common.RelationCheck{Name: "Location", ID: &req.LocationId, Exists: s.Repository.LocationExists},
|
||||
common.RelationCheck{Name: "Pic", ID: &req.PicId, Exists: s.Repository.PicExists},
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//TODO: created by dummy
|
||||
createBody := &entity.Kandang{
|
||||
Name: req.Name,
|
||||
LocationId: req.LocationId,
|
||||
PicId: req.PicId,
|
||||
CreatedBy: 1,
|
||||
}
|
||||
|
||||
if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil {
|
||||
s.Log.Errorf("Failed to create kandang: %+v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.GetOne(c, createBody.Id)
|
||||
}
|
||||
|
||||
func (s kandangService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Kandang, error) {
|
||||
if err := s.Validate.Struct(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
updateBody := make(map[string]any)
|
||||
|
||||
if req.Name != nil {
|
||||
if exists, err := s.Repository.NameExists(c.Context(), *req.Name, &id); err != nil {
|
||||
s.Log.Errorf("Failed to check kandang name: %+v", err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check kandang name")
|
||||
} else if exists {
|
||||
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Kandang with name %s already exists", *req.Name))
|
||||
}
|
||||
updateBody["name"] = *req.Name
|
||||
}
|
||||
|
||||
if err := common.EnsureRelations(c.Context(),
|
||||
common.RelationCheck{Name: "Location", ID: req.LocationId, Exists: s.Repository.LocationExists},
|
||||
common.RelationCheck{Name: "Pic", ID: req.PicId, Exists: s.Repository.PicExists},
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if req.LocationId != nil {
|
||||
updateBody["location_id"] = *req.LocationId
|
||||
}
|
||||
|
||||
if req.PicId != nil {
|
||||
updateBody["pic_id"] = *req.PicId
|
||||
}
|
||||
|
||||
if len(updateBody) == 0 {
|
||||
return s.GetOne(c, id)
|
||||
}
|
||||
|
||||
if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Kandang not found")
|
||||
}
|
||||
s.Log.Errorf("Failed to update kandang: %+v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.GetOne(c, id)
|
||||
}
|
||||
|
||||
func (s kandangService) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||
if err := s.Repository.DeleteOne(c.Context(), id); err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(fiber.StatusNotFound, "Kandang not found")
|
||||
}
|
||||
s.Log.Errorf("Failed to delete kandang: %+v", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package validation
|
||||
|
||||
type Create struct {
|
||||
Name string `json:"name" validate:"required_strict,min=3"`
|
||||
LocationId uint `json:"location_id" validate:"required_strict,number,gt=0"`
|
||||
PicId uint `json:"pic_id" validate:"required_strict,number,gt=0"`
|
||||
}
|
||||
|
||||
type Update struct {
|
||||
Name *string `json:"name,omitempty" validate:"omitempty"`
|
||||
LocationId *uint `json:"location_id,omitempty" validate:"omitempty,number,gt=0"`
|
||||
PicId *uint `json:"pic_id,omitempty" validate:"omitempty,number,gt=0"`
|
||||
}
|
||||
|
||||
type Query struct {
|
||||
Page int `query:"page" validate:"omitempty,number,min=1"`
|
||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
|
||||
Search string `query:"search" validate:"omitempty,max=50"`
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strconv"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto"
|
||||
service "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/services"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/validations"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/response"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type LocationController struct {
|
||||
LocationService service.LocationService
|
||||
}
|
||||
|
||||
func NewLocationController(locationService service.LocationService) *LocationController {
|
||||
return &LocationController{
|
||||
LocationService: locationService,
|
||||
}
|
||||
}
|
||||
|
||||
func (u *LocationController) GetAll(c *fiber.Ctx) error {
|
||||
query := &validation.Query{
|
||||
Page: c.QueryInt("page", 1),
|
||||
Limit: c.QueryInt("limit", 10),
|
||||
Search: c.Query("search", ""),
|
||||
}
|
||||
|
||||
result, totalResults, err := u.LocationService.GetAll(c, query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.SuccessWithPaginate[dto.LocationListDTO]{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Get all locations successfully",
|
||||
Meta: response.Meta{
|
||||
Page: query.Page,
|
||||
Limit: query.Limit,
|
||||
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
|
||||
TotalResults: totalResults,
|
||||
},
|
||||
Data: dto.ToLocationListDTOs(result),
|
||||
})
|
||||
}
|
||||
|
||||
func (u *LocationController) GetOne(c *fiber.Ctx) error {
|
||||
param := c.Params("id")
|
||||
|
||||
id, err := strconv.Atoi(param)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||
}
|
||||
|
||||
result, err := u.LocationService.GetOne(c, uint(id))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.Success{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Get location successfully",
|
||||
Data: dto.ToLocationListDTO(*result),
|
||||
})
|
||||
}
|
||||
|
||||
func (u *LocationController) CreateOne(c *fiber.Ctx) error {
|
||||
req := new(validation.Create)
|
||||
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||
}
|
||||
|
||||
result, err := u.LocationService.CreateOne(c, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusCreated).
|
||||
JSON(response.Success{
|
||||
Code: fiber.StatusCreated,
|
||||
Status: "success",
|
||||
Message: "Create location successfully",
|
||||
Data: dto.ToLocationListDTO(*result),
|
||||
})
|
||||
}
|
||||
|
||||
func (u *LocationController) UpdateOne(c *fiber.Ctx) error {
|
||||
req := new(validation.Update)
|
||||
param := c.Params("id")
|
||||
|
||||
id, err := strconv.Atoi(param)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||
}
|
||||
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||
}
|
||||
|
||||
result, err := u.LocationService.UpdateOne(c, req, uint(id))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.Success{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Update location successfully",
|
||||
Data: dto.ToLocationListDTO(*result),
|
||||
})
|
||||
}
|
||||
|
||||
func (u *LocationController) DeleteOne(c *fiber.Ctx) error {
|
||||
param := c.Params("id")
|
||||
|
||||
id, err := strconv.Atoi(param)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||
}
|
||||
|
||||
if err := u.LocationService.DeleteOne(c, uint(id)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.Common{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Delete location successfully",
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
areaDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/dto"
|
||||
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
||||
)
|
||||
|
||||
// === DTO Structs ===
|
||||
|
||||
type LocationBaseDTO struct {
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Address string `json:"address"`
|
||||
Area *areaDTO.AreaBaseDTO `json:"area"`
|
||||
}
|
||||
|
||||
type LocationListDTO struct {
|
||||
LocationBaseDTO
|
||||
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type LocationDetailDTO struct {
|
||||
LocationListDTO
|
||||
}
|
||||
|
||||
// === Mapper Functions ===
|
||||
|
||||
func ToLocationBaseDTO(e entity.Location) LocationBaseDTO {
|
||||
var area *areaDTO.AreaBaseDTO
|
||||
if e.Area.Id != 0 {
|
||||
mapped := areaDTO.ToAreaBaseDTO(e.Area)
|
||||
area = &mapped
|
||||
}
|
||||
|
||||
return LocationBaseDTO{
|
||||
Id: e.Id,
|
||||
Name: e.Name,
|
||||
Address: e.Address,
|
||||
Area: area,
|
||||
}
|
||||
}
|
||||
|
||||
func ToLocationListDTO(e entity.Location) LocationListDTO {
|
||||
var createdUser *userDTO.UserBaseDTO
|
||||
if e.CreatedUser.Id != 0 {
|
||||
mapped := userDTO.ToUserBaseDTO(e.CreatedUser)
|
||||
createdUser = &mapped
|
||||
}
|
||||
|
||||
return LocationListDTO{
|
||||
LocationBaseDTO: ToLocationBaseDTO(e),
|
||||
CreatedUser: createdUser,
|
||||
CreatedAt: e.CreatedAt,
|
||||
UpdatedAt: e.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func ToLocationListDTOs(e []entity.Location) []LocationListDTO {
|
||||
result := make([]LocationListDTO, len(e))
|
||||
for i, r := range e {
|
||||
result[i] = ToLocationListDTO(r)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func ToLocationDetailDTO(e entity.Location) LocationDetailDTO {
|
||||
return LocationDetailDTO{
|
||||
LocationListDTO: ToLocationListDTO(e),
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user