Base Project

This commit is contained in:
Tran Anh Tuan
2026-05-08 14:32:24 +07:00
parent 5a9249c9ea
commit 6a4a96e0ca
74 changed files with 6749 additions and 0 deletions

50
.dockerignore Normal file
View File

@@ -0,0 +1,50 @@
# Ignore node_modules, build directories, and other dependencies
node_modules
dist
build
# Ignore log files and temporary files
*.log
*.tmp
# Ignore OS generated files
.DS_Store
Thumbs.db
# Ignore version control directories and files
.git
.github
.vscode
tests
.gitignore
# Ignore environment and configuration files
.env.local
.env.example
.env.development
.env.production
.env.test
# Ignore IDE/editor config files
.vscode
.idea
*.swp
*~
# Ignore Docker related files (optional)
.dockerignore
Dockerfile
docker-compose*.yml
# Ignore tests and documentation (optional)
*.md
*.yaml
makefile
docs/assets
docs/postman
docs/GO.md
docs/CODE.md
.kilo

21
.env.example Normal file
View File

@@ -0,0 +1,21 @@
# APPLICATION ENVIRONMENT (dev | prod)
ENV=
#SYS_ADMIN SEED
ADMIN_USERNAME=
ADMIN_EMAIL=
ADMIN_PASSWORD=
ADMIN_FULL_NAME=
# POSTGRESQL CONFIGURATION
POSTGRES_DB=
POSTGRES_USER=
POSTGRES_PASSWORD
POSTGRES_PORT=
POSTGRES_PORT_MAPPING=
# Redis CONFIGURATION`
REDIS_PORT=
REDIS_PORT_MAPPING=
REDIS_USER=
REDIS_PASSWORD=

36
.gitignore vendored Normal file
View File

@@ -0,0 +1,36 @@
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Code coverage profiles and other test artifacts
*.out
coverage.*
*.coverprofile
profile.cov
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
go.work.sum
# env file
.env
configs/yaml/config.dev.yaml
configs/yaml/config.prod.yaml
docs/postman
.kilo
tmp
# Editor/IDE
# .idea/
# .vscode/

62
Makefile Normal file
View File

@@ -0,0 +1,62 @@
#* GET FILE ENV
include .env
export $(shell sed 's/=.*//' .env)
DB_URL=postgres://$(POSTGRES_USER):$(POSTGRES_PASSWORD)@localhost:$(POSTGRES_PORT)/$(POSTGRES_DB)?sslmode=disable
# * FOLDER
SWAGGER_DIR=./docs/swagger
# * FILE RUN GO
GO_SERVER_PRO := ./cmd/server/main.go
GO_SERVER_DEV:= ./fsnotify.go
# * DOCKER COMPOSE
DOCKER_COMPOSE_DEV := docker-compose.dev.yml
DOCKER_COMPOSE_PRO := docker-compose.pro.yml
tidy:
go mod tidy
dev:
go run $(GO_SERVER_DEV)
################# SEED #################
seed:
go run ./cmd/seed/main.go
################# DOCKER #################
build-pro:
docker-compose -f $(DOCKER_COMPOSE_PRO) up -d --build
down-pro:
docker-compose -f $(DOCKER_COMPOSE_PRO) down
build-dev:
docker-compose -f $(DOCKER_COMPOSE_DEV) up -d --build
down-dev:
docker-compose -f $(DOCKER_COMPOSE_DEV) down
################# MIGRATE #################
new_migration:
migrate create -ext sql -dir db/migrations -seq $(name)
migrate_version:
migrate -path db/migrations -database "$(DB_URL)" version
migrate_up_all:
migrate -path db/migrations -database "$(DB_URL)" up
migrate_down_all:
migrate -path db/migrations -database "$(DB_URL)" down
migrate_up:
migrate -path db/migrations -database "$(DB_URL)" up $(version)
migrate_down:
migrate -path db/migrations -database "$(DB_URL)" down $(version)
################# SWAGGER #################
swag:
swag init -g cmd/server/main.go -o ./docs/swagger --parseDependency --parseInternal
################# SQLC #################
sqlc:
sqlc generate

104
cmd/seed/main.go Normal file
View File

@@ -0,0 +1,104 @@
package main
import (
"context"
"fmt"
"wm-backend/configs"
"wm-backend/internal/initialization"
"wm-backend/internal/models"
"wm-backend/pkg/helper"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/rs/zerolog/log"
)
func main() {
cfg, err := configs.LoadConfig("configs")
if err != nil {
log.Fatal().Err(err).Msg("Error loading config")
}
pool, err := initialization.ConnectPostgreSQL(&cfg)
if err != nil {
log.Fatal().Err(err).Msg("Error connecting to database")
}
defer pool.Close()
err = seedAdmin(pool, &cfg)
if err != nil {
log.Fatal().Err(err).Msg("Error seeding admin")
}
log.Info().Msg("Seed completed successfully")
}
func seedAdmin(pool *pgxpool.Pool, cfg *models.Config) error {
ctx := context.Background()
// Check if admin user already exists
var exists bool
err := pool.QueryRow(ctx,
"SELECT EXISTS(SELECT 1 FROM users WHERE username = $1)",
cfg.Admin.Username,
).Scan(&exists)
if err != nil {
return fmt.Errorf("checking existing admin: %w", err)
}
if exists {
log.Info().Str("username", cfg.Admin.Username).Msg("Admin user already exists, skipping")
return nil
}
// Hash password
hashedPassword, err := helper.HashPassword(cfg.Admin.Password)
if err != nil {
return fmt.Errorf("hashing password: %w", err)
}
// Get SYS_ADMIN role ID
var roleID string
err = pool.QueryRow(ctx,
"SELECT id FROM roles WHERE name = 'SYS_ADMIN'",
).Scan(&roleID)
if err != nil {
return fmt.Errorf("SYS_ADMIN role not found (did init.sql run?): %w", err)
}
// Create admin user and assign role in a transaction
tx, err := pool.Begin(ctx)
if err != nil {
return fmt.Errorf("begin transaction: %w", err)
}
defer tx.Rollback(ctx)
var userID string
err = tx.QueryRow(ctx,
`INSERT INTO users (username, email, password_hash, full_name, is_active, created_by)
VALUES ($1, $2, $3, $4, TRUE, 'system')
RETURNING id`,
cfg.Admin.Username, cfg.Admin.Email, hashedPassword, cfg.Admin.FullName,
).Scan(&userID)
if err != nil {
return fmt.Errorf("creating admin user: %w", err)
}
_, err = tx.Exec(ctx,
`INSERT INTO user_roles (user_id, role_id) VALUES ($1, $2)`,
userID, roleID,
)
if err != nil {
return fmt.Errorf("assigning SYS_ADMIN role: %w", err)
}
if err := tx.Commit(ctx); err != nil {
return fmt.Errorf("commit transaction: %w", err)
}
log.Info().
Str("username", cfg.Admin.Username).
Str("email", cfg.Admin.Email).
Str("role", "SYS_ADMIN").
Msg("Admin user created successfully")
return nil
}

18
cmd/server/main.go Normal file
View File

@@ -0,0 +1,18 @@
package main
import (
"wm-backend/global"
"wm-backend/internal/routers"
_ "wm-backend/internal/services"
)
// @title Warehouse Management API
// @version 1.0
// @description This is the Warehouse Management API server.
// @host localhost:3000
// @BasePath /api/v1
func main() {
r := routers.NewRouter()
_ = global.Cfg // ensure config is loaded via init()
r.Run(":" + global.Cfg.Server.Port)
}

42
configs/config.go Normal file
View File

@@ -0,0 +1,42 @@
package configs
import (
"log"
"os"
"wm-backend/configs/constants"
"wm-backend/internal/models"
"github.com/joho/godotenv"
"github.com/spf13/viper"
)
func LoadConfig(path string) (config models.Config, err error) {
// Load environment variables from .env file
err = godotenv.Load()
if err != nil {
log.Fatalf("Error loading .env file: %v", err)
}
viper.AddConfigPath(path)
env := os.Getenv("ENV")
if env == constants.ProdEnvironment {
viper.SetConfigName("yaml/config.prod")
} else {
viper.SetConfigName("yaml/config.dev")
}
viper.SetConfigType("yaml")
viper.AutomaticEnv()
// Read the configuration file
if err := viper.ReadInConfig(); err != nil {
return config, err
}
// Unmarshal the configuration into the config struct
if err := viper.Unmarshal(&config); err != nil {
return config, err
}
return config, nil
}

View File

@@ -0,0 +1,22 @@
package constants
const (
DevEnvironment = "dev"
ProdEnvironment = "prod"
)
const (
API_VERSION_1 = "/api/v1"
)
const (
API_GROUP_AUTH = "/auth"
API_GROUP_WAREHOUSE = "/warehouses"
)
const (
API_PATH_PING = "/ping"
API_PATH_DOCS = "/swagger/*any"
API_PATH_AUTH_REGISTER = "/register"
API_PATH_AUTH_LOGIN = "/login"
)

View File

@@ -0,0 +1,71 @@
package constants
// Permission định nghĩa toàn bộ quyền trong hệ thống
// Format: {module}:{action}
// Giá trị string phải KHỚP với seed data trong db/init/init.sql
const (
// ── Warehouse (Kho) ──
PermWarehouseCreate = "warehouse:create"
PermWarehouseRead = "warehouse:read"
PermWarehouseUpdate = "warehouse:update"
PermWarehouseDelete = "warehouse:delete"
// ── Room (Phòng) ──
PermRoomCreate = "room:create"
PermRoomRead = "room:read"
PermRoomUpdate = "room:update"
PermRoomDelete = "room:delete"
// ── Cabinet (Tủ) ──
PermCabinetCreate = "cabinet:create"
PermCabinetRead = "cabinet:read"
PermCabinetUpdate = "cabinet:update"
PermCabinetDelete = "cabinet:delete"
// ── Shelf (Kệ) ──
PermShelfCreate = "shelf:create"
PermShelfRead = "shelf:read"
PermShelfUpdate = "shelf:update"
PermShelfDelete = "shelf:delete"
// ── Container (Vật chứa) ──
PermContainerCreate = "container:create"
PermContainerRead = "container:read"
PermContainerUpdate = "container:update"
PermContainerDelete = "container:delete"
// ── Component Type (Loại linh kiện) ──
PermComponentTypeCreate = "component_type:create"
PermComponentTypeRead = "component_type:read"
PermComponentTypeUpdate = "component_type:update"
PermComponentTypeDelete = "component_type:delete"
// ── Component (Linh kiện) ──
PermComponentCreate = "component:create"
PermComponentRead = "component:read"
PermComponentUpdate = "component:update"
PermComponentDelete = "component:delete"
// ── Invoice (Hóa đơn) ──
PermInvoiceCreate = "invoice:create"
PermInvoiceRead = "invoice:read"
PermInvoiceUpdate = "invoice:update"
PermInvoiceDelete = "invoice:delete"
PermInvoiceApprove = "invoice:approve"
// ── Stock (Kho) ──
PermStockImport = "stock:import"
PermStockExport = "stock:export"
PermStockAdjust = "stock:adjust"
PermStockTransfer = "stock:transfer"
PermStockRead = "stock:read"
// ── User (Người dùng) ──
PermUserCreate = "user:create"
PermUserRead = "user:read"
PermUserUpdate = "user:update"
PermUserDelete = "user:delete"
// ── Role (Vai trò & quyền) ──
PermRoleManage = "role:manage"
)

View File

@@ -0,0 +1,28 @@
server:
host: "localhost"
port: 3000
portfrontend: "http://localhost:8000"
keypassword: ""
admin:
username:
email:
password:
fullname:
jwt:
secretkey:
expirehours:
database:
username:
password:
name:
host:
port:
cache:
username:
password:
host:
port:

485
db/init/init.sql Normal file
View File

@@ -0,0 +1,485 @@
-- ============================================================
-- Warehouse Management Database - Init Script (PostgreSQL)
-- Based on WareHouseDB.md specification
-- ============================================================
-- ============================================================
-- ENUM Types
-- ============================================================
CREATE TYPE container_type_enum AS ENUM ('empty_box', 'tray', 'paper_box', 'plastic_box', 'bag', 'other');
CREATE TYPE component_item_status_enum AS ENUM ('normal', 'damaged', 'long_unused', 'expired', 'pending_inspection');
CREATE TYPE invoice_type_enum AS ENUM ('import', 'export');
CREATE TYPE invoice_status_enum AS ENUM ('draft', 'pending', 'approved', 'completed', 'cancelled');
CREATE TYPE transaction_type_enum AS ENUM ('import', 'export', 'adjustment', 'transfer');
-- ============================================================
-- Trigger function: auto-update updated_at
-- ============================================================
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- ============================================================
-- 1. warehouses (Kho)
-- ============================================================
CREATE TABLE warehouses (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT,
address VARCHAR(500),
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TRIGGER trg_warehouses_updated_at
BEFORE UPDATE ON warehouses
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- ============================================================
-- 2. rooms (Phòng)
-- ============================================================
CREATE TABLE rooms (
id BIGSERIAL PRIMARY KEY,
warehouse_id BIGINT NOT NULL REFERENCES warehouses(id),
name VARCHAR(255) NOT NULL,
description TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TRIGGER trg_rooms_updated_at
BEFORE UPDATE ON rooms
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- ============================================================
-- 3. cabinets (Tủ)
-- ============================================================
CREATE TABLE cabinets (
id BIGSERIAL PRIMARY KEY,
room_id BIGINT NOT NULL REFERENCES rooms(id),
name VARCHAR(255) NOT NULL,
description TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TRIGGER trg_cabinets_updated_at
BEFORE UPDATE ON cabinets
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- ============================================================
-- 4. shelves (Tầng / Kệ)
-- ============================================================
CREATE TABLE shelves (
id BIGSERIAL PRIMARY KEY,
cabinet_id BIGINT NOT NULL REFERENCES cabinets(id),
name VARCHAR(255) NOT NULL,
level_index INT NOT NULL,
description TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TRIGGER trg_shelves_updated_at
BEFORE UPDATE ON shelves
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- ============================================================
-- 5. containers (Vật chứa)
-- ============================================================
CREATE TABLE containers (
id BIGSERIAL PRIMARY KEY,
shelf_id BIGINT NOT NULL REFERENCES shelves(id),
name VARCHAR(255) NOT NULL,
container_type container_type_enum NOT NULL,
description TEXT,
max_capacity INT,
metadata JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TRIGGER trg_containers_updated_at
BEFORE UPDATE ON containers
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- ============================================================
-- 6. component_types (Loại linh kiện)
-- ============================================================
CREATE TABLE component_types (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT,
metadata JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT uk_component_types_name UNIQUE (name)
);
CREATE TRIGGER trg_component_types_updated_at
BEFORE UPDATE ON component_types
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- ============================================================
-- 7. components (Linh kiện)
-- ============================================================
CREATE TABLE components (
id BIGSERIAL PRIMARY KEY,
component_type_id BIGINT NOT NULL REFERENCES component_types(id),
name VARCHAR(255) NOT NULL,
description TEXT,
unit VARCHAR(50) NOT NULL DEFAULT 'cái',
total_quantity INT NOT NULL DEFAULT 0,
min_quantity INT NOT NULL DEFAULT 0,
metadata JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TRIGGER trg_components_updated_at
BEFORE UPDATE ON components
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- ============================================================
-- 8. component_codes (Mã linh kiện)
-- ============================================================
CREATE TABLE component_codes (
id BIGSERIAL PRIMARY KEY,
component_id BIGINT NOT NULL REFERENCES components(id),
code VARCHAR(255) NOT NULL,
code_type VARCHAR(100),
is_primary BOOLEAN NOT NULL DEFAULT FALSE,
metadata JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT uk_component_codes_code_type UNIQUE (code, code_type)
);
-- ============================================================
-- 9. component_items (Linh kiện tại từng vị trí)
-- ============================================================
CREATE TABLE component_items (
id BIGSERIAL PRIMARY KEY,
component_id BIGINT NOT NULL REFERENCES components(id),
container_id BIGINT NOT NULL REFERENCES containers(id),
quantity INT NOT NULL DEFAULT 0,
status component_item_status_enum NOT NULL DEFAULT 'normal',
metadata JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT uk_component_items_comp_cont_status UNIQUE (component_id, container_id, status)
);
CREATE TRIGGER trg_component_items_updated_at
BEFORE UPDATE ON component_items
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- ============================================================
-- 10. component_status_history (Lịch sử thay đổi tình trạng)
-- ============================================================
CREATE TABLE component_status_history (
id BIGSERIAL PRIMARY KEY,
component_item_id BIGINT NOT NULL REFERENCES component_items(id),
old_status component_item_status_enum,
new_status component_item_status_enum NOT NULL,
changed_quantity INT,
note TEXT,
changed_by VARCHAR(255),
changed_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- ============================================================
-- 11. invoice_configs (Cấu hình hóa đơn mẫu)
-- ============================================================
CREATE TABLE invoice_configs (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
type invoice_type_enum NOT NULL,
description TEXT,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
metadata JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TRIGGER trg_invoice_configs_updated_at
BEFORE UPDATE ON invoice_configs
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- ============================================================
-- 12. invoice_config_items (Chi tiết cấu hình hóa đơn)
-- ============================================================
CREATE TABLE invoice_config_items (
id BIGSERIAL PRIMARY KEY,
invoice_config_id BIGINT NOT NULL REFERENCES invoice_configs(id),
component_id BIGINT NOT NULL REFERENCES components(id),
required_quantity INT NOT NULL,
allow_alternative BOOLEAN NOT NULL DEFAULT FALSE,
priority_order INT NOT NULL DEFAULT 0,
note TEXT,
metadata JSONB,
CONSTRAINT uk_invoice_config_items_config_comp UNIQUE (invoice_config_id, component_id)
);
-- ============================================================
-- 13. alternative_components (Linh kiện thay thế)
-- ============================================================
CREATE TABLE alternative_components (
id BIGSERIAL PRIMARY KEY,
invoice_config_item_id BIGINT NOT NULL REFERENCES invoice_config_items(id),
alternative_component_id BIGINT NOT NULL REFERENCES components(id),
conversion_ratio DECIMAL(10,2) NOT NULL DEFAULT 1.00,
priority INT NOT NULL DEFAULT 0,
note TEXT,
metadata JSONB,
CONSTRAINT uk_alternative_components_item_alt UNIQUE (invoice_config_item_id, alternative_component_id)
);
-- ============================================================
-- 14. invoices (Hóa đơn nhập/xuất)
-- ============================================================
CREATE TABLE invoices (
id BIGSERIAL PRIMARY KEY,
invoice_code VARCHAR(100) NOT NULL,
type invoice_type_enum NOT NULL,
status invoice_status_enum NOT NULL DEFAULT 'draft',
invoice_config_id BIGINT REFERENCES invoice_configs(id),
total_items INT NOT NULL DEFAULT 0,
note TEXT,
created_by VARCHAR(255),
approved_by VARCHAR(255),
completed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
metadata JSONB,
CONSTRAINT uk_invoices_invoice_code UNIQUE (invoice_code)
);
CREATE TRIGGER trg_invoices_updated_at
BEFORE UPDATE ON invoices
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- ============================================================
-- 15. invoice_items (Chi tiết hóa đơn)
-- ============================================================
CREATE TABLE invoice_items (
id BIGSERIAL PRIMARY KEY,
invoice_id BIGINT NOT NULL REFERENCES invoices(id),
component_id BIGINT NOT NULL REFERENCES components(id),
original_component_id BIGINT REFERENCES components(id),
required_quantity INT NOT NULL,
actual_quantity INT NOT NULL DEFAULT 0,
is_substituted BOOLEAN NOT NULL DEFAULT FALSE,
is_short BOOLEAN NOT NULL DEFAULT FALSE,
shortage_quantity INT NOT NULL DEFAULT 0,
note TEXT,
metadata JSONB,
CONSTRAINT uk_invoice_items_invoice_comp UNIQUE (invoice_id, component_id)
);
-- ============================================================
-- 16. invoice_item_locations (Vị trí xuất/nhập cho từng item)
-- ============================================================
CREATE TABLE invoice_item_locations (
id BIGSERIAL PRIMARY KEY,
invoice_item_id BIGINT NOT NULL REFERENCES invoice_items(id),
container_id BIGINT NOT NULL REFERENCES containers(id),
quantity INT NOT NULL
);
-- ============================================================
-- 17. invoice_status_history (Lịch sử trạng thái hóa đơn)
-- ============================================================
CREATE TABLE invoice_status_history (
id BIGSERIAL PRIMARY KEY,
invoice_id BIGINT NOT NULL REFERENCES invoices(id),
old_status VARCHAR(50),
new_status VARCHAR(50) NOT NULL,
changed_by VARCHAR(255),
note TEXT,
changed_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- ============================================================
-- 18. stock_transactions (Lịch sử nhập xuất kho)
-- ============================================================
CREATE TABLE stock_transactions (
id BIGSERIAL PRIMARY KEY,
invoice_id BIGINT NOT NULL REFERENCES invoices(id),
component_id BIGINT NOT NULL REFERENCES components(id),
container_id BIGINT NOT NULL REFERENCES containers(id),
transaction_type transaction_type_enum NOT NULL,
quantity INT NOT NULL,
balance_after INT,
note TEXT,
created_by VARCHAR(255),
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
full_name VARCHAR(100),
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
created_by VARCHAR(50)
);
-- Bảng Roles: Lưu các vai trò (admin, editor, viewer...)
CREATE TABLE roles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(50) UNIQUE NOT NULL,
description VARCHAR(255),
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
created_by VARCHAR(50)
);
-- Bảng Permissions: Lưu các quyền hạn (read, write, delete...)
CREATE TABLE permissions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(100) UNIQUE NOT NULL,
description VARCHAR(255),
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
created_by VARCHAR(50)
);
-- Bảng user_roles: Liên kết user với role (N-N)
CREATE TABLE user_roles (
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
role_id UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
assigned_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (user_id, role_id)
);
-- Bảng role_permissions: Liên kết role với permission (N-N)
CREATE TABLE role_permissions (
role_id UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
permission_id UUID NOT NULL REFERENCES permissions(id) ON DELETE CASCADE,
assigned_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (role_id, permission_id)
);
-- ============================================================
-- Indexes
-- ============================================================
-- Comment: Tạo index cho việc tìm kiếm nhanh (tùy chọn)
CREATE INDEX idx_users_username ON users(username);
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_roles_name ON roles(name);
-- Index cho truy vấn nhanh
CREATE INDEX idx_user_roles_user_id ON user_roles(user_id);
CREATE INDEX idx_user_roles_role_id ON user_roles(role_id);
CREATE INDEX idx_role_permissions_role_id ON role_permissions(role_id);
CREATE INDEX idx_role_permissions_permission_id ON role_permissions(permission_id);
-- Tìm linh kiện theo vị trí
CREATE INDEX idx_component_items_component ON component_items(component_id);
CREATE INDEX idx_component_items_container ON component_items(container_id);
CREATE INDEX idx_component_items_status ON component_items(status);
-- Tìm mã linh kiện
CREATE INDEX idx_component_codes_code ON component_codes(code);
CREATE INDEX idx_component_codes_component ON component_codes(component_id);
-- Thống kê hóa đơn theo thời gian
CREATE INDEX idx_invoices_type_status ON invoices(type, status);
CREATE INDEX idx_invoices_created_at ON invoices(created_at);
CREATE INDEX idx_invoices_type_created ON invoices(type, created_at);
-- Thống kê giao dịch kho
CREATE INDEX idx_stock_transactions_component_date ON stock_transactions(component_id, created_at);
CREATE INDEX idx_stock_transactions_type_date ON stock_transactions(transaction_type, created_at);
CREATE INDEX idx_stock_transactions_invoice ON stock_transactions(invoice_id);
-- Tìm vị trí container (theo cấu trúc phân cấp)
CREATE INDEX idx_containers_shelf ON containers(shelf_id);
CREATE INDEX idx_shelves_cabinet ON shelves(cabinet_id);
CREATE INDEX idx_cabinets_room ON cabinets(room_id);
CREATE INDEX idx_rooms_warehouse ON rooms(warehouse_id);
-- ============================================================
-- Seed: Default permissions (theo module)
-- ============================================================
-- Warehouse module
INSERT INTO permissions (name, description) VALUES
('warehouse:create', 'Tạo kho mới'),
('warehouse:read', 'Xem thông tin kho'),
('warehouse:update', 'Cập nhật kho'),
('warehouse:delete', 'Xóa kho');
-- Room module
INSERT INTO permissions (name, description) VALUES
('room:create', 'Tạo phòng mới'),
('room:read', 'Xem thông tin phòng'),
('room:update', 'Cập nhật phòng'),
('room:delete', 'Xóa phòng');
-- Cabinet module
INSERT INTO permissions (name, description) VALUES
('cabinet:create', 'Tạo tủ mới'),
('cabinet:read', 'Xem thông tin tủ'),
('cabinet:update', 'Cập nhật tủ'),
('cabinet:delete', 'Xóa tủ');
-- Shelf module
INSERT INTO permissions (name, description) VALUES
('shelf:create', 'Tạo kệ mới'),
('shelf:read', 'Xem thông tin kệ'),
('shelf:update', 'Cập nhật kệ'),
('shelf:delete', 'Xóa kệ');
-- Container module
INSERT INTO permissions (name, description) VALUES
('container:create', 'Tạo vật chứa mới'),
('container:read', 'Xem thông tin vật chứa'),
('container:update', 'Cập nhật vật chứa'),
('container:delete', 'Xóa vật chứa');
-- Component Type module
INSERT INTO permissions (name, description) VALUES
('component_type:create', 'Tạo loại linh kiện mới'),
('component_type:read', 'Xem loại linh kiện'),
('component_type:update', 'Cập nhật loại linh kiện'),
('component_type:delete', 'Xóa loại linh kiện');
-- Component module
INSERT INTO permissions (name, description) VALUES
('component:create', 'Tạo linh kiện mới'),
('component:read', 'Xem thông tin linh kiện'),
('component:update', 'Cập nhật linh kiện'),
('component:delete', 'Xóa linh kiện');
-- Invoice module
INSERT INTO permissions (name, description) VALUES
('invoice:create', 'Tạo hóa đơn mới'),
('invoice:read', 'Xem thông tin hóa đơn'),
('invoice:update', 'Cập nhật hóa đơn'),
('invoice:delete', 'Xóa hóa đơn'),
('invoice:approve', 'Duyệt hóa đơn');
-- Stock module
INSERT INTO permissions (name, description) VALUES
('stock:import', 'Nhập kho'),
('stock:export', 'Xuất kho'),
('stock:adjust', 'Điều chỉnh tồn kho'),
('stock:transfer', 'Chuyển kho'),
('stock:read', 'Xem báo cáo tồn kho');
-- User & RBAC module
INSERT INTO permissions (name, description) VALUES
('user:create', 'Tạo người dùng mới'),
('user:read', 'Xem thông tin người dùng'),
('user:update', 'Cập nhật người dùng'),
('user:delete', 'Xóa người dùng'),
('role:manage', 'Quản lý vai trò và quyền hạn');
-- Seed: SYS_ADMIN role
INSERT INTO roles (name, description, created_by) VALUES
('SYS_ADMIN', 'Quản trị viên hệ thống - toàn quyền', 'system');
-- Gán TẤT CẢ permissions cho SYS_ADMIN
INSERT INTO role_permissions (role_id, permission_id)
SELECT r.id, p.id
FROM roles r
CROSS JOIN permissions p
WHERE r.name = 'SYS_ADMIN';

29
db/queries/cabinet.sql Normal file
View File

@@ -0,0 +1,29 @@
-- name: GetCabinetByID :one
SELECT * FROM cabinets
WHERE id = sqlc.arg(id);
-- name: ListCabinets :many
SELECT * FROM cabinets
ORDER BY created_at DESC;
-- name: CreateCabinet :one
INSERT INTO cabinets (room_id,name, description, created_at)
VALUES (
sqlc.arg(room_id),
sqlc.arg(name),
sqlc.arg(description),
sqlc.arg(created_at)
)
RETURNING *;
-- name: UpdateCabinet :one
UPDATE cabinets
SET name = coalesce(sqlc.arg(name), name),
description = coalesce(sqlc.arg(description), description),
updated_at = sqlc.arg(updated_at)
WHERE id = sqlc.arg(id)
RETURNING *;
-- name: DeleteCabinet :exec
DELETE FROM cabinets
WHERE id = sqlc.arg(id);

View File

26
db/queries/roles.sql Normal file
View File

@@ -0,0 +1,26 @@
-- name: GetRoleByID :one
SELECT * FROM roles
WHERE id = sqlc.arg(id);
-- name: ListRoles :many
SELECT * FROM roles
ORDER BY created_at DESC;
-- name: CreateRole :one
INSERT INTO roles (name, description, created_by)
VALUES (
sqlc.arg(name),
sqlc.arg(description),
sqlc.arg(created_by))
RETURNING *;
-- name: UpdateRole :one
UPDATE roles
SET name = sqlc.arg(name),
description = sqlc.arg(description)
WHERE id = sqlc.arg(id)
RETURNING *;
-- name: DeleteRole :exec
DELETE FROM roles
WHERE id = sqlc.arg(id);

29
db/queries/room.sql Normal file
View File

@@ -0,0 +1,29 @@
-- name: GetRoomByID :one
SELECT * FROM rooms
WHERE id = sqlc.arg(id);
-- name: ListRooms :many
SELECT * FROM rooms
ORDER BY created_at DESC;
-- name: CreateRoom :one
INSERT INTO rooms (warehouse_id,name, description, created_at)
VALUES (
sqlc.arg(warehouse_id),
sqlc.arg(name),
sqlc.arg(description),
sqlc.arg(created_at)
)
RETURNING *;
-- name: UpdateRoom :one
UPDATE rooms
SET name = coalesce(sqlc.arg(name), name),
description = coalesce(sqlc.arg(description), description),
updated_at = sqlc.arg(updated_at)
WHERE id = sqlc.arg(id)
RETURNING *;
-- name: DeleteRoom :exec
DELETE FROM rooms
WHERE id = sqlc.arg(id);

36
db/queries/user_roles.sql Normal file
View File

@@ -0,0 +1,36 @@
-- name: GetUserRolesByUserID :many
SELECT ur.*, r.name AS role_name, r.description AS role_description
FROM user_roles ur
JOIN roles r ON r.id = ur.role_id
WHERE ur.user_id = sqlc.arg(user_id)
ORDER BY ur.assigned_at DESC;
-- name: GetUserRolesByRoleID :many
SELECT ur.*, u.username, u.email, u.full_name
FROM user_roles ur
JOIN users u ON u.id = ur.user_id
WHERE ur.role_id = sqlc.arg(role_id)
ORDER BY ur.assigned_at DESC;
-- name: GetUserRole :one
SELECT * FROM user_roles
WHERE user_id = sqlc.arg(user_id) AND role_id = sqlc.arg(role_id);
-- name: AssignRoleToUser :one
INSERT INTO user_roles (user_id, role_id)
VALUES (
sqlc.arg(user_id),
sqlc.arg(role_id))
RETURNING *;
-- name: RemoveRoleFromUser :exec
DELETE FROM user_roles
WHERE user_id = sqlc.arg(user_id) AND role_id = sqlc.arg(role_id);
-- name: RemoveAllRolesFromUser :exec
DELETE FROM user_roles
WHERE user_id = sqlc.arg(user_id);
-- name: CountUsersByRoleID :one
SELECT COUNT(*) FROM user_roles
WHERE role_id = sqlc.arg(role_id);

23
db/queries/users.sql Normal file
View File

@@ -0,0 +1,23 @@
-- name: GetUserByID :one
SELECT * FROM users
WHERE id = sqlc.arg(id);
-- name: GetUserByEmail :one
SELECT * FROM users
WHERE email = sqlc.arg(email)
LIMIT 1;
-- name: GetUserByUsername :one
SELECT * FROM users
WHERE username = sqlc.arg(username)
LIMIT 1;
-- name: CreateUser :one
INSERT INTO users (username, email, password_hash, full_name, created_by)
VALUES (
sqlc.arg(username),
sqlc.arg(email),
sqlc.arg(password_hash),
sqlc.arg(full_name),
sqlc.arg(created_by))
RETURNING id;

30
db/queries/warehouse.sql Normal file
View File

@@ -0,0 +1,30 @@
-- name: GetWarehouseByID :one
SELECT * FROM warehouses
WHERE id = sqlc.arg(id);
-- name: ListWarehouses :many
SELECT * FROM warehouses
ORDER BY created_at DESC;
-- name: CreateWarehouse :one
INSERT INTO warehouses (name, description, address, created_at)
VALUES (
sqlc.arg(name),
sqlc.arg(description),
sqlc.arg(address),
sqlc.arg(created_at)
)
RETURNING *;
-- name: UpdateWarehouse :one
UPDATE warehouses
SET name = CASE WHEN sqlc.arg(name) = '' THEN name ELSE sqlc.arg(name) END,
description = coalesce(sqlc.arg(description), description),
address = coalesce(sqlc.arg(address), address),
updated_at = sqlc.arg(updated_at)
WHERE id = sqlc.arg(id)
RETURNING *;
-- name: DeleteWarehouse :exec
DELETE FROM warehouses
WHERE id = sqlc.arg(id);

47
docker-compose.dev.yml Normal file
View File

@@ -0,0 +1,47 @@
services:
postgres:
container_name: wm-postgres
image: postgres:18-alpine
ports:
- "${POSTGRES_PORT_MAPPING}:${POSTGRES_PORT}"
environment:
- POSTGRES_DB=${POSTGRES_DB}
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
env_file:
- .env
volumes:
- postgres_data:/var/lib/postgresql
- ./db/init/init.sql:/docker-entrypoint-initdb.d/01_init.sql
restart: unless-stopped
healthcheck:
test:
[
"CMD-SHELL",
"sh -c 'pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}'",
]
interval: 10s
timeout: 3s
retries: 5
start_period: 30s
redis:
container_name: wm-redis
image: redis:8-alpine
command: redis-server --requirepass ${REDIS_PASSWORD}
ports:
- "${REDIS_PORT_MAPPING}:${REDIS_PORT}"
restart: unless-stopped
env_file:
- .env
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "--pass", "${REDIS_PASSWORD}", "ping"]
interval: 10s
timeout: 3s
retries: 5
start_period: 30s
volumes:
postgres_data:
redis_data:

427
docs/db/WareHouseDB.md Normal file
View File

@@ -0,0 +1,427 @@
# Warehouse Management Database Schema
## ER Diagram Overview
```
Warehouse 1──N Room 1──N Cabinet 1──N Shelf 1──N Container
M──N Component (qua ComponentItem)
ComponentType 1──N Component 1──N ComponentCode
├── ComponentItem (instance tại từng Container)
└── ComponentStatusHistory (lịch sử thay đổi tình trạng)
InvoiceConfig 1──N InvoiceConfigItem 1──N AlternativeComponent
Invoice 1──N InvoiceItem
└── InvoiceStatusHistory (lịch sử trạng thái hóa đơn)
```
---
## Tables
### 1. warehouses (Kho)
| Column | Type | Constraints | Description |
| ------------ | ------------ | -------------------- | ------------------ |
| id | BIGINT | PK, AUTO_INCREMENT | |
| name | VARCHAR(255) | NOT NULL | Tên kho |
| description | TEXT | | Mô tả |
| address | VARCHAR(500) | | Địa chỉ |
| created_at | DATETIME | DEFAULT NOW() | |
| updated_at | DATETIME | DEFAULT NOW() | |
### 2. rooms (Phòng)
| Column | Type | Constraints | Description |
| ------------ | ------------ | -------------------- | ------------------ |
| id | BIGINT | PK, AUTO_INCREMENT | |
| warehouse_id | BIGINT | FK → warehouses(id) | Thuộc kho nào |
| name | VARCHAR(255) | NOT NULL | Tên phòng |
| description | TEXT | | Mô tả |
| created_at | DATETIME | DEFAULT NOW() | |
| updated_at | DATETIME | DEFAULT NOW() | |
### 3. cabinets (Tủ)
| Column | Type | Constraints | Description |
| ------------ | ------------ | -------------------- | ------------------ |
| id | BIGINT | PK, AUTO_INCREMENT | |
| room_id | BIGINT | FK → rooms(id) | Thuộc phòng nào |
| name | VARCHAR(255) | NOT NULL | Tên tủ |
| description | TEXT | | Mô tả |
| created_at | DATETIME | DEFAULT NOW() | |
| updated_at | DATETIME | DEFAULT NOW() | |
### 4. shelves (Tầng / Kệ)
| Column | Type | Constraints | Description |
| ------------ | ------------ | -------------------- | -------------------- |
| id | BIGINT | PK, AUTO_INCREMENT | |
| cabinet_id | BIGINT | FK → cabinets(id) | Thuộc tủ nào |
| name | VARCHAR(255) | NOT NULL | Tên tầng (VD: Tầng 1)|
| level_index | INT | NOT NULL | Thứ tự tầng |
| description | TEXT | | Mô tả |
| created_at | DATETIME | DEFAULT NOW() | |
| updated_at | DATETIME | DEFAULT NOW() | |
### 5. containers (Vật chứa: thùng rỗng, khay, thùng giấy, ...)
| Column | Type | Constraints | Description |
| ------------ | ------------ | -------------------- | ----------------------------- |
| id | BIGINT | PK, AUTO_INCREMENT | |
| shelf_id | BIGINT | FK → shelves(id) | Nằm trên tầng nào |
| name | VARCHAR(255) | NOT NULL | Tên vật chứa |
| container_type | ENUM('empty_box', 'tray', 'paper_box', 'plastic_box', 'bag', 'other') | NOT NULL | Loại vật chứa |
| description | TEXT | | Mô tả |
| max_capacity | INT | | Sức chứa tối đa (số linh kiện)|
| created_at | DATETIME | DEFAULT NOW() | |
| updated_at | DATETIME | DEFAULT NOW() | |
### 6. component_types (Loại linh kiện)
| Column | Type | Constraints | Description |
| ------------ | ------------ | -------------------- | ---------------------------- |
| id | BIGINT | PK, AUTO_INCREMENT | |
| name | VARCHAR(255) | NOT NULL, UNIQUE | Tên loại (VD: Resistor, IC) |
| description | TEXT | | Mô tả |
| created_at | DATETIME | DEFAULT NOW() | |
| updated_at | DATETIME | DEFAULT NOW() | |
### 7. components (Linh kiện)
| Column | Type | Constraints | Description |
| ----------------- | ------------ | ------------------------- | ----------------------------- |
| id | BIGINT | PK, AUTO_INCREMENT | |
| component_type_id | BIGINT | FK → component_types(id) | Thuộc loại nào |
| name | VARCHAR(255) | NOT NULL | Tên linh kiện |
| description | TEXT | | Mô tả chi tiết |
| unit | VARCHAR(50) | DEFAULT 'cái' | Đơn vị tính (cái, m, kg, ...) |
| total_quantity | INT | DEFAULT 0 | Tổng số lượng (tính tự động) |
| min_quantity | INT | DEFAULT 0 | Số lượng tối thiểu (cảnh báo) |
| created_at | DATETIME | DEFAULT NOW() | |
| updated_at | DATETIME | DEFAULT NOW() | |
> **Note**: `total_quantity` được tính tổng từ `component_items.quantity` và nên được cache/update qua trigger hoặc application logic.
### 8. component_codes (Mã linh kiện - 1 linh kiện có thể có nhiều mã)
| Column | Type | Constraints | Description |
| ------------ | ------------ | ----------------------- | -------------------------------- |
| id | BIGINT | PK, AUTO_INCREMENT | |
| component_id | BIGINT | FK → components(id) | Thuộc linh kiện nào |
| code | VARCHAR(255) | NOT NULL | Mã (VD: ESP32-WROOM-32D) |
| code_type | VARCHAR(100) | | Loại mã (VD: SKU, Part Number) |
| is_primary | BOOLEAN | DEFAULT FALSE | Mã chính |
| created_at | DATETIME | DEFAULT NOW() | |
```
UNIQUE (code, code_type)
```
### 9. component_items (Linh kiện tại từng vị trí - nằm trong container)
| Column | Type | Constraints | Description |
| ------------ | ------- | ------------------------------------ | ------------------------------ |
| id | BIGINT | PK, AUTO_INCREMENT | |
| component_id | BIGINT | FK → components(id) | Linh kiện nào |
| container_id | BIGINT | FK → containers(id) | Nằm trong vật chứa nào |
| quantity | INT | NOT NULL, DEFAULT 0 | Số lượng tại vị trí này |
| status | ENUM('normal', 'damaged', 'long_unused', 'expired', 'pending_inspection') | NOT NULL DEFAULT 'normal' | Tình trạng |
| created_at | DATETIME| DEFAULT NOW() | |
| updated_at | DATETIME| DEFAULT NOW() | |
```
UNIQUE (component_id, container_id, status)
```
> **Note**: Cùng 1 linh kiện ở cùng 1 container nhưng khác status sẽ là các record khác nhau. Điều này cho phép phân biệt linh kiện hỏng và bình thường trong cùng vị trí.
### 10. component_status_history (Lịch sử thay đổi tình trạng linh kiện)
| Column | Type | Constraints | Description |
| ---------------- | ---------- | --------------------------------- | ----------------------------------- |
| id | BIGINT | PK, AUTO_INCREMENT | |
| component_item_id| BIGINT | FK → component_items(id) | Linh kiện item nào |
| old_status | ENUM (như component_items.status) | | Tình trạng cũ |
| new_status | ENUM (như component_items.status) | | Tình trạng mới |
| changed_quantity | INT | | Số lượng bị thay đổi tình trạng |
| note | TEXT | | Ghi chú lý do |
| changed_by | VARCHAR(255)| | Người thay đổi |
| changed_at | DATETIME | DEFAULT NOW() | |
### 11. invoice_configs (Cấu hình hóa đơn mẫu)
| Column | Type | Constraints | Description |
| ------------ | ------------ | -------------------- | -------------------------------- |
| id | BIGINT | PK, AUTO_INCREMENT | |
| name | VARCHAR(255) | NOT NULL | Tên cấu hình (VD: Kit xuất kho A)|
| type | ENUM('import', 'export') | NOT NULL | Loại hóa đơn |
| description | TEXT | | Mô tả |
| is_active | BOOLEAN | DEFAULT TRUE | Đang sử dụng |
| created_at | DATETIME | DEFAULT NOW() | |
| updated_at | DATETIME | DEFAULT NOW() | |
### 12. invoice_config_items (Chi tiết cấu hình hóa đơn - linh kiện cần xuất/nhập)
| Column | Type | Constraints | Description |
| ----------------- | ------------ | ---------------------------------- | ----------------------- |
| id | BIGINT | PK, AUTO_INCREMENT | |
| invoice_config_id | BIGINT | FK → invoice_configs(id) | Thuộc cấu hình nào |
| component_id | BIGINT | FK → components(id) | Linh kiện cần |
| required_quantity | INT | NOT NULL | Số lượng yêu cầu |
| allow_alternative | BOOLEAN | DEFAULT FALSE | Cho phép dùng linh kiện thay thế |
| priority_order | INT | DEFAULT 0 | Thứ tự ưu tiên |
| note | TEXT | | Ghi chú |
```
UNIQUE (invoice_config_id, component_id)
```
### 13. alternative_components (Linh kiện thay thế - dùng khi thiếu)
| Column | Type | Constraints | Description |
| ---------------------- | ------- | ---------------------------------------- | ---------------------------------- |
| id | BIGINT | PK, AUTO_INCREMENT | |
| invoice_config_item_id | BIGINT | FK → invoice_config_items(id) | Item cấu hình nào |
| alternative_component_id | BIGINT | FK → components(id) | Linh kiện thay thế |
| conversion_ratio | DECIMAL(10,2) | DEFAULT 1.0 | Tỷ lệ quy đổi (VD: 2 cái nhỏ = 1 cái lớn) |
| priority | INT | DEFAULT 0 | Thứ tự ưu tiên khi thay thế |
| note | TEXT | | Ghi chú |
```
UNIQUE (invoice_config_item_id, alternative_component_id)
```
> **Ví dụ**: Nếu cấu hình yêu cầu "IC WiFi ESP32" nhưng hết, có thể dùng "IC WiFi ESP32-C3" thay thế với `conversion_ratio = 1.0`.
### 14. invoices (Hóa đơn nhập / xuất)
| Column | Type | Constraints | Description |
| ----------------- | ------------ | ---------------------------- | ---------------------------------------- |
| id | BIGINT | PK, AUTO_INCREMENT | |
| invoice_code | VARCHAR(100) | NOT NULL, UNIQUE | Mã hóa đơn (tự generate) |
| type | ENUM('import', 'export') | NOT NULL | Loại hóa đơn |
| status | ENUM('draft', 'pending', 'approved', 'completed', 'cancelled') | NOT NULL DEFAULT 'draft' | Trạng thái |
| invoice_config_id | BIGINT | FK → invoice_configs(id), NULL | Cấu hình áp dụng (có thể null) |
| total_items | INT | DEFAULT 0 | Tổng số loại linh kiện |
| note | TEXT | | Ghi chú |
| created_by | VARCHAR(255) | | Người tạo |
| approved_by | VARCHAR(255) | | Người duyệt |
| completed_at | DATETIME | | Thời gian hoàn thành |
| created_at | DATETIME | DEFAULT NOW() | |
| updated_at | DATETIME | DEFAULT NOW() | |
### 15. invoice_items (Chi tiết hóa đơn - từng linh kiện)
| Column | Type | Constraints | Description |
| ----------------- | ------------ | ---------------------------------- | -------------------------------- |
| id | BIGINT | PK, AUTO_INCREMENT | |
| invoice_id | BIGINT | FK → invoices(id) | Thuộc hóa đơn nào |
| component_id | BIGINT | FK → components(id) | Linh kiện |
| original_component_id | BIGINT | FK → components(id), NULL | Linh kiện gốc (nếu dùng thay thế)|
| required_quantity | INT | NOT NULL | Số lượng yêu cầu |
| actual_quantity | INT | DEFAULT 0 | Số lượng thực tế |
| is_substituted | BOOLEAN | DEFAULT FALSE | Đã dùng linh kiện thay thế |
| is_short | BOOLEAN | DEFAULT FALSE | Có thiếu hay không |
| shortage_quantity | INT | DEFAULT 0 | Số lượng thiếu |
| note | TEXT | | Ghi chú |
```
UNIQUE (invoice_id, component_id)
```
### 16. invoice_item_locations (Chi tiết vị trí xuất/nhập cho từng item)
| Column | Type | Constraints | Description |
| --------------- | ------- | --------------------------------- | ------------------------------------ |
| id | BIGINT | PK, AUTO_INCREMENT | |
| invoice_item_id | BIGINT | FK → invoice_items(id) | Item hóa đơn nào |
| container_id | BIGINT | FK → containers(id) | Xuất/nhập từ container nào |
| quantity | INT | NOT NULL | Số lượng xuất/nhập tại vị trí này |
> **Note**: Bảng này cho phép xuất 1 linh kiện từ nhiều vị trí khác nhau, và ghi lại chính xác vị trí.
### 17. invoice_status_history (Lịch sử trạng thái hóa đơn)
| Column | Type | Constraints | Description |
| ---------- | ------------ | ---------------------------- | -------------------- |
| id | BIGINT | PK, AUTO_INCREMENT | |
| invoice_id | BIGINT | FK → invoices(id) | Hóa đơn nào |
| old_status | VARCHAR(50) | | Trạng thái cũ |
| new_status | VARCHAR(50) | NOT NULL | Trạng thái mới |
| changed_by | VARCHAR(255) | | Người thay đổi |
| note | TEXT | | Ghi chú |
| changed_at | DATETIME | DEFAULT NOW() | |
### 18. stock_transactions (Lịch sử nhập xuất kho - phục vụ thống kê)
| Column | Type | Constraints | Description |
| ----------------- | ------------ | ---------------------------------- | ----------------------------- |
| id | BIGINT | PK, AUTO_INCREMENT | |
| invoice_id | BIGINT | FK → invoices(id) | Hóa đơn liên quan |
| component_id | BIGINT | FK → components(id) | Linh kiện |
| container_id | BIGINT | FK → containers(id) | Vị trí |
| transaction_type | ENUM('import', 'export', 'adjustment', 'transfer') | NOT NULL | Loại giao dịch |
| quantity | INT | NOT NULL | Số lượng (âm = xuất, dương = nhập) |
| balance_after | INT | | Số dư sau giao dịch |
| note | TEXT | | Ghi chú |
| created_by | VARCHAR(255) | | Người thực hiện |
| created_at | DATETIME | DEFAULT NOW() | |
> **Note**: Bảng này là bảng tổng hợp để phục vụ thống kê nhanh, tránh phải tính toán từ invoice_items mỗi lần.
---
## Indexes (Quan trọng)
```sql
-- Tìm linh kiện theo vị trí
CREATE INDEX idx_component_items_component ON component_items(component_id);
CREATE INDEX idx_component_items_container ON component_items(container_id);
CREATE INDEX idx_component_items_status ON component_items(status);
-- Tìm mã linh kiện
CREATE INDEX idx_component_codes_code ON component_codes(code);
CREATE INDEX idx_component_codes_component ON component_codes(component_id);
-- Thống kê hóa đơn theo thời gian
CREATE INDEX idx_invoices_type_status ON invoices(type, status);
CREATE INDEX idx_invoices_created_at ON invoices(created_at);
CREATE INDEX idx_invoices_type_created ON invoices(type, created_at);
-- Thống kê giao dịch kho
CREATE INDEX idx_stock_transactions_component_date ON stock_transactions(component_id, created_at);
CREATE INDEX idx_stock_transactions_type_date ON stock_transactions(transaction_type, created_at);
CREATE INDEX idx_stock_transactions_invoice ON stock_transactions(invoice_id);
-- Tìm vị trí container
CREATE INDEX idx_containers_shelf ON containers(shelf_id);
CREATE INDEX idx_shelves_cabinet ON shelves(cabinet_id);
CREATE INDEX idx_cabinets_room ON cabinets(room_id);
CREATE INDEX idx_rooms_warehouse ON rooms(warehouse_id);
```
---
## Ví dụ truy vấn phổ biến
### 1. Tìm vị trí của 1 linh kiện
```sql
SELECT
c.name AS component_name,
ct.name AS type_name,
ci.quantity,
ci.status,
cn.name AS container_name,
cn.container_type,
s.name AS shelf_name,
cb.name AS cabinet_name,
r.name AS room_name,
w.name AS warehouse_name
FROM component_items ci
JOIN components c ON ci.component_id = c.id
JOIN component_types ct ON c.component_type_id = ct.id
JOIN containers cn ON ci.container_id = cn.id
JOIN shelves s ON cn.shelf_id = s.id
JOIN cabinets cb ON s.cabinet_id = cb.id
JOIN rooms r ON cb.room_id = r.id
JOIN warehouses w ON r.warehouse_id = w.id
WHERE ci.component_id = :componentId AND ci.quantity > 0;
```
### 2. Kiểm tra thiếu hụt khi xuất hóa đơn
```sql
SELECT
ici.component_id,
c.name AS component_name,
ici.required_quantity,
COALESCE(SUM(ci.quantity), 0) AS available_quantity,
CASE
WHEN COALESCE(SUM(ci.quantity), 0) < ici.required_quantity THEN TRUE
ELSE FALSE
END AS is_short,
ici.allow_alternative
FROM invoice_config_items ici
JOIN components c ON ici.component_id = c.id
LEFT JOIN component_items ci ON ci.component_id = ici.component_id
AND ci.status = 'normal'
WHERE ici.invoice_config_id = :configId
GROUP BY ici.component_id, c.name, ici.required_quantity, ici.allow_alternative;
```
### 3. Thống kê nhập xuất theo thời gian
```sql
SELECT
st.transaction_type,
c.name AS component_name,
SUM(ABS(st.quantity)) AS total_quantity,
DATE(st.created_at) AS transaction_date
FROM stock_transactions st
JOIN components c ON st.component_id = c.id
WHERE st.created_at BETWEEN :startDate AND :endDate
GROUP BY st.transaction_type, c.id, DATE(st.created_at)
ORDER BY transaction_date DESC;
```
### 4. Linh kiện sắp hết (dưới mức tối thiểu)
```sql
SELECT
c.id,
c.name,
c.total_quantity,
c.min_quantity,
ct.name AS type_name
FROM components c
JOIN component_types ct ON c.component_type_id = ct.id
WHERE c.total_quantity <= c.min_quantity AND c.min_quantity > 0;
```
---
## Flow xử lý hóa đơn xuất
```
1. Tạo Invoice (status: draft)
2. Chọn InvoiceConfig hoặc thêm items thủ công
3. Kiểm tra tồn kho:
- Query component_items WHERE status = 'normal'
- So sánh available vs required
- Nếu thiếu:
a. Cảnh báo item thiếu
b. Kiểm tra allow_alternative
c. Query alternative_components
d. Đề xuất linh kiện thay thế
4. Chấp nhận / Điều chỉnh (status: approved)
5. Thực hiện xuất kho:
- Trừ quantity trong component_items
- Tạo stock_transactions
- Cập nhật total_quantity trong components
- Tạo invoice_item_locations
6. Hoàn thành (status: completed)
```
---
## Enum Values Reference
| Enum Field | Values |
| ---------------- | --------------------------------------------------- |
| container_type | `empty_box`, `tray`, `paper_box`, `plastic_box`, `bag`, `other` |
| status (item) | `normal`, `damaged`, `long_unused`, `expired`, `pending_inspection` |
| invoice type | `import`, `export` |
| invoice status | `draft`, `pending`, `approved`, `completed`, `cancelled` |
| transaction_type | `import`, `export`, `adjustment`, `transfer` |

170
docs/sqlc/config.md Normal file
View File

@@ -0,0 +1,170 @@
# sqlc.yaml — Configuration Reference (Version 2)
sqlc sử dụng **Configuration Version 2** khi khai báo `version: "2"` ở đầu file. Dưới đây là bộ quy tắc và danh sách đầy đủ các key.
---
## Cấu trúc tổng thể
```yaml
version: "2"
sql:
- engine: "<engine>"
queries: "<path>"
schema: "<path>"
gen:
go:
package: "<name>"
out: "<path>"
```
---
## Các key cấp gốc (root-level)
| Key | Kiểu | Bắt buộc | Mô tả |
| ----------- | ----- | -------- | ------------------------------------------------------------------------ |
| `version` | `"2"` | ✅ | Khai báo phiên bản cấu hình. Giá trị `"2"` cho Version 2. |
| `sql` | list | ✅ | Danh sách các block cấu hình, mỗi block sinh code cho một ngôn ngữ đích. |
| `overrides` | map | ❌ | (Deprecated v1) — Không dùng trong v2, thay bằng `gen.<lang>.overrides`. |
| `rename` | map | ❌ | (Deprecated v1) — Không dùng trong v2, thay bằng `gen.<lang>.rename`. |
---
## Các key bên trong mỗi phần tử của `sql[]`
| Key | Kiểu | Bắt buộc | Mô tả |
| ------------------------ | ----------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `engine` | string | ✅ | CSDL mục tiêu. Giá trị: `"postgresql"`, `"mysql"`, `"sqlite"`. |
| `queries` | string / string[] | ✅ | Đường dẫn tới thư mục/file chứa các query `.sql`. Có thể là mảng nhiều path. |
| `schema` | string / string[] | ✅ | Đường dẫn tới thư mục/file chứa schema DDL (`.sql`). |
| `strict_function_checks` | bool | ❌ | Nếu `true`, sqlc sẽ báo lỗi khi gọi function không tồn tại trong schema. Mặc định `false`. |
| `strict_order_by` | bool | ❌ | Nếu `true`, yêu cầu tất cả column trong `ORDER BY` phải tồn tại. Mặc định `false` (chỉ cho PostgreSQL). |
| `query_parameter_limit` | int | ❌ | Giới hạn số lượng parameter trong một query. Mặc định `1` (nếu > 1 thì sqlc ưu tiên sinh `sql.NamedArg`). Đặt `0` để bỏ giới hạn. |
| `codegen` | list | ❌ | Danh sách cấu hình cho **plugin codegen** bên ngoài. Mỗi item có `out`, `plugin`, `options`. |
| `gen` | map | ✅ (ít nhất 1) | Map các ngôn ngữ sinh code. Các key con: `go`, `kotlin`, `python`, `json`, `typescript`, `java`, `swift`, `rust`, `csharp`. |
---
## Các key bên trong `gen.go`
| Key | Kiểu | Bắt buộc | Mô tả |
| -------------------------------- | ------ | -------- | ----------------------------------------------------------------------------------------------------------------- |
| `out` | string | ✅ | Thư mục output cho Go code sinh ra. |
| `package` | string | ✅ | Tên Go package. |
| `sql_package` | string | ❌ | Package SQL driver. Giá trị: `"database/sql"`, `"pgx/v4"`, `"pgx/v5"`, `"lib/pq"`. Mặc định `"database/sql"`. |
| `sql_driver` | string | ❌ | Tên driver cụ thể, dùng để sinh import đúng. VD: `"github.com/jackc/pgx/v5/stdlib"`. |
| `emit_json_tags` | bool | ❌ | Nếu `true`, sinh `json:"column_name"` tag cho struct field. Mặc định `false`. |
| `emit_db_tags` | bool | ❌ | Nếu `true`, sinh `db:"column_name"` tag. Mặc định `false`. |
| `emit_prepared_queries` | bool | ❌ | Nếu `true`, sinh method `Prepare` cho mỗi query. Mặc định `false`. |
| `emit_interface` | bool | ❌ | Nếu `true`, sinh interface ` Querier` thay vì chỉ struct. Mặc định `false`. |
| `emit_empty_slices` | bool | ❌ | Nếu `true`, trả về `[]T` rỗng thay vì `nil` khi không có row. Mặc định `false`. |
| `emit_result_struct_pointers` | bool | ❌ | Sinh con trỏ `*T` cho result struct. Mặc định `false`. |
| `emit_params_struct_pointers` | bool | ❌ | Sinh con trỏ `*T` cho params struct. Mặc định `false`. |
| `emit_method_with_db_argument` | bool | ❌ | Nếu `true`, mỗi method nhận thêm `DB` argument, cho phép dùng transaction dễ hơn. Mặc định `false`. |
| `emit_pointers_for_null_types` | bool | ❌ | Nếu `true`, dùng con trỏ cho null type thay vì `sql.Null*`. Mặc định `false`. |
| `emit_enum_valid_method` | bool | ❌ | Sinh method `Valid()` cho enum type. Mặc định `false`. |
| `emit_all_enum_values` | bool | ❌ | Sinh constant cho tất cả giá trị enum. Mặc định `false`. |
| `emit_build_tags` | string | ❌ | Thêm Go build tag vào file sinh ra. VD: `"//go:build linux"`. |
| `json_tags_case_style` | string | ❌ | Style cho JSON tag. Giá trị: `"camel"`, `"pascal"`, `"snake"`, `"none"`. Mặc định phụ thuộc vào `emit_json_tags`. |
| `output_db_file_name` | string | ❌ | Tên file chứa `DB` struct. Mặc định `"db.go"`. |
| `output_models_file_name` | string | ❌ | Tên file chứa model struct. Mặc định `"models.go"`. |
| `output_querier_file_name` | string | ❌ | Tên file chứa interface. Mặc định `"querier.go"`. |
| `output_files_suffix` | string | ❌ | Hậu tố cho file query. Mặc định `""`. VD: `"_sql"``user_sql.go`. |
| `inflection_exclude_table_names` | list | ❌ | Danh sách tên table không áp dụng quy tắc số nhiều. VD: `["user"]`. |
| `overrides` | list | ❌ | Ghi đè kiểu dữ liệu cho column cụ thể hoặc cho kiểu Go toàn cục (xem chi tiết bên dưới). |
| `rename` | map | ❌ | Map đổi tên. Key = tên cần đổi, Value = tên mới. Dùng để rename struct field. |
| `import` | string | ❌ | Import path của Go module dùng trong generated code. |
---
## Cấu trúc của `overrides[]` (bên trong `gen.go`)
```yaml
overrides:
- db_type: "uuid"
go_type: "github.com/google/uuid.UUID"
- db_type: "timestamptz"
go_type: "time.Time"
- column: "users.status"
go_type: "UserStatus"
go_struct_tag:
tags:
json: "status,omitempty"
- db_type: "text"
go_type:
import: "github.com/lib/pq"
type: "StringArray"
nullable: true
```
| Key | Mô tả |
| --------------- | ------------------------------------------------------------------------- |
| `db_type` | Kiểu dữ liệu SQL cần ghi đè. Dùng cùng với `go_type`. |
| `column` | Đường dẫn `"table.column"` cụ thể. Ưu tiên cao hơn `db_type`. |
| `go_type` | Kiểu Go thay thế. Có thể là string hoặc object `{import, type, pointer}`. |
| `nullable` | bool — Nếu `true`, áp dụng cho phiên bản nullable của kiểu. |
| `go_struct_tag` | Custom struct tag cho field. |
---
## `gen.json`
| Key | Kiểu | Bắt buộc | Mô tả |
| ---------- | ------ | -------- | --------------------------------------------------- |
| `out` | string | ✅ | Thư mục output file JSON. |
| `indent` | string | ❌ | Ký tự indent. Mặc định `" "`. |
| `filename` | string | ❌ | Tên file output. Mặc định `"codegen_request.json"`. |
---
## `gen.typescript`
| Key | Kiểu | Bắt buộc | Mô tả |
| ------------------- | ------ | -------- | ------------------------------------------------------ |
| `out` | string | ✅ | Thư mục output. |
| `plugin` | string | ❌ | Tên plugin (nếu dùng plugin ngoài). |
| `runtime` | string | ❌ | Runtime cho generated code: `"node"` hoặc `"browser"`. |
| `driver` | string | ❌ | Driver: `"pg"` hoặc `"pg-query-stream"`. |
| `emit_json_tags` | bool | ❌ | Sinh JSON tag cho property. |
| `emit_result_types` | bool | ❌ | Sinh interface cho result. |
---
## Ví dụ hoàn chỉnh
```yaml
version: "2"
sql:
- engine: "postgresql"
queries: "query/"
schema: "schema/"
gen:
go:
package: "db"
out: "db"
sql_package: "pgx/v5"
emit_json_tags: true
emit_interface: true
emit_empty_slices: true
emit_prepared_queries: false
json_tags_case_style: "camel"
overrides:
- db_type: "uuid"
go_type: "github.com/google/uuid.UUID"
- db_type: "timestamptz"
go_type: "time.Time"
- column: "orders.status"
go_type:
import: "warehouse-management/types"
type: "OrderStatus"
pointer: true
inflection_exclude_table_names:
- "status"
```
---
**Tóm lại**: Key bắt buộc tối thiểu cho một config sqlc v2 hoạt động là `version`, `sql[].engine`, `sql[].queries`, `sql[].schema`, và ít nhất một block `gen.<lang>` với `out` + `package` (hoặc tương đương cho ngôn ngữ khác).

400
docs/sqlc/query.md Normal file
View File

@@ -0,0 +1,400 @@
# Bộ quy tắc viết file query trong sqlc
---
## 1. Cấu trúc cơ bản
Mỗi file query là một file `.sql` thông thường, nhưng chứa **sqlc annotation** ở dạng SQL comment để sqlc phân tích metadata.
```sql
-- name: <TênMethod> <:command>
-- <comment mô tả (tuỳ chọn)>
SELECT * FROM users WHERE id = $1;
```
**Cú pháp annotation:**
```
-- name: <MethodName> <:command>
```
- `MethodName` → Tên method sẽ được sinh trong Go code (phải là identifier hợp lệ, khuyến nghị dùng **camelCase** hoặc **PascalCase**).
- `:command` → Loại thao tác, quyết định kiểu trả về.
---
## 2. Các loại `:command`
| Command | Kiểu trả về (Go) | Dùng khi |
| ------------- | --------------------- | ------------------------------------------------------------------------------------- |
| `:one` | `(T, error)` | Trả về **đúng 1 row**. Nếu không tìm thấy → `sql.ErrNoRows`. |
| `:many` | `([]T, error)` | Trả về **danh sách rows**. Có thể rỗng. |
| `:exec` | `(sql.Result, error)` | Thực thi **không trả về row** (INSERT/UPDATE/DELETE không cần data trả về). |
| `:execrows` | `(int64, error)` | Giống `:exec` nhưng trả về **số rows affected**. |
| `:execresult` | `(sql.Result, error)` | Giống `:exec`, trả về `sql.Result` đầy đủ (có `.LastInsertId()` + `.RowsAffected()`). |
| `:copyfrom` | `(int64, error)` | Dùng cho PostgreSQL `COPY FROM`, truyền slice struct. |
---
## 3. Quy tắc viết annotation
### 3.1. Annotation phải nằm **ngay trên** câu query
```sql
-- ✅ ĐÚNG: annotation ngay trên query
-- name: GetUser :one
SELECT * FROM users WHERE id = $1;
-- ❌ SAI: có dòng trống giữa annotation và query
-- name: GetUser :one
SELECT * FROM users WHERE id = $1;
```
### 3.2. Mỗi query phải kết thúc bằng dấu `;`
```sql
-- ✅ ĐÚNG
-- name: GetUser :one
SELECT * FROM users WHERE id = $1;
-- ❌ SAI: thiếu dấu ;
-- name: GetUser :one
SELECT * FROM users WHERE id = $1
```
### 3.3. Tên method **không được trùng** trong cùng một file (hoặc cùng package queries)
```sql
-- ❌ SAI: trùng tên
-- name: GetUser :one
SELECT * FROM users WHERE id = $1;
-- name: GetUser :many
SELECT * FROM users WHERE role = $1;
```
### 3.4. Có thể viết nhiều query trong 1 file
```sql
-- name: GetUser :one
SELECT * FROM users WHERE id = $1;
-- name: ListUsers :many
SELECT * FROM users ORDER BY created_at DESC;
-- name: CreateUser :one
INSERT INTO users (id, username, email)
VALUES ($1, $2, $3)
RETURNING *;
-- name: DeleteUser :exec
DELETE FROM users WHERE id = $1;
```
---
## 4. Tham số (Parameters)
### 4.1. PostgreSQL — dùng positional params `$1, $2, ...`
```sql
-- name: CreateUser :one
INSERT INTO users (username, email, password_hash)
VALUES ($1, $2, $3)
RETURNING *;
```
### 4.2. Sử dụng `sqlc.arg()` — **khuyến nghị dùng** vì rõ ràng hơn
```sql
-- name: CreateUser :one
INSERT INTO users (username, email, password_hash)
VALUES (sqlc.arg(username), sqlc.arg(email), sqlc.arg(password_hash))
RETURNING *;
```
> Khi dùng `sqlc.arg()`, sqlc sinh **tên param có ý nghĩa** trong Go struct thay vì `ID`/`Column2` không rõ ràng.
### 4.3. `sqlc.narg()` — nullable parameter
Dùng khi tham số có thể là `NULL`:
```sql
-- name: SearchUsers :many
SELECT * FROM users
WHERE (
sqlc.narg(username) IS NULL OR username = sqlc.narg(username)
)
AND (
sqlc.narg(email) IS NULL OR email = sqlc.narg(email)
);
```
sqlc sẽ sinh kiểu `NullString` hoặc con trỏ `*string` (tuỳ config) cho các param này.
### 4.4. Truyền struct/array — `sqlc.arg()` với nhiều field
```sql
-- name: UpdateUser :one
UPDATE users
SET
username = COALESCE(sqlc.arg(username), username),
email = COALESCE(sqlc.arg(email), email)
WHERE id = sqlc.arg(id)
RETURNING *;
```
---
## 5. `RETURNING *` — Khuyến nghị dùng với `:one` / `:many`
Khi `INSERT`, `UPDATE`, `DELETE` mà bạn muốn nhận lại dữ liệu, hãy dùng `RETURNING *`:
```sql
-- name: CreateUser :one
INSERT INTO users (username, email)
VALUES ($1, $2)
RETURNING *;
-- name: UpdateUser :one
UPDATE users SET email = $2 WHERE id = $1
RETURNING *;
-- name: DeleteUser :one
DELETE FROM users WHERE id = $1
RETURNING *;
```
Nếu không có `RETURNING *`, phải dùng `:exec` hoặc `:execrows`.
---
## 6. Quy tắc cho `SELECT *`
sqlc sẽ phân tích schema và thay `*` bằng danh sách cột cụ thể trong code sinh ra. Tuy nhiên:
```sql
-- ✅ Khuyến nghị: chỉ định cột rõ ràng
-- name: GetUser :one
SELECT id, username, email, created_at FROM users WHERE id = $1;
-- ✅ Cũng hợp lệ: dùng *
-- name: GetUser :one
SELECT * FROM users WHERE id = $1;
```
> **Khi JOIN nhiều bảng**, KHÔNG nên dùng `SELECT *` vì có thể trùng tên cột. Nên alias rõ ràng.
---
## 7. JOIN và Alias
```sql
-- name: GetUserWithRole :one
SELECT
u.id,
u.username,
u.email,
r.name AS role_name
FROM users u
JOIN user_roles ur ON u.id = ur.user_id
JOIN roles r ON ur.role_id = r.id
WHERE u.id = $1;
```
sqlc sẽ sinh struct với các field `ID`, `Username`, `Email`, `RoleName`.
---
## 8. Sử dụng `sqlc.embed()`
Dùng khi muốn **nhúng toàn bộ một struct** vào kết quả (từ sqlc v1.18+):
```sql
-- name: GetUserWithRole :one
SELECT
sqlc.embed(u),
r.name AS role_name
FROM users u
JOIN user_roles ur ON u.id = ur.user_id
JOIN roles r ON ur.role_id = r.id
WHERE u.id = $1;
```
→ Go struct sinh ra sẽ có dạng:
```go
type GetUserWithRoleRow struct {
User // embedded struct từ bảng users
RoleName string
}
```
---
## 9. Sử dụng `sqlc.slice()` — IN clause
Dùng cho `WHERE id IN ($1, $2, ...)` với số lượng phần tử động:
```sql
-- name: GetUsersByIDs :many
SELECT * FROM users
WHERE id = ANY(sqlc.slice(ids));
```
hoặc với PostgreSQL:
```sql
-- name: GetUsersByIDs :many
SELECT * FROM users
WHERE id = ANY($1::uuid[]);
```
Nhưng `sqlc.slice()` được khuyến nghị hơn vì sqlc sẽ tự xử lý kiểu.
---
## 10. Sub-query và CTE
sqlc hỗ trợ đầy đủ:
```sql
-- name: GetUserStats :one
WITH user_orders AS (
SELECT user_id, COUNT(*) AS order_count
FROM orders
GROUP BY user_id
)
SELECT u.*, COALESCE(uo.order_count, 0) AS order_count
FROM users u
LEFT JOIN user_orders uo ON u.id = uo.user_id
WHERE u.id = $1;
```
---
## 11. CASE expression
```sql
-- name: ListUsersWithStatus :many
SELECT
id,
username,
CASE
WHEN deleted_at IS NOT NULL THEN 'deleted'
WHEN last_login_at > NOW() - INTERVAL '30 days' THEN 'active'
ELSE 'inactive'
END AS status
FROM users;
```
---
## 12. Transaction — không viết trong file query
sqlc **không quản lý transaction** trong file `.sql`. Transaction được xử lý ở tầng ứng dụng Go:
```go
// Trong Go code (không phải file .sql)
tx, _ := db.BeginTx(ctx, nil)
q := New(tx) // tạo Queries instance với tx
q.CreateUser(ctx, ...)
q.CreateUserRole(ctx, ...)
tx.Commit()
```
---
## 13. Comment thường vs sqlc annotation
```sql
-- Đây là comment thường, sqlc bỏ qua
-- name: GetUser :one ← Đây là sqlc annotation
SELECT * FROM users WHERE id = $1;
/*
Multi-line comment cũng được
sqlc bỏ qua
*/
```
---
## 14. Quy tắc đặt tên file
| Quy ước | Ví dụ |
| ------------------------------------------------------------------ | ----------------------------------------- |
| 1 file = 1 bảng chính | `users.sql`, `orders.sql`, `products.sql` |
| Đặt tên theo **feature/domain** | `auth.sql`, `inventory.sql` |
| File nằm trong thư mục được chỉ định ở `queries` trong `sqlc.yaml` | `./db/queries/` |
---
## 15. Toàn bộ mẫu tham khảo
```sql
-- name: GetUser :one
SELECT * FROM users WHERE id = sqlc.arg(id);
-- name: GetUserByEmail :one
SELECT * FROM users WHERE email = sqlc.arg(email);
-- name: ListUsers :many
SELECT * FROM users
ORDER BY created_at DESC
LIMIT sqlc.arg(limit) OFFSET sqlc.arg(offset);
-- name: SearchUsers :many
SELECT * FROM users
WHERE (
sqlc.narg(username) IS NULL OR username ILIKE '%' || sqlc.narg(username) || '%'
)
AND (
sqlc.narg(email) IS NULL OR email ILIKE '%' || sqlc.narg(email) || '%'
);
-- name: CreateUser :one
INSERT INTO users (id, username, email, password_hash)
VALUES (
sqlc.arg(id),
sqlc.arg(username),
sqlc.arg(email),
sqlc.arg(password_hash)
)
RETURNING *;
-- name: UpdateUser :one
UPDATE users
SET
username = COALESCE(sqlc.arg(username), username),
email = COALESCE(sqlc.arg(email), email)
WHERE id = sqlc.arg(id)
RETURNING *;
-- name: DeleteUser :execrows
DELETE FROM users WHERE id = sqlc.arg(id);
-- name: GetUsersByIDs :many
SELECT * FROM users WHERE id = ANY(sqlc.slice(ids));
-- name: CountUsers :one
SELECT COUNT(*) AS count FROM users;
```
---
## Tóm tắt nhanh
| Quy tắc | Mô tả |
| -------------------------- | ----------------------------------------------------- |
| Annotation format | `-- name: MethodName :command` |
| Phải có `;` kết thúc | Mỗi query kết thúc bằng dấu chấm phẩy |
| Annotation ngay trên query | Không có dòng trống ở giữa |
| Tên method không trùng | Trong cùng package queries |
| Dùng `sqlc.arg()` | Khuyến nghị thay vì `$1` để sinh tên param có ý nghĩa |
| Dùng `sqlc.narg()` | Cho nullable parameter |
| Dùng `sqlc.slice()` | Cho `IN (...)` dynamic |
| Dùng `sqlc.embed()` | Để nhúng struct khi JOIN |
| `RETURNING *` | Khi cần dữ liệu trả về với INSERT/UPDATE/DELETE |
| Transaction | Không viết trong `.sql`, xử lý ở Go code |

526
docs/swagger/docs.go Normal file
View File

@@ -0,0 +1,526 @@
// Package swagger Code generated by swaggo/swag. DO NOT EDIT
package swagger
import "github.com/swaggo/swag"
const docTemplate = `{
"schemes": {{ marshal .Schemes }},
"swagger": "2.0",
"info": {
"description": "{{escape .Description}}",
"title": "{{.Title}}",
"contact": {},
"version": "{{.Version}}"
},
"host": "{{.Host}}",
"basePath": "{{.BasePath}}",
"paths": {
"/auth/register": {
"post": {
"description": "Register with email, username and password",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"auth"
],
"summary": "Register a new user",
"parameters": [
{
"description": "Register request",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/requests.BodyRegisterRequest"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.SuccessResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/responses.BodyRegisterResponse"
}
}
}
]
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
},
"409": {
"description": "Conflict",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
}
},
"/ping": {
"get": {
"description": "Check server is running",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"health"
],
"summary": "Health check",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/v1/warehouses": {
"get": {
"description": "Retrieve a list of all warehouses ordered by creation date",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"warehouse"
],
"summary": "List all warehouses",
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.SuccessResponse"
},
{
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/models.Warehouse"
}
}
}
}
]
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
},
"post": {
"description": "Create a new warehouse with the provided details",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"warehouse"
],
"summary": "Create a new warehouse",
"parameters": [
{
"description": "Warehouse request body",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/requests.CUWarehouseRequest"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.SuccessResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/responses.CreateWarehouseResponse"
}
}
}
]
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
}
},
"/v1/warehouses/{id}": {
"get": {
"description": "Retrieve a single warehouse using its unique identifier",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"warehouse"
],
"summary": "Get warehouse by ID",
"parameters": [
{
"type": "integer",
"description": "Warehouse ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.SuccessResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/models.Warehouse"
}
}
}
]
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
},
"put": {
"description": "Update an existing warehouse by its ID with the provided details",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"warehouse"
],
"summary": "Update warehouse",
"parameters": [
{
"type": "integer",
"description": "Warehouse ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Warehouse request body",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/requests.CUWarehouseRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.SuccessResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/responses.UpdateWarehouseResponse"
}
}
}
]
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
},
"delete": {
"description": "Delete a warehouse by its unique identifier",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"warehouse"
],
"summary": "Delete warehouse",
"parameters": [
{
"type": "integer",
"description": "Warehouse ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.SuccessResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
}
}
},
"definitions": {
"models.Warehouse": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"createdAt": {
"type": "string"
},
"description": {
"type": "string"
},
"id": {
"type": "integer"
},
"name": {
"type": "string"
},
"updatedAt": {
"type": "string"
}
}
},
"requests.BodyRegisterRequest": {
"type": "object",
"required": [
"email",
"password",
"username"
],
"properties": {
"email": {
"type": "string"
},
"fullName": {
"type": "string"
},
"password": {
"type": "string",
"minLength": 8
},
"username": {
"type": "string"
}
}
},
"requests.CUWarehouseRequest": {
"type": "object",
"required": [
"address",
"name"
],
"properties": {
"address": {
"type": "string"
},
"description": {
"type": "string"
},
"name": {
"type": "string"
}
}
},
"response.ErrorResponse": {
"type": "object",
"properties": {
"code": {
"type": "integer"
},
"message": {
"type": "string"
},
"now": {
"type": "integer"
},
"status": {
"type": "integer"
}
}
},
"response.SuccessResponse": {
"type": "object",
"properties": {
"data": {},
"message": {
"type": "string"
},
"option": {},
"reason_status_code": {
"type": "string"
},
"status": {
"type": "integer"
}
}
},
"responses.BodyRegisterResponse": {
"type": "object",
"properties": {
"id": {
"type": "string"
}
}
},
"responses.CreateWarehouseResponse": {
"type": "object",
"properties": {
"id": {
"type": "integer"
}
}
},
"responses.UpdateWarehouseResponse": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"description": {
"type": "string"
},
"id": {
"type": "integer"
},
"name": {
"type": "string"
}
}
}
}
}`
// SwaggerInfo holds exported Swagger Info so clients can modify it
var SwaggerInfo = &swag.Spec{
Version: "1.0",
Host: "localhost:3000",
BasePath: "/api/v1",
Schemes: []string{},
Title: "Warehouse Management API",
Description: "This is the Warehouse Management API server.",
InfoInstanceName: "swagger",
SwaggerTemplate: docTemplate,
LeftDelim: "{{",
RightDelim: "}}",
}
func init() {
swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo)
}

502
docs/swagger/swagger.json Normal file
View File

@@ -0,0 +1,502 @@
{
"swagger": "2.0",
"info": {
"description": "This is the Warehouse Management API server.",
"title": "Warehouse Management API",
"contact": {},
"version": "1.0"
},
"host": "localhost:3000",
"basePath": "/api/v1",
"paths": {
"/auth/register": {
"post": {
"description": "Register with email, username and password",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"auth"
],
"summary": "Register a new user",
"parameters": [
{
"description": "Register request",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/requests.BodyRegisterRequest"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.SuccessResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/responses.BodyRegisterResponse"
}
}
}
]
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
},
"409": {
"description": "Conflict",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
}
},
"/ping": {
"get": {
"description": "Check server is running",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"health"
],
"summary": "Health check",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/v1/warehouses": {
"get": {
"description": "Retrieve a list of all warehouses ordered by creation date",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"warehouse"
],
"summary": "List all warehouses",
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.SuccessResponse"
},
{
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/models.Warehouse"
}
}
}
}
]
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
},
"post": {
"description": "Create a new warehouse with the provided details",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"warehouse"
],
"summary": "Create a new warehouse",
"parameters": [
{
"description": "Warehouse request body",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/requests.CUWarehouseRequest"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.SuccessResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/responses.CreateWarehouseResponse"
}
}
}
]
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
}
},
"/v1/warehouses/{id}": {
"get": {
"description": "Retrieve a single warehouse using its unique identifier",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"warehouse"
],
"summary": "Get warehouse by ID",
"parameters": [
{
"type": "integer",
"description": "Warehouse ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.SuccessResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/models.Warehouse"
}
}
}
]
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
},
"put": {
"description": "Update an existing warehouse by its ID with the provided details",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"warehouse"
],
"summary": "Update warehouse",
"parameters": [
{
"type": "integer",
"description": "Warehouse ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Warehouse request body",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/requests.CUWarehouseRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.SuccessResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/responses.UpdateWarehouseResponse"
}
}
}
]
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
},
"delete": {
"description": "Delete a warehouse by its unique identifier",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"warehouse"
],
"summary": "Delete warehouse",
"parameters": [
{
"type": "integer",
"description": "Warehouse ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.SuccessResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
}
}
},
"definitions": {
"models.Warehouse": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"createdAt": {
"type": "string"
},
"description": {
"type": "string"
},
"id": {
"type": "integer"
},
"name": {
"type": "string"
},
"updatedAt": {
"type": "string"
}
}
},
"requests.BodyRegisterRequest": {
"type": "object",
"required": [
"email",
"password",
"username"
],
"properties": {
"email": {
"type": "string"
},
"fullName": {
"type": "string"
},
"password": {
"type": "string",
"minLength": 8
},
"username": {
"type": "string"
}
}
},
"requests.CUWarehouseRequest": {
"type": "object",
"required": [
"address",
"name"
],
"properties": {
"address": {
"type": "string"
},
"description": {
"type": "string"
},
"name": {
"type": "string"
}
}
},
"response.ErrorResponse": {
"type": "object",
"properties": {
"code": {
"type": "integer"
},
"message": {
"type": "string"
},
"now": {
"type": "integer"
},
"status": {
"type": "integer"
}
}
},
"response.SuccessResponse": {
"type": "object",
"properties": {
"data": {},
"message": {
"type": "string"
},
"option": {},
"reason_status_code": {
"type": "string"
},
"status": {
"type": "integer"
}
}
},
"responses.BodyRegisterResponse": {
"type": "object",
"properties": {
"id": {
"type": "string"
}
}
},
"responses.CreateWarehouseResponse": {
"type": "object",
"properties": {
"id": {
"type": "integer"
}
}
},
"responses.UpdateWarehouseResponse": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"description": {
"type": "string"
},
"id": {
"type": "integer"
},
"name": {
"type": "string"
}
}
}
}
}

317
docs/swagger/swagger.yaml Normal file
View File

@@ -0,0 +1,317 @@
basePath: /api/v1
definitions:
models.Warehouse:
properties:
address:
type: string
createdAt:
type: string
description:
type: string
id:
type: integer
name:
type: string
updatedAt:
type: string
type: object
requests.BodyRegisterRequest:
properties:
email:
type: string
fullName:
type: string
password:
minLength: 8
type: string
username:
type: string
required:
- email
- password
- username
type: object
requests.CUWarehouseRequest:
properties:
address:
type: string
description:
type: string
name:
type: string
required:
- address
- name
type: object
response.ErrorResponse:
properties:
code:
type: integer
message:
type: string
now:
type: integer
status:
type: integer
type: object
response.SuccessResponse:
properties:
data: {}
message:
type: string
option: {}
reason_status_code:
type: string
status:
type: integer
type: object
responses.BodyRegisterResponse:
properties:
id:
type: string
type: object
responses.CreateWarehouseResponse:
properties:
id:
type: integer
type: object
responses.UpdateWarehouseResponse:
properties:
address:
type: string
description:
type: string
id:
type: integer
name:
type: string
type: object
host: localhost:3000
info:
contact: {}
description: This is the Warehouse Management API server.
title: Warehouse Management API
version: "1.0"
paths:
/auth/register:
post:
consumes:
- application/json
description: Register with email, username and password
parameters:
- description: Register request
in: body
name: body
required: true
schema:
$ref: '#/definitions/requests.BodyRegisterRequest'
produces:
- application/json
responses:
"201":
description: Created
schema:
allOf:
- $ref: '#/definitions/response.SuccessResponse'
- properties:
data:
$ref: '#/definitions/responses.BodyRegisterResponse'
type: object
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.ErrorResponse'
"409":
description: Conflict
schema:
$ref: '#/definitions/response.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.ErrorResponse'
summary: Register a new user
tags:
- auth
/ping:
get:
consumes:
- application/json
description: Check server is running
produces:
- application/json
responses:
"200":
description: OK
schema:
additionalProperties:
type: string
type: object
summary: Health check
tags:
- health
/v1/warehouses:
get:
consumes:
- application/json
description: Retrieve a list of all warehouses ordered by creation date
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/response.SuccessResponse'
- properties:
data:
items:
$ref: '#/definitions/models.Warehouse'
type: array
type: object
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.ErrorResponse'
summary: List all warehouses
tags:
- warehouse
post:
consumes:
- application/json
description: Create a new warehouse with the provided details
parameters:
- description: Warehouse request body
in: body
name: body
required: true
schema:
$ref: '#/definitions/requests.CUWarehouseRequest'
produces:
- application/json
responses:
"201":
description: Created
schema:
allOf:
- $ref: '#/definitions/response.SuccessResponse'
- properties:
data:
$ref: '#/definitions/responses.CreateWarehouseResponse'
type: object
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.ErrorResponse'
summary: Create a new warehouse
tags:
- warehouse
/v1/warehouses/{id}:
delete:
consumes:
- application/json
description: Delete a warehouse by its unique identifier
parameters:
- description: Warehouse ID
in: path
name: id
required: true
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/response.SuccessResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.ErrorResponse'
summary: Delete warehouse
tags:
- warehouse
get:
consumes:
- application/json
description: Retrieve a single warehouse using its unique identifier
parameters:
- description: Warehouse ID
in: path
name: id
required: true
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/response.SuccessResponse'
- properties:
data:
$ref: '#/definitions/models.Warehouse'
type: object
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.ErrorResponse'
"404":
description: Not Found
schema:
$ref: '#/definitions/response.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.ErrorResponse'
summary: Get warehouse by ID
tags:
- warehouse
put:
consumes:
- application/json
description: Update an existing warehouse by its ID with the provided details
parameters:
- description: Warehouse ID
in: path
name: id
required: true
type: integer
- description: Warehouse request body
in: body
name: body
required: true
schema:
$ref: '#/definitions/requests.CUWarehouseRequest'
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/response.SuccessResponse'
- properties:
data:
$ref: '#/definitions/responses.UpdateWarehouseResponse'
type: object
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.ErrorResponse'
summary: Update warehouse
tags:
- warehouse
swagger: "2.0"

107
fsnotify.go Normal file
View File

@@ -0,0 +1,107 @@
package main
import (
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"sync"
"syscall"
"time"
"github.com/fsnotify/fsnotify"
)
func getFreePort(port int) {
// macOS-compatible: use lsof to find and kill process on port
cmd := exec.Command("sh", "-c", fmt.Sprintf("lsof -ti :%d | xargs kill -9 2>/dev/null", port))
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
log.Printf("Failed to free port %d: %v", port, err)
}
}
func main() {
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Fatal(err)
}
defer watcher.Close()
done := make(chan bool)
var cmd *exec.Cmd
var mu sync.Mutex
var debounceTimer *time.Timer
buildAndRestart := func() {
mu.Lock()
defer mu.Unlock()
// Build the application
buildCmd := exec.Command("go", "build", "-o", "./tmp/main", "./cmd/server/main.go")
buildCmd.Stdout = os.Stdout
buildCmd.Stderr = os.Stderr
if err := buildCmd.Run(); err != nil {
log.Println("Build failed:", err)
return
}
// Free the port
getFreePort(3000)
// Restart the application
if cmd != nil && cmd.Process != nil {
if err := cmd.Process.Signal(syscall.SIGTERM); err != nil {
log.Printf("Failed to terminate process: %v", err)
}
_ = cmd.Wait()
}
cmd = exec.Command("./tmp/main")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Start(); err != nil {
log.Fatal(err)
}
log.Println("Application restarted")
}
go func() {
buildAndRestart()
for {
select {
case event, ok := <-watcher.Events:
if !ok {
return
}
if (event.Op&fsnotify.Write == fsnotify.Write || event.Op&fsnotify.Create == fsnotify.Create) && filepath.Ext(event.Name) == ".go" {
log.Println("Modified file:", event.Name)
if debounceTimer != nil {
debounceTimer.Stop()
}
debounceTimer = time.AfterFunc(500*time.Millisecond, buildAndRestart)
}
case err := <-watcher.Errors:
log.Println("error:", err)
}
}
}()
err = filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
switch info.Name() {
case "vendor", "tmp", "node_modules", ".git", ".vscode", "docs", "tests", "scripts", "github":
return filepath.SkipDir
default:
return watcher.Add(path)
}
}
return nil
})
if err != nil {
log.Fatal(err)
}
<-done
}

45
global/global.go Normal file
View File

@@ -0,0 +1,45 @@
package global
import (
"wm-backend/configs"
"wm-backend/internal/initialization"
"wm-backend/internal/models"
db "wm-backend/sqlc_gen"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/redis/go-redis/v9"
"github.com/rs/zerolog/log"
)
var (
Cfg models.Config
DB *pgxpool.Pool
Queries *db.Queries
Cache *redis.Client
)
func init() {
var err error
// Load Configurations
Cfg, err = configs.LoadConfig("configs")
if err != nil {
log.Fatal().Err(err).Msg("Error loading config")
panic(err)
}
// DATABASE
DB, err = initialization.ConnectPostgreSQL(&Cfg)
if err != nil {
log.Fatal().Err(err).Msg("Error connecting to database")
panic(err)
}
Queries = db.New(DB)
// CACHE
Cache, err = initialization.ConnectRedis(Cfg)
if err != nil {
log.Fatal().Err(err).Msg("Error connecting to Redis")
panic(err)
}
}

80
go.mod Normal file
View File

@@ -0,0 +1,80 @@
module wm-backend
go 1.25.1
require (
github.com/fsnotify/fsnotify v1.10.1
github.com/gin-gonic/gin v1.12.0
github.com/golang-jwt/jwt/v5 v5.3.1
github.com/google/uuid v1.6.0
github.com/jackc/pgx/v5 v5.9.2
github.com/joho/godotenv v1.5.1
github.com/redis/go-redis/v9 v9.19.0
github.com/rs/zerolog v1.35.1
github.com/spf13/viper v1.21.0
github.com/swaggo/files v1.0.1
github.com/swaggo/gin-swagger v1.6.1
)
require (
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.19.6 // indirect
github.com/go-openapi/spec v0.20.4 // indirect
github.com/go-openapi/swag v0.19.15 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.7.6 // indirect
golang.org/x/mod v0.32.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/tools v0.41.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
require (
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.15.0 // indirect
github.com/bytedance/sonic/loader v0.5.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.30.1
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.59.0 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/swaggo/swag v1.16.6
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.1 // indirect
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/arch v0.22.0 // indirect
golang.org/x/crypto v0.48.0
golang.org/x/net v0.51.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.34.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
)

228
go.sum Normal file
View File

@@ -0,0 +1,228 @@
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
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/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
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.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/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.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs=
github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M=
github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
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.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
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/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-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw=
github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/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/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
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/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
github.com/redis/go-redis/v9 v9.19.0 h1:XPVaaPSnG6RhYf7p+rmSa9zZfeVAnWsH5h3lxthOm/k=
github.com/redis/go-redis/v9 v9.19.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI=
github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
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.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
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.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
github.com/swaggo/gin-swagger v1.6.1 h1:Ri06G4gc9N4t4k8hekMigJ9zKTFSlqj/9paAQCQs7cY=
github.com/swaggo/gin-swagger v1.6.1/go.mod h1:LQ+hJStHakCWRiK/YNYtJOu4mR2FP+pxLnILT/qNiTw=
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
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/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
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.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
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-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
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.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
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-20210420072515-93ed5bcd2bfe/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-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
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.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
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.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/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/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/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=

View File

@@ -0,0 +1,47 @@
package initialization
import (
"context"
"fmt"
"time"
"wm-backend/internal/models"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/rs/zerolog/log"
)
func ConnectPostgreSQL(config *models.Config) (*pgxpool.Pool, error) {
connStr := fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable",
config.Database.Username, config.Database.Password, config.Database.Host, config.Database.Port, config.Database.Name)
var pool *pgxpool.Pool
var err error
maxRetries := 5
for i := range maxRetries {
poolConfig, parseErr := pgxpool.ParseConfig(connStr)
if parseErr != nil {
log.Error().Err(parseErr).Msgf("Failed to parse DB config (attempt %d/%d)", i+1, maxRetries)
time.Sleep(5 * time.Second)
continue
}
pool, err = pgxpool.NewWithConfig(context.Background(), poolConfig)
if err != nil {
log.Error().Err(err).Msgf("Failed to connect to PostgreSQL (attempt %d/%d)", i+1, maxRetries)
time.Sleep(5 * time.Second)
continue
}
err = pool.Ping(context.Background())
if err != nil {
log.Error().Err(err).Msgf("Failed to ping PostgreSQL (attempt %d/%d)", i+1, maxRetries)
time.Sleep(5 * time.Second)
continue
}
log.Info().Msg("Successfully connected to PostgreSQL")
return pool, nil
}
return nil, fmt.Errorf("failed to connect to PostgreSQL after %d attempts: %v", maxRetries, err)
}

View File

@@ -0,0 +1,39 @@
package initialization
import (
"context"
"fmt"
"time"
"wm-backend/internal/models"
"github.com/redis/go-redis/v9"
"github.com/rs/zerolog/log"
)
func ConnectRedis(cfg models.Config) (*redis.Client, error) {
var rdb *redis.Client
var pong string
var err error
maxRetries := 10
for range maxRetries {
rdb = redis.NewClient(&redis.Options{
Addr: fmt.Sprintf("%s:%s", cfg.Cache.Host, cfg.Cache.Port),
Password: cfg.Cache.Password,
DB: 0,
})
pong, err = rdb.Ping(context.Background()).Result()
if err != nil {
log.Error().Err(err).Msg("Error connecting to Redis")
log.Error().Msg("Retrying in 5 seconds...")
time.Sleep(5 * time.Second)
continue
} else {
log.Info().Msg("CONNECTED TO REDIS:" + pong + "🥩")
return rdb, nil
}
}
return nil, fmt.Errorf("failed to connect to Redis after %d attempts: %v", maxRetries, err)
}

View File

@@ -0,0 +1,28 @@
package mapper
import (
"wm-backend/internal/models"
db "wm-backend/sqlc_gen"
"github.com/jackc/pgx/v5/pgtype"
)
// ToDomainRole maps a SQLC-generated Role to the domain Role model.
func ToDomainRole(r db.Role) *models.Role {
return &models.Role{
ID: r.ID.String(),
Name: r.Name,
Description: r.Description.String,
CreatedAt: r.CreatedAt.Time,
CreatedBy: r.CreatedBy.String,
}
}
// ToModelRole maps a domain Role model to the parameters needed for creating a Role in the database.
func ToModelRole(r *models.Role) *db.CreateRoleParams {
return &db.CreateRoleParams{
Name: r.Name,
Description: pgtype.Text{String: r.Description, Valid: r.Description != ""},
CreatedBy: pgtype.Text{String: r.CreatedBy, Valid: r.CreatedBy != ""},
}
}

View File

@@ -0,0 +1,21 @@
package mapper
import (
"wm-backend/internal/models"
db "wm-backend/sqlc_gen"
)
// toDomainUser maps a SQLC-generated User to the domain User model.
func ToDomainUser(u db.User) *models.User {
return &models.User{
ID: u.ID.String(),
Username: u.Username,
Email: u.Email,
FullName: u.FullName.String,
PasswordHash: u.PasswordHash,
IsActive: u.IsActive.Bool,
CreatedAt: u.CreatedAt.Time,
UpdatedAt: u.UpdatedAt.Time,
CreatedBy: u.CreatedBy.String,
}
}

View File

@@ -0,0 +1,51 @@
package mapper
import (
"wm-backend/internal/models"
db "wm-backend/sqlc_gen"
"github.com/jackc/pgx/v5/pgtype"
)
// ToDomainWarehouse maps a SQLC-generated Warehouse to the domain Warehouse model.
func ToDomainWarehouse(r db.Warehouse) *models.Warehouse {
return &models.Warehouse{
ID: r.ID,
Name: r.Name,
Description: r.Description.String,
Address: r.Address.String,
CreatedAt: r.CreatedAt,
UpdatedAt: r.UpdatedAt,
}
}
func ToModelWarehouse(r *models.Warehouse) *db.CreateWarehouseParams {
return &db.CreateWarehouseParams{
Name: r.Name,
Description: pgtype.Text{
String: r.Description,
Valid: r.Description != "",
},
Address: pgtype.Text{
String: r.Address,
Valid: r.Address != "",
},
CreatedAt: r.CreatedAt,
}
}
func ToUpdateModelWarehouse(r *models.Warehouse) *db.UpdateWarehouseParams {
return &db.UpdateWarehouseParams{
Name: r.Name,
Description: pgtype.Text{
String: r.Description,
Valid: r.Description != "",
},
Address: pgtype.Text{
String: r.Address,
Valid: r.Address != "",
},
UpdatedAt: r.UpdatedAt,
ID: r.ID,
}
}

View File

@@ -0,0 +1,10 @@
package middlewares
import "github.com/gin-gonic/gin"
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// 1. Get the Authorization header
c.Next()
}
}

View File

@@ -0,0 +1,34 @@
package middlewares
import (
"time"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog/log"
)
func LoggingMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
log.Info().
Str("type", "request").
Str("method", c.Request.Method).
Str("path", c.Request.URL.Path).
Str("ip", c.ClientIP()).
Str("query", c.Request.URL.RawQuery).
Time("time", start).
Msg("incoming request")
c.Next()
log.Info().
Str("type", "response").
Str("method", c.Request.Method).
Str("path", c.Request.URL.Path).
Int("status", c.Writer.Status()).
Dur("latency", time.Since(start)).
Time("time", time.Now()).
Msg("outgoing response")
}
}

View File

@@ -0,0 +1,43 @@
package models
type Config struct {
Server ServerConfig
Database DatabaseConfig
Cache CacheConfig
Admin AdminConfig
JWT JWTConfig
}
type AdminConfig struct {
Username string
Email string
Password string
FullName string
}
type JWTConfig struct {
SecretKey string
ExpirationHours int
}
type DatabaseConfig struct {
Host string
Port string
Username string
Password string
Name string
}
type ServerConfig struct {
Host string
Port string
PortFrontend string
KeyPassword string
}
type CacheConfig struct {
Username string
Password string
Host string
Port string
}

View File

@@ -0,0 +1 @@
package models

View File

@@ -0,0 +1,13 @@
package requests
type BodyRegisterRequest struct {
Username string `json:"username" binding:"required"`
Email string `json:"email" binding:"required,email"`
FullName string `json:"fullName"`
Password string `json:"password" binding:"required,min=8"`
}
type BodyLoginRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}

View File

@@ -0,0 +1,6 @@
package requests
type CreateRoleRequest struct {
Name string `json:"name" binding:"required"`
Description string `json:"description"`
}

View File

@@ -0,0 +1,13 @@
package requests
type CreateWarehouseRequest struct {
Name string `json:"name" binding:"required"`
Description string `json:"description"`
Address string `json:"address" binding:"required"`
}
type UpdateWarehouseRequest struct {
Name string `json:"name"`
Description string `json:"description"`
Address string `json:"address"`
}

View File

@@ -0,0 +1,8 @@
package responses
type BodyRegisterResponse struct {
ID string `json:"id"`
}
type BodyLoginResponse struct {
Token string `json:"token"`
}

View File

@@ -0,0 +1,5 @@
package responses
type BodyRoleResponse struct {
ID string `json:"id"`
}

View File

@@ -0,0 +1,12 @@
package responses
type CreateWarehouseResponse struct {
ID int64 `json:"id"`
}
type UpdateWarehouseResponse struct {
ID int64 `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Address string `json:"address"`
}

View File

@@ -0,0 +1,11 @@
package models
import "time"
type Role struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
CreatedAt time.Time `json:"createdAt"`
CreatedBy string `json:"createdBy"`
}

View File

@@ -0,0 +1,17 @@
package models
import (
"time"
)
type User struct {
ID string `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
FullName string `json:"fullName"`
PasswordHash string `json:"-"`
IsActive bool `json:"isActive"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
CreatedBy string `json:"createdBy"`
}

View File

@@ -0,0 +1,12 @@
package models
import "time"
type Warehouse struct {
ID int64 `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Address string `json:"address"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}

View File

@@ -0,0 +1,45 @@
package repositories
import (
"context"
"errors"
"wm-backend/internal/mapper"
"wm-backend/internal/models"
db "wm-backend/sqlc_gen"
"github.com/jackc/pgx/v5"
)
// GetUserByEmail retrieves a user by their email address using SQLC-generated queries.
// Returns nil, nil if no user is found.
func GetUserByEmail(ctx context.Context, queries *db.Queries, email string) (*models.User, error) {
user, err := queries.GetUserByEmail(ctx, email)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, nil
}
return nil, err
}
return mapper.ToDomainUser(user), nil
}
func GetUserByUsername(ctx context.Context, queries *db.Queries, username string) (*models.User, error) {
user, err := queries.GetUserByUsername(ctx, username)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, nil
}
return nil, err
}
return mapper.ToDomainUser(user), nil
}
// CreateUser inserts a new user using SQLC-generated queries.
// Returns the created user's ID as a string.
func CreateUser(ctx context.Context, queries *db.Queries, params db.CreateUserParams) (string, error) {
id, err := queries.CreateUser(ctx, params)
if err != nil {
return "", err
}
return id.String(), nil
}

View File

@@ -0,0 +1 @@
package redis

View File

@@ -0,0 +1,16 @@
package repositories
import (
"context"
"wm-backend/internal/mapper"
"wm-backend/internal/models"
db "wm-backend/sqlc_gen"
)
func CreateRole(ctx context.Context, queries *db.Queries, body models.Role) (models.Role, error) {
role, err := queries.CreateRole(ctx, *mapper.ToModelRole(&body))
if err != nil {
return models.Role{}, err
}
return *mapper.ToDomainRole(role), nil
}

View File

@@ -0,0 +1,48 @@
package repositories
import (
"context"
"wm-backend/internal/mapper"
"wm-backend/internal/models"
db "wm-backend/sqlc_gen"
)
func CreateWareHouse(ctx context.Context, queries *db.Queries, body models.Warehouse) (models.Warehouse, error) {
warehouse, err := queries.CreateWarehouse(ctx, *mapper.ToModelWarehouse(&body))
if err != nil {
return models.Warehouse{}, err
}
return *mapper.ToDomainWarehouse(warehouse), nil
}
func GetWarehouseByID(ctx context.Context, queries *db.Queries, id int64) (models.Warehouse, error) {
result, err := queries.GetWarehouseByID(ctx, id)
if err != nil {
return models.Warehouse{}, err
}
return *mapper.ToDomainWarehouse(result), nil
}
func ListWarehouses(ctx context.Context, queries *db.Queries) ([]models.Warehouse, error) {
results, err := queries.ListWarehouses(ctx)
if err != nil {
return nil, err
}
var items []models.Warehouse
for _, r := range results {
items = append(items, *mapper.ToDomainWarehouse(r))
}
return items, nil
}
func UpdateWarehouse(ctx context.Context, queries *db.Queries, body models.Warehouse) (models.Warehouse, error) {
result, err := queries.UpdateWarehouse(ctx, *mapper.ToUpdateModelWarehouse(&body))
if err != nil {
return models.Warehouse{}, err
}
return *mapper.ToDomainWarehouse(result), nil
}
func DeleteWarehouse(ctx context.Context, queries *db.Queries, id int64) error {
return queries.DeleteWarehouse(ctx, id)
}

View File

@@ -0,0 +1,45 @@
package routers
import (
"os"
"wm-backend/configs/constants"
_ "wm-backend/docs/swagger"
"wm-backend/internal/middlewares"
"wm-backend/internal/services"
"wm-backend/pkg/utils"
"github.com/gin-gonic/gin"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
)
func NewRouter() *gin.Engine {
nodeEnv := os.Getenv("ENV")
if nodeEnv != constants.DevEnvironment {
gin.SetMode(gin.ReleaseMode)
}
r := gin.Default()
r.Use(middlewares.LoggingMiddleware())
v1 := r.Group(constants.API_VERSION_1)
{
auth := v1.Group(constants.API_GROUP_AUTH)
{
auth.POST(constants.API_PATH_AUTH_REGISTER, utils.AsyncHandler(services.Register))
auth.POST(constants.API_PATH_AUTH_LOGIN, utils.AsyncHandler(services.Login))
}
warehouse := v1.Group(constants.API_GROUP_WAREHOUSE)
{
warehouse.GET("", utils.AsyncHandler(services.WareHouseList))
warehouse.GET("/:id", utils.AsyncHandler(services.WareHouseGetByID))
warehouse.POST("", utils.AsyncHandler(services.WareHouseCreate))
warehouse.PUT("/:id", utils.AsyncHandler(services.WareHouseUpdate))
warehouse.DELETE("/:id", utils.AsyncHandler(services.WareHouseDelete))
}
}
r.GET(constants.API_PATH_PING, services.PingHandler)
r.GET(constants.API_PATH_DOCS, ginSwagger.WrapHandler(swaggerFiles.Handler))
return r
}

View File

@@ -0,0 +1,128 @@
package services
import (
"net/http"
"wm-backend/global"
"wm-backend/internal/models"
"wm-backend/internal/models/requests"
"wm-backend/internal/models/responses"
"wm-backend/internal/repositories"
"wm-backend/pkg/helper"
"wm-backend/response"
db "wm-backend/sqlc_gen"
"github.com/gin-gonic/gin"
"github.com/jackc/pgx/v5/pgtype"
"github.com/rs/zerolog/log"
)
// Register handles user registration.
// It validates the request body, checks for duplicate email,
// hashes the password, and creates the user in the database.
//
// @Summary Register a new user
// @Description Register with email, username and password
// @Tags auth
// @Accept json
// @Produce json
// @Param body body requests.BodyRegisterRequest true "Register request"
// @Success 201 {object} response.SuccessResponse{data=responses.BodyRegisterResponse}
// @Failure 400 {object} response.ErrorResponse
// @Failure 409 {object} response.ErrorResponse
// @Failure 500 {object} response.ErrorResponse
// @Router /auth/register [post]
func Register(c *gin.Context) error {
// 1. Bind & validate request body
requestBody := requests.BodyRegisterRequest{}
if helper.IsShouldBindJSON(c, &requestBody) {
return nil
}
// 2. Check if user already exists by email
existingUser, err := repositories.GetUserByEmail(c.Request.Context(), global.Queries, requestBody.Email)
if err != nil {
response.InternalServerError(c, http.StatusInternalServerError)
log.Error().Err(err).Msg("Error checking existing user")
return nil
}
if existingUser != nil {
response.ConflictError(c, http.StatusConflict, "Email already registered")
return nil
}
// 3. Hash the password
hashedPassword, err := helper.HashPassword(requestBody.Password)
if err != nil {
response.InternalServerError(c, http.StatusInternalServerError)
log.Error().Err(err).Msg("Password hashing error")
return nil
}
// 4. Create user in database
userID, err := repositories.CreateUser(c.Request.Context(), global.Queries, db.CreateUserParams{
Username: requestBody.Username,
Email: requestBody.Email,
PasswordHash: hashedPassword,
FullName: pgtype.Text{String: requestBody.FullName, Valid: requestBody.FullName != ""},
CreatedBy: pgtype.Text{String: requestBody.Username, Valid: true},
})
if err != nil {
response.InternalServerError(c, http.StatusInternalServerError)
log.Error().Err(err).Msg("Error creating user")
return nil
}
// 5. Return success response
response.Created(c, "User registered successfully", responses.BodyRegisterResponse{
ID: userID,
})
return nil
}
func Login(c *gin.Context) error {
loginRequestBody := requests.BodyLoginRequest{}
if helper.IsShouldBindJSON(c, &loginRequestBody) {
return nil
}
var user *models.User
var err error
if helper.IsEmail(loginRequestBody.Username) {
user, err = repositories.GetUserByEmail(c.Request.Context(), global.Queries, loginRequestBody.Username)
} else {
user, err = repositories.GetUserByUsername(c.Request.Context(), global.Queries, loginRequestBody.Username)
}
if err != nil {
response.InternalServerError(c, http.StatusInternalServerError)
log.Error().Err(err).Msg("Error finding user")
return nil
}
if user == nil {
response.UnauthorizedError(c, http.StatusUnauthorized, "Invalid credentials")
return nil
}
// 2. Check if user is active
if !user.IsActive {
response.UnauthorizedError(c, http.StatusUnauthorized, "Account is disabled")
return nil
}
// 3. Compare password
if err := helper.ComparePassword(loginRequestBody.Password, user.PasswordHash); err != nil {
response.UnauthorizedError(c, http.StatusUnauthorized, "Invalid credentials")
return nil
}
// 4. Generate JWT token
token, err := helper.GenerateToken(user.ID)
if err != nil {
response.InternalServerError(c, http.StatusInternalServerError)
log.Error().Err(err).Msg("Error generating token")
return nil
}
// 5. Return token
response.Ok(c, "Login successful", responses.BodyLoginResponse{
Token: token,
})
return nil
}

View File

@@ -0,0 +1,18 @@
package services
import (
"wm-backend/response"
"github.com/gin-gonic/gin"
)
// @Summary Health check
// @Description Check server is running
// @Tags health
// @Accept json
// @Produce json
// @Success 200 {object} map[string]string
// @Router /ping [get]
func PingHandler(c *gin.Context) {
response.Ok(c, "Ponggg", nil)
}

View File

@@ -0,0 +1,58 @@
package services
import (
"net/http"
"wm-backend/global"
"wm-backend/internal/models"
"wm-backend/internal/models/requests"
"wm-backend/internal/models/responses"
"wm-backend/internal/repositories"
"wm-backend/pkg/helper"
"wm-backend/response"
"github.com/gin-gonic/gin"
)
func RoleCreate(c *gin.Context) error {
requestBody := requests.CreateRoleRequest{}
if helper.IsShouldBindJSON(c, &requestBody) {
return nil
}
roleModel := &models.Role{
Name: requestBody.Name,
Description: requestBody.Description,
CreatedBy: "",
}
role, err := repositories.CreateRole(c.Request.Context(), global.Queries, *roleModel)
if err != nil {
response.InternalServerError(c, http.StatusInternalServerError, "Failed to create role")
return nil
}
// if isUniqueViolation(err) {
// response.BadRequest(c, "Role name already exists")
// } else {
// response.InternalServerError(c, "Failed to create role")
// }
// Return success response with the created role
response.Created(c, "Role created successfully", &responses.BodyRoleResponse{
ID: role.ID,
})
return nil
}
func RoleList(c *gin.Context) error {
return nil
}
func RoleGetByID(c *gin.Context) error {
return nil
}
func RoleUpdate(c *gin.Context) error {
return nil
}
func RoleDelete(c *gin.Context) error {
return nil
}

View File

@@ -0,0 +1,171 @@
package services
import (
"net/http"
"strconv"
"time"
"wm-backend/global"
"wm-backend/internal/models"
"wm-backend/internal/models/requests"
"wm-backend/internal/models/responses"
"wm-backend/internal/repositories"
"wm-backend/pkg/helper"
"wm-backend/response"
"github.com/gin-gonic/gin"
)
// WareHouseCreate creates a new warehouse.
// It validates the request body and creates the warehouse in the database.
//
// @Summary Create a new warehouse
// @Description Create a new warehouse with the provided details
// @Tags warehouse
// @Accept json
// @Produce json
// @Param body body requests.CreateWarehouseRequest true "Warehouse request body"
// @Success 201 {object} response.SuccessResponse{data=responses.CreateWarehouseResponse}
// @Failure 400 {object} response.ErrorResponse
// @Failure 500 {object} response.ErrorResponse
// @Router /v1/warehouses [post]
func WareHouseCreate(c *gin.Context) error {
requestBody := requests.CreateWarehouseRequest{}
if helper.IsShouldBindJSON(c, &requestBody) {
return nil
}
warehouseModel := &models.Warehouse{
Name: requestBody.Name,
Description: requestBody.Description,
Address: requestBody.Address,
CreatedAt: time.Now(),
}
warehouse, err := repositories.CreateWareHouse(c.Request.Context(), global.Queries, *warehouseModel)
if err != nil {
response.InternalServerError(c, http.StatusInternalServerError, "Failed to create warehouse")
return nil
}
response.Created(c, "Warehouse created successfully", &responses.CreateWarehouseResponse{
ID: warehouse.ID,
})
return nil
}
// WareHouseGetByID retrieves a single warehouse by its ID.
//
// @Summary Get warehouse by ID
// @Description Retrieve a single warehouse using its unique identifier
// @Tags warehouse
// @Accept json
// @Produce json
// @Param id path int true "Warehouse ID"
// @Success 200 {object} response.SuccessResponse{data=models.Warehouse}
// @Failure 400 {object} response.ErrorResponse
// @Failure 404 {object} response.ErrorResponse
// @Failure 500 {object} response.ErrorResponse
// @Router /v1/warehouses/{id} [get]
func WareHouseGetByID(c *gin.Context) error {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
response.BadRequestError(c, http.StatusBadRequest, "Invalid ID")
return nil
}
warehouse, err := repositories.GetWarehouseByID(c.Request.Context(), global.Queries, id)
if err != nil {
response.NotFoundError(c, http.StatusNotFound, "Warehouse not found")
return nil
}
response.Ok(c, "Success", warehouse)
return nil
}
// WareHouseList retrieves all warehouses.
//
// @Summary List all warehouses
// @Description Retrieve a list of all warehouses ordered by creation date
// @Tags warehouse
// @Accept json
// @Produce json
// @Success 200 {object} response.SuccessResponse{data=[]models.Warehouse}
// @Failure 500 {object} response.ErrorResponse
// @Router /v1/warehouses [get]
func WareHouseList(c *gin.Context) error {
warehouses, err := repositories.ListWarehouses(c.Request.Context(), global.Queries)
if err != nil {
response.InternalServerError(c, http.StatusInternalServerError, "Failed to list warehouses")
return nil
}
response.Ok(c, "Success", warehouses)
return nil
}
// WareHouseUpdate updates an existing warehouse by its ID.
// It validates the request body and updates the warehouse in the database.
//
// @Summary Update warehouse
// @Description Update an existing warehouse by its ID with the provided details
// @Tags warehouse
// @Accept json
// @Produce json
// @Param id path int true "Warehouse ID"
// @Param body body requests.UpdateWarehouseRequest true "Warehouse request body"
// @Success 200 {object} response.SuccessResponse{data=responses.UpdateWarehouseResponse}
// @Failure 400 {object} response.ErrorResponse
// @Failure 500 {object} response.ErrorResponse
// @Router /v1/warehouses/{id} [put]
func WareHouseUpdate(c *gin.Context) error {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
response.BadRequestError(c, http.StatusBadRequest, "Invalid ID")
return nil
}
requestBody := requests.UpdateWarehouseRequest{}
if helper.IsShouldBindJSON(c, &requestBody) {
return nil
}
warehouseModel := &models.Warehouse{
ID: id,
Name: requestBody.Name,
Description: requestBody.Description,
Address: requestBody.Address,
UpdatedAt: time.Now(),
}
warehouse, err := repositories.UpdateWarehouse(c.Request.Context(), global.Queries, *warehouseModel)
if err != nil {
response.InternalServerError(c, http.StatusInternalServerError, "Failed to update warehouse")
return nil
}
response.Ok(c, "Warehouse updated successfully", &responses.UpdateWarehouseResponse{
ID: warehouse.ID,
Name: warehouse.Name,
Description: warehouse.Description,
Address: warehouse.Address,
})
return nil
}
// WareHouseDelete deletes a warehouse by its ID.
//
// @Summary Delete warehouse
// @Description Delete a warehouse by its unique identifier
// @Tags warehouse
// @Accept json
// @Produce json
// @Param id path int true "Warehouse ID"
// @Success 200 {object} response.SuccessResponse
// @Failure 400 {object} response.ErrorResponse
// @Failure 500 {object} response.ErrorResponse
// @Router /v1/warehouses/{id} [delete]
func WareHouseDelete(c *gin.Context) error {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
response.BadRequestError(c, http.StatusBadRequest, "Invalid ID")
return nil
}
err = repositories.DeleteWarehouse(c.Request.Context(), global.Queries, id)
if err != nil {
response.InternalServerError(c, http.StatusInternalServerError, "Failed to delete warehouse")
return nil
}
response.Ok(c, "Đã xóa thành công", nil)
return nil
}

39
pkg/helper/jwt.go Normal file
View File

@@ -0,0 +1,39 @@
package helper
import (
"fmt"
"time"
"wm-backend/global"
"github.com/golang-jwt/jwt/v5"
)
// GenerateToken tạo JWT token cho user
func GenerateToken(userID string) (string, error) {
claims := jwt.MapClaims{
"user_id": userID,
"iat": time.Now().Unix(), // issued at
"exp": time.Now().Add(time.Duration(global.Cfg.JWT.ExpirationHours) * time.Hour * 7).Unix(), // expiry
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(global.Cfg.JWT.SecretKey)) // <-- lấy từ config
}
func ParseToken(tokenString string) (jwt.MapClaims, error) {
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (any, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(global.Cfg.JWT.SecretKey), nil // <-- lấy từ config
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
return claims, nil
}
return nil, fmt.Errorf("invalid token")
}

22
pkg/helper/password.go Normal file
View File

@@ -0,0 +1,22 @@
package helper
import "golang.org/x/crypto/bcrypt"
// HashPassword generates a salted and hashed password using bcrypt.
// It takes the plain-text password and the number of salt rounds as input.
// It returns the generated salt, the hashed password, and any error encountered.
func HashPassword(password string) (string, error) {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return "", err
}
return string(hashedPassword), nil
}
// ComparePassword compares a plain-text password with a hashed password and returns true if they match.
// It uses bcrypt.CompareHashAndPassword to perform the comparison.
func ComparePassword(password string, hashedPassword string) error {
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
return err
}

24
pkg/helper/validator.go Normal file
View File

@@ -0,0 +1,24 @@
package helper
import (
"net/http"
"wm-backend/response"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
)
func IsShouldBindJSON(c *gin.Context, obj any) bool {
if err := c.ShouldBindJSON(obj); err != nil {
response.BadRequestError(c, http.StatusBadRequest, err.Error())
return true
}
return false
}
var validate = validator.New()
func IsEmail(input string) bool {
err := validate.Var(input, "email")
return err == nil
}

74
pkg/log/logger.go Normal file
View File

@@ -0,0 +1,74 @@
package log
// import (
// "fmt"
// "github.com/rs/zerolog"
// "github.com/rs/zerolog/log"
// )
// type Logger struct{}
// func NewLogger() *Logger {
// return &Logger{}
// }
// func (logger *Logger) Print(level zerolog.Level, args any) {
// log.WithLevel(level).Msg(fmt.Sprint(args))
// }
// // Printf logs a formatted message at the given level.
// func (logger *Logger) Printf(level zerolog.Level, format string, args ...any) {
// log.WithLevel(level).Msg(fmt.Sprintf(format, args...))
// }
// // Debug logs a message at Debug level.
// func (logger *Logger) Debug(args ...any) {
// logger.Print(zerolog.DebugLevel, args)
// }
// // Debugf logs a formatted message at Debug level.
// func (logger *Logger) Debugf(format string, args ...any) {
// logger.Printf(zerolog.DebugLevel, format, args...)
// }
// // Info logs a message at Info level.
// func (logger *Logger) Info(args ...any) {
// logger.Print(zerolog.InfoLevel, args)
// }
// // Infof logs a formatted message at Info level.
// func (logger *Logger) Infof(format string, args ...any) {
// logger.Printf(zerolog.InfoLevel, format, args...)
// }
// // Warn logs a message at Warning level.
// func (logger *Logger) Warn(args ...any) {
// logger.Print(zerolog.WarnLevel, args)
// }
// // Warnf logs a formatted message at Warning level.
// func (logger *Logger) Warnf(format string, args ...any) {
// logger.Printf(zerolog.WarnLevel, format, args...)
// }
// // Error logs a message at Error level.
// func (logger *Logger) Error(args ...any) {
// logger.Print(zerolog.ErrorLevel, args)
// }
// // Errorf logs a formatted message at Error level.
// func (logger *Logger) Errorf(format string, args ...any) {
// logger.Printf(zerolog.ErrorLevel, format, args...)
// }
// // Fatal logs a message at Fatal level
// // and process will exit with status set to 1.
// func (logger *Logger) Fatal(args ...any) {
// logger.Print(zerolog.FatalLevel, args)
// }
// // Fatalf logs a formatted message at Fatal level.
// func (logger *Logger) Fatalf(format string, args ...any) {
// logger.Printf(zerolog.FatalLevel, format, args...)
// }

View File

@@ -0,0 +1,12 @@
package utils
import "github.com/gin-gonic/gin"
func AsyncHandler(fn func(c *gin.Context) error) gin.HandlerFunc {
return func(c *gin.Context) {
if err := fn(c); err != nil {
c.Error(err)
c.Next()
}
}
}

175
response/error_response.go Normal file
View File

@@ -0,0 +1,175 @@
package response
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
)
// ErrorResponse represents a structured error response
type ErrorResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Status int `json:"status"`
Now int64 `json:"now"`
}
// NewErrorResponse creates a new ErrorResponse
func NewErrorResponse(message string, status int, code int) *ErrorResponse {
return &ErrorResponse{
Code: code,
Message: message,
Status: status,
Now: time.Now().Unix(),
}
}
// Send sends the error response to the client.
// It aborts the request and responds with the error response as JSON.
func (sr *ErrorResponse) Send(c *gin.Context) {
c.AbortWithStatusJSON(sr.Status, sr)
}
// BadRequestError represents a 400 Bad Request error
func BadRequestError(c *gin.Context, code int, messages ...string) {
message := ""
if len(messages) > 0 {
message = messages[0]
}
if message == "" {
message = GetReasonPhrase(http.StatusBadRequest)
}
response := NewErrorResponse(message, http.StatusBadRequest, code)
response.Send(c)
}
// NotFoundError represents a 404 Not Found error
func NotFoundError(c *gin.Context, code int, messages ...string) {
message := ""
if len(messages) > 0 {
message = messages[0]
}
if message == "" {
message = GetReasonPhrase(http.StatusNotFound)
}
response := NewErrorResponse(message, http.StatusNotFound, code)
response.Send(c)
}
// TooManyRequestsError represents a 429 Too Many Requests error
func TooManyRequestsError(c *gin.Context, code int, messages ...string) {
message := ""
if len(messages) > 0 {
message = messages[0]
}
if message == "" {
message = GetReasonPhrase(http.StatusTooManyRequests)
}
response := NewErrorResponse(message, http.StatusTooManyRequests, code)
response.Send(c)
}
// UnauthorizedError represents a 401 Unauthorized error
func UnauthorizedError(c *gin.Context, code int, messages ...string) {
message := ""
if len(messages) > 0 {
message = messages[0]
}
if message == "" {
message = GetReasonPhrase(http.StatusUnauthorized)
}
response := NewErrorResponse(message, http.StatusUnauthorized, code)
response.Send(c)
}
// ForbiddenError handles the generation and sending of a Forbidden error response.
func ForbiddenError(c *gin.Context, code int, messages ...string) {
message := ""
if len(messages) > 0 {
message = messages[0]
}
if message == "" {
message = GetReasonPhrase(http.StatusForbidden)
}
response := NewErrorResponse(message, http.StatusForbidden, code)
response.Send(c)
}
// ConflictError represents a 409 Conflict error
func ConflictError(c *gin.Context, code int, messages ...string) {
message := ""
if len(messages) > 0 {
message = messages[0]
}
if message == "" {
message = GetReasonPhrase(http.StatusConflict)
}
response := NewErrorResponse(message, http.StatusConflict, code)
response.Send(c)
}
// EntityTooLargeError handles the HTTP 413 Request Entity Too Large error.
func EntityTooLargeError(c *gin.Context, code int, messages ...string) {
message := ""
if len(messages) > 0 {
message = messages[0]
}
if message == "" {
message = GetReasonPhrase(http.StatusRequestEntityTooLarge)
}
response := NewErrorResponse(message, http.StatusRequestEntityTooLarge, code)
response.Send(c)
}
// UnSupportMediaTypeError handles the unsupported media type error by sending an error response to the client.
func UnSupportMediaTypeError(c *gin.Context, code int, messages ...string) {
message := ""
if len(messages) > 0 {
message = messages[0]
}
if message == "" {
message = GetReasonPhrase(http.StatusUnsupportedMediaType)
}
response := NewErrorResponse(message, http.StatusUnsupportedMediaType, code)
response.Send(c)
}
// InternalServerError represents a 500 Internal Server Error
func InternalServerError(c *gin.Context, code int, messages ...string) {
message := ""
if len(messages) > 0 {
message = messages[0]
}
if message == "" {
message = GetReasonPhrase(http.StatusInternalServerError)
}
response := NewErrorResponse(message, http.StatusInternalServerError, code)
response.Send(c)
}
// ServiceUnavailable represents a 503 Service Unavailable
func ServiceUnavailable(c *gin.Context, code int, messages ...string) {
message := ""
if len(messages) > 0 {
message = messages[0]
}
if message == "" {
message = GetReasonPhrase(http.StatusServiceUnavailable)
}
response := NewErrorResponse(message, http.StatusServiceUnavailable, code)
response.Send(c)
}

View File

@@ -0,0 +1,51 @@
package response
// ReasonPhrases is a map of HTTP status codes to their reason phrases
var ReasonPhrases = map[int]string{
100: "Continue",
101: "Switching Protocols",
200: "OK",
201: "Created",
202: "Accepted",
203: "Non-Authoritative Information",
204: "No Content",
205: "Reset Content",
206: "Partial Content",
300: "Multiple Choices",
301: "Moved Permanently",
302: "Found",
303: "See Other",
304: "Not Modified",
305: "Use Proxy",
307: "Temporary Redirect",
400: "Bad Request",
401: "Unauthorized",
402: "Payment Required",
403: "Forbidden",
404: "Not Found",
405: "Method Not Allowed",
406: "Not Acceptable",
407: "Proxy Authentication Required",
408: "Request Timeout",
409: "Conflict",
410: "Gone",
411: "Length Required",
412: "Precondition Failed",
413: "Payload Too Large",
414: "URI Too Long",
415: "Unsupported Media Type",
416: "Range Not Satisfiable",
417: "Expectation Failed",
429: "Too Many Requests",
500: "Internal Server Error",
501: "Not Implemented",
502: "Bad Gateway",
503: "Service Unavailable",
504: "Gateway Timeout",
505: "HTTP Version Not Supported",
}
// GetReasonPhrase returns the reason phrase for a given status code
func GetReasonPhrase(statusCode int) string {
return ReasonPhrases[statusCode]
}

View File

@@ -0,0 +1,50 @@
package response
import (
"net/http"
"github.com/gin-gonic/gin"
)
// SuccessResponse represents a structured success response
type SuccessResponse struct {
Message string `json:"message"`
Status int `json:"status"`
ReasonStatusCode string `json:"reason_status_code"`
Option any `json:"option,omitempty"`
Data any `json:"data,omitempty"`
}
// NewSuccessResponse creates a new SuccessResponse
func NewSuccessResponse(message string, statusCode int, reasonStatusCode string, option any, data any) *SuccessResponse {
return &SuccessResponse{
Message: message,
Status: statusCode,
ReasonStatusCode: reasonStatusCode,
Option: option,
Data: data,
}
}
// Send sends the success response to the client
func (sr *SuccessResponse) Send(c *gin.Context) {
c.JSON(sr.Status, sr)
}
// Ok represents a 200 OK success response
func Ok(c *gin.Context, message string, metadata any) {
if message == "" {
message = GetReasonPhrase(http.StatusOK)
}
response := NewSuccessResponse(message, http.StatusOK, GetReasonPhrase(http.StatusOK), nil, metadata)
response.Send(c)
}
// Created represents a 201 Created success response
func Created(c *gin.Context, message string, metadata any) {
if message == "" {
message = GetReasonPhrase(http.StatusCreated)
}
response := NewSuccessResponse(message, http.StatusCreated, GetReasonPhrase(http.StatusCreated), nil, metadata)
response.Send(c)
}

36
sqlc.yaml Normal file
View File

@@ -0,0 +1,36 @@
version: "2"
sql:
- engine: "postgresql"
queries: "./db/queries/"
schema:
- "./db/init/"
- "./db/migrations/"
gen:
go:
emit_interface: true
emit_json_tags: true
emit_db_tags: true
emit_prepared_queries: false
emit_empty_slices: false
emit_result_struct_pointers: false
emit_params_struct_pointers: false
emit_methods_with_db_argument: false
json_tags_case_style: "camel"
output_db_file_name: db.go
output_models_file_name: models.go
output_querier_file_name: querier.go
package: "db"
out: "sqlc_gen"
sql_package: "pgx/v5"
overrides:
- db_type: "uuid"
go_type: "github.com/google/uuid.UUID"
- db_type: "timestamptz"
go_type: "time.Time"
# - column: "orders.status"
# go_type:
# import: "warehouse-management/types"
# type: "OrderStatus"
# pointer: true
# inflection_exclude_table_names:
# - "status"

146
sqlc_gen/cabinet.sql.go Normal file
View File

@@ -0,0 +1,146 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: cabinet.sql
package db
import (
"context"
"time"
"github.com/jackc/pgx/v5/pgtype"
)
const createCabinet = `-- name: CreateCabinet :one
INSERT INTO cabinets (room_id,name, description, created_at)
VALUES (
$1,
$2,
$3,
$4
)
RETURNING id, room_id, name, description, created_at, updated_at
`
type CreateCabinetParams struct {
RoomID int64 `db:"room_id" json:"roomId"`
Name string `db:"name" json:"name"`
Description pgtype.Text `db:"description" json:"description"`
CreatedAt time.Time `db:"created_at" json:"createdAt"`
}
func (q *Queries) CreateCabinet(ctx context.Context, arg CreateCabinetParams) (Cabinet, error) {
row := q.db.QueryRow(ctx, createCabinet,
arg.RoomID,
arg.Name,
arg.Description,
arg.CreatedAt,
)
var i Cabinet
err := row.Scan(
&i.ID,
&i.RoomID,
&i.Name,
&i.Description,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const deleteCabinet = `-- name: DeleteCabinet :exec
DELETE FROM cabinets
WHERE id = $1
`
func (q *Queries) DeleteCabinet(ctx context.Context, id int64) error {
_, err := q.db.Exec(ctx, deleteCabinet, id)
return err
}
const getCabinetByID = `-- name: GetCabinetByID :one
SELECT id, room_id, name, description, created_at, updated_at FROM cabinets
WHERE id = $1
`
func (q *Queries) GetCabinetByID(ctx context.Context, id int64) (Cabinet, error) {
row := q.db.QueryRow(ctx, getCabinetByID, id)
var i Cabinet
err := row.Scan(
&i.ID,
&i.RoomID,
&i.Name,
&i.Description,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const listCabinets = `-- name: ListCabinets :many
SELECT id, room_id, name, description, created_at, updated_at FROM cabinets
ORDER BY created_at DESC
`
func (q *Queries) ListCabinets(ctx context.Context) ([]Cabinet, error) {
rows, err := q.db.Query(ctx, listCabinets)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Cabinet
for rows.Next() {
var i Cabinet
if err := rows.Scan(
&i.ID,
&i.RoomID,
&i.Name,
&i.Description,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const updateCabinet = `-- name: UpdateCabinet :one
UPDATE cabinets
SET name = coalesce($1, name),
description = coalesce($2, description),
updated_at = $3
WHERE id = $4
RETURNING id, room_id, name, description, created_at, updated_at
`
type UpdateCabinetParams struct {
Name string `db:"name" json:"name"`
Description pgtype.Text `db:"description" json:"description"`
UpdatedAt time.Time `db:"updated_at" json:"updatedAt"`
ID int64 `db:"id" json:"id"`
}
func (q *Queries) UpdateCabinet(ctx context.Context, arg UpdateCabinetParams) (Cabinet, error) {
row := q.db.QueryRow(ctx, updateCabinet,
arg.Name,
arg.Description,
arg.UpdatedAt,
arg.ID,
)
var i Cabinet
err := row.Scan(
&i.ID,
&i.RoomID,
&i.Name,
&i.Description,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}

32
sqlc_gen/db.go Normal file
View File

@@ -0,0 +1,32 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
package db
import (
"context"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
)
type DBTX interface {
Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error)
Query(context.Context, string, ...interface{}) (pgx.Rows, error)
QueryRow(context.Context, string, ...interface{}) pgx.Row
}
func New(db DBTX) *Queries {
return &Queries{db: db}
}
type Queries struct {
db DBTX
}
func (q *Queries) WithTx(tx pgx.Tx) *Queries {
return &Queries{
db: tx,
}
}

471
sqlc_gen/models.go Normal file
View File

@@ -0,0 +1,471 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
package db
import (
"database/sql/driver"
"fmt"
"time"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgtype"
)
type ComponentItemStatusEnum string
const (
ComponentItemStatusEnumNormal ComponentItemStatusEnum = "normal"
ComponentItemStatusEnumDamaged ComponentItemStatusEnum = "damaged"
ComponentItemStatusEnumLongUnused ComponentItemStatusEnum = "long_unused"
ComponentItemStatusEnumExpired ComponentItemStatusEnum = "expired"
ComponentItemStatusEnumPendingInspection ComponentItemStatusEnum = "pending_inspection"
)
func (e *ComponentItemStatusEnum) Scan(src interface{}) error {
switch s := src.(type) {
case []byte:
*e = ComponentItemStatusEnum(s)
case string:
*e = ComponentItemStatusEnum(s)
default:
return fmt.Errorf("unsupported scan type for ComponentItemStatusEnum: %T", src)
}
return nil
}
type NullComponentItemStatusEnum struct {
ComponentItemStatusEnum ComponentItemStatusEnum `json:"componentItemStatusEnum"`
Valid bool `json:"valid"` // Valid is true if ComponentItemStatusEnum is not NULL
}
// Scan implements the Scanner interface.
func (ns *NullComponentItemStatusEnum) Scan(value interface{}) error {
if value == nil {
ns.ComponentItemStatusEnum, ns.Valid = "", false
return nil
}
ns.Valid = true
return ns.ComponentItemStatusEnum.Scan(value)
}
// Value implements the driver Valuer interface.
func (ns NullComponentItemStatusEnum) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return string(ns.ComponentItemStatusEnum), nil
}
type ContainerTypeEnum string
const (
ContainerTypeEnumEmptyBox ContainerTypeEnum = "empty_box"
ContainerTypeEnumTray ContainerTypeEnum = "tray"
ContainerTypeEnumPaperBox ContainerTypeEnum = "paper_box"
ContainerTypeEnumPlasticBox ContainerTypeEnum = "plastic_box"
ContainerTypeEnumBag ContainerTypeEnum = "bag"
ContainerTypeEnumOther ContainerTypeEnum = "other"
)
func (e *ContainerTypeEnum) Scan(src interface{}) error {
switch s := src.(type) {
case []byte:
*e = ContainerTypeEnum(s)
case string:
*e = ContainerTypeEnum(s)
default:
return fmt.Errorf("unsupported scan type for ContainerTypeEnum: %T", src)
}
return nil
}
type NullContainerTypeEnum struct {
ContainerTypeEnum ContainerTypeEnum `json:"containerTypeEnum"`
Valid bool `json:"valid"` // Valid is true if ContainerTypeEnum is not NULL
}
// Scan implements the Scanner interface.
func (ns *NullContainerTypeEnum) Scan(value interface{}) error {
if value == nil {
ns.ContainerTypeEnum, ns.Valid = "", false
return nil
}
ns.Valid = true
return ns.ContainerTypeEnum.Scan(value)
}
// Value implements the driver Valuer interface.
func (ns NullContainerTypeEnum) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return string(ns.ContainerTypeEnum), nil
}
type InvoiceStatusEnum string
const (
InvoiceStatusEnumDraft InvoiceStatusEnum = "draft"
InvoiceStatusEnumPending InvoiceStatusEnum = "pending"
InvoiceStatusEnumApproved InvoiceStatusEnum = "approved"
InvoiceStatusEnumCompleted InvoiceStatusEnum = "completed"
InvoiceStatusEnumCancelled InvoiceStatusEnum = "cancelled"
)
func (e *InvoiceStatusEnum) Scan(src interface{}) error {
switch s := src.(type) {
case []byte:
*e = InvoiceStatusEnum(s)
case string:
*e = InvoiceStatusEnum(s)
default:
return fmt.Errorf("unsupported scan type for InvoiceStatusEnum: %T", src)
}
return nil
}
type NullInvoiceStatusEnum struct {
InvoiceStatusEnum InvoiceStatusEnum `json:"invoiceStatusEnum"`
Valid bool `json:"valid"` // Valid is true if InvoiceStatusEnum is not NULL
}
// Scan implements the Scanner interface.
func (ns *NullInvoiceStatusEnum) Scan(value interface{}) error {
if value == nil {
ns.InvoiceStatusEnum, ns.Valid = "", false
return nil
}
ns.Valid = true
return ns.InvoiceStatusEnum.Scan(value)
}
// Value implements the driver Valuer interface.
func (ns NullInvoiceStatusEnum) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return string(ns.InvoiceStatusEnum), nil
}
type InvoiceTypeEnum string
const (
InvoiceTypeEnumImport InvoiceTypeEnum = "import"
InvoiceTypeEnumExport InvoiceTypeEnum = "export"
)
func (e *InvoiceTypeEnum) Scan(src interface{}) error {
switch s := src.(type) {
case []byte:
*e = InvoiceTypeEnum(s)
case string:
*e = InvoiceTypeEnum(s)
default:
return fmt.Errorf("unsupported scan type for InvoiceTypeEnum: %T", src)
}
return nil
}
type NullInvoiceTypeEnum struct {
InvoiceTypeEnum InvoiceTypeEnum `json:"invoiceTypeEnum"`
Valid bool `json:"valid"` // Valid is true if InvoiceTypeEnum is not NULL
}
// Scan implements the Scanner interface.
func (ns *NullInvoiceTypeEnum) Scan(value interface{}) error {
if value == nil {
ns.InvoiceTypeEnum, ns.Valid = "", false
return nil
}
ns.Valid = true
return ns.InvoiceTypeEnum.Scan(value)
}
// Value implements the driver Valuer interface.
func (ns NullInvoiceTypeEnum) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return string(ns.InvoiceTypeEnum), nil
}
type TransactionTypeEnum string
const (
TransactionTypeEnumImport TransactionTypeEnum = "import"
TransactionTypeEnumExport TransactionTypeEnum = "export"
TransactionTypeEnumAdjustment TransactionTypeEnum = "adjustment"
TransactionTypeEnumTransfer TransactionTypeEnum = "transfer"
)
func (e *TransactionTypeEnum) Scan(src interface{}) error {
switch s := src.(type) {
case []byte:
*e = TransactionTypeEnum(s)
case string:
*e = TransactionTypeEnum(s)
default:
return fmt.Errorf("unsupported scan type for TransactionTypeEnum: %T", src)
}
return nil
}
type NullTransactionTypeEnum struct {
TransactionTypeEnum TransactionTypeEnum `json:"transactionTypeEnum"`
Valid bool `json:"valid"` // Valid is true if TransactionTypeEnum is not NULL
}
// Scan implements the Scanner interface.
func (ns *NullTransactionTypeEnum) Scan(value interface{}) error {
if value == nil {
ns.TransactionTypeEnum, ns.Valid = "", false
return nil
}
ns.Valid = true
return ns.TransactionTypeEnum.Scan(value)
}
// Value implements the driver Valuer interface.
func (ns NullTransactionTypeEnum) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return string(ns.TransactionTypeEnum), nil
}
type AlternativeComponent struct {
ID int64 `db:"id" json:"id"`
InvoiceConfigItemID int64 `db:"invoice_config_item_id" json:"invoiceConfigItemId"`
AlternativeComponentID int64 `db:"alternative_component_id" json:"alternativeComponentId"`
ConversionRatio pgtype.Numeric `db:"conversion_ratio" json:"conversionRatio"`
Priority int32 `db:"priority" json:"priority"`
Note pgtype.Text `db:"note" json:"note"`
Metadata []byte `db:"metadata" json:"metadata"`
}
type Cabinet struct {
ID int64 `db:"id" json:"id"`
RoomID int64 `db:"room_id" json:"roomId"`
Name string `db:"name" json:"name"`
Description pgtype.Text `db:"description" json:"description"`
CreatedAt time.Time `db:"created_at" json:"createdAt"`
UpdatedAt time.Time `db:"updated_at" json:"updatedAt"`
}
type Component struct {
ID int64 `db:"id" json:"id"`
ComponentTypeID int64 `db:"component_type_id" json:"componentTypeId"`
Name string `db:"name" json:"name"`
Description pgtype.Text `db:"description" json:"description"`
Unit string `db:"unit" json:"unit"`
TotalQuantity int32 `db:"total_quantity" json:"totalQuantity"`
MinQuantity int32 `db:"min_quantity" json:"minQuantity"`
Metadata []byte `db:"metadata" json:"metadata"`
CreatedAt time.Time `db:"created_at" json:"createdAt"`
UpdatedAt time.Time `db:"updated_at" json:"updatedAt"`
}
type ComponentCode struct {
ID int64 `db:"id" json:"id"`
ComponentID int64 `db:"component_id" json:"componentId"`
Code string `db:"code" json:"code"`
CodeType pgtype.Text `db:"code_type" json:"codeType"`
IsPrimary bool `db:"is_primary" json:"isPrimary"`
Metadata []byte `db:"metadata" json:"metadata"`
CreatedAt time.Time `db:"created_at" json:"createdAt"`
}
type ComponentItem struct {
ID int64 `db:"id" json:"id"`
ComponentID int64 `db:"component_id" json:"componentId"`
ContainerID int64 `db:"container_id" json:"containerId"`
Quantity int32 `db:"quantity" json:"quantity"`
Status ComponentItemStatusEnum `db:"status" json:"status"`
Metadata []byte `db:"metadata" json:"metadata"`
CreatedAt time.Time `db:"created_at" json:"createdAt"`
UpdatedAt time.Time `db:"updated_at" json:"updatedAt"`
}
type ComponentStatusHistory struct {
ID int64 `db:"id" json:"id"`
ComponentItemID int64 `db:"component_item_id" json:"componentItemId"`
OldStatus NullComponentItemStatusEnum `db:"old_status" json:"oldStatus"`
NewStatus ComponentItemStatusEnum `db:"new_status" json:"newStatus"`
ChangedQuantity pgtype.Int4 `db:"changed_quantity" json:"changedQuantity"`
Note pgtype.Text `db:"note" json:"note"`
ChangedBy pgtype.Text `db:"changed_by" json:"changedBy"`
ChangedAt time.Time `db:"changed_at" json:"changedAt"`
}
type ComponentType struct {
ID int64 `db:"id" json:"id"`
Name string `db:"name" json:"name"`
Description pgtype.Text `db:"description" json:"description"`
Metadata []byte `db:"metadata" json:"metadata"`
CreatedAt time.Time `db:"created_at" json:"createdAt"`
UpdatedAt time.Time `db:"updated_at" json:"updatedAt"`
}
type Container struct {
ID int64 `db:"id" json:"id"`
ShelfID int64 `db:"shelf_id" json:"shelfId"`
Name string `db:"name" json:"name"`
ContainerType ContainerTypeEnum `db:"container_type" json:"containerType"`
Description pgtype.Text `db:"description" json:"description"`
MaxCapacity pgtype.Int4 `db:"max_capacity" json:"maxCapacity"`
Metadata []byte `db:"metadata" json:"metadata"`
CreatedAt time.Time `db:"created_at" json:"createdAt"`
UpdatedAt time.Time `db:"updated_at" json:"updatedAt"`
}
type Invoice struct {
ID int64 `db:"id" json:"id"`
InvoiceCode string `db:"invoice_code" json:"invoiceCode"`
Type InvoiceTypeEnum `db:"type" json:"type"`
Status InvoiceStatusEnum `db:"status" json:"status"`
InvoiceConfigID pgtype.Int8 `db:"invoice_config_id" json:"invoiceConfigId"`
TotalItems int32 `db:"total_items" json:"totalItems"`
Note pgtype.Text `db:"note" json:"note"`
CreatedBy pgtype.Text `db:"created_by" json:"createdBy"`
ApprovedBy pgtype.Text `db:"approved_by" json:"approvedBy"`
CompletedAt pgtype.Timestamptz `db:"completed_at" json:"completedAt"`
CreatedAt time.Time `db:"created_at" json:"createdAt"`
UpdatedAt time.Time `db:"updated_at" json:"updatedAt"`
Metadata []byte `db:"metadata" json:"metadata"`
}
type InvoiceConfig struct {
ID int64 `db:"id" json:"id"`
Name string `db:"name" json:"name"`
Type InvoiceTypeEnum `db:"type" json:"type"`
Description pgtype.Text `db:"description" json:"description"`
IsActive bool `db:"is_active" json:"isActive"`
Metadata []byte `db:"metadata" json:"metadata"`
CreatedAt time.Time `db:"created_at" json:"createdAt"`
UpdatedAt time.Time `db:"updated_at" json:"updatedAt"`
}
type InvoiceConfigItem struct {
ID int64 `db:"id" json:"id"`
InvoiceConfigID int64 `db:"invoice_config_id" json:"invoiceConfigId"`
ComponentID int64 `db:"component_id" json:"componentId"`
RequiredQuantity int32 `db:"required_quantity" json:"requiredQuantity"`
AllowAlternative bool `db:"allow_alternative" json:"allowAlternative"`
PriorityOrder int32 `db:"priority_order" json:"priorityOrder"`
Note pgtype.Text `db:"note" json:"note"`
Metadata []byte `db:"metadata" json:"metadata"`
}
type InvoiceItem struct {
ID int64 `db:"id" json:"id"`
InvoiceID int64 `db:"invoice_id" json:"invoiceId"`
ComponentID int64 `db:"component_id" json:"componentId"`
OriginalComponentID pgtype.Int8 `db:"original_component_id" json:"originalComponentId"`
RequiredQuantity int32 `db:"required_quantity" json:"requiredQuantity"`
ActualQuantity int32 `db:"actual_quantity" json:"actualQuantity"`
IsSubstituted bool `db:"is_substituted" json:"isSubstituted"`
IsShort bool `db:"is_short" json:"isShort"`
ShortageQuantity int32 `db:"shortage_quantity" json:"shortageQuantity"`
Note pgtype.Text `db:"note" json:"note"`
Metadata []byte `db:"metadata" json:"metadata"`
}
type InvoiceItemLocation struct {
ID int64 `db:"id" json:"id"`
InvoiceItemID int64 `db:"invoice_item_id" json:"invoiceItemId"`
ContainerID int64 `db:"container_id" json:"containerId"`
Quantity int32 `db:"quantity" json:"quantity"`
}
type InvoiceStatusHistory struct {
ID int64 `db:"id" json:"id"`
InvoiceID int64 `db:"invoice_id" json:"invoiceId"`
OldStatus pgtype.Text `db:"old_status" json:"oldStatus"`
NewStatus string `db:"new_status" json:"newStatus"`
ChangedBy pgtype.Text `db:"changed_by" json:"changedBy"`
Note pgtype.Text `db:"note" json:"note"`
ChangedAt time.Time `db:"changed_at" json:"changedAt"`
}
type Permission struct {
ID uuid.UUID `db:"id" json:"id"`
Name string `db:"name" json:"name"`
Description pgtype.Text `db:"description" json:"description"`
CreatedAt pgtype.Timestamptz `db:"created_at" json:"createdAt"`
CreatedBy pgtype.Text `db:"created_by" json:"createdBy"`
}
type Role struct {
ID uuid.UUID `db:"id" json:"id"`
Name string `db:"name" json:"name"`
Description pgtype.Text `db:"description" json:"description"`
CreatedAt pgtype.Timestamptz `db:"created_at" json:"createdAt"`
CreatedBy pgtype.Text `db:"created_by" json:"createdBy"`
}
type RolePermission struct {
RoleID uuid.UUID `db:"role_id" json:"roleId"`
PermissionID uuid.UUID `db:"permission_id" json:"permissionId"`
AssignedAt pgtype.Timestamptz `db:"assigned_at" json:"assignedAt"`
}
type Room struct {
ID int64 `db:"id" json:"id"`
WarehouseID int64 `db:"warehouse_id" json:"warehouseId"`
Name string `db:"name" json:"name"`
Description pgtype.Text `db:"description" json:"description"`
CreatedAt time.Time `db:"created_at" json:"createdAt"`
UpdatedAt time.Time `db:"updated_at" json:"updatedAt"`
}
type Shelf struct {
ID int64 `db:"id" json:"id"`
CabinetID int64 `db:"cabinet_id" json:"cabinetId"`
Name string `db:"name" json:"name"`
LevelIndex int32 `db:"level_index" json:"levelIndex"`
Description pgtype.Text `db:"description" json:"description"`
CreatedAt time.Time `db:"created_at" json:"createdAt"`
UpdatedAt time.Time `db:"updated_at" json:"updatedAt"`
}
type StockTransaction struct {
ID int64 `db:"id" json:"id"`
InvoiceID int64 `db:"invoice_id" json:"invoiceId"`
ComponentID int64 `db:"component_id" json:"componentId"`
ContainerID int64 `db:"container_id" json:"containerId"`
TransactionType TransactionTypeEnum `db:"transaction_type" json:"transactionType"`
Quantity int32 `db:"quantity" json:"quantity"`
BalanceAfter pgtype.Int4 `db:"balance_after" json:"balanceAfter"`
Note pgtype.Text `db:"note" json:"note"`
CreatedBy pgtype.Text `db:"created_by" json:"createdBy"`
CreatedAt time.Time `db:"created_at" json:"createdAt"`
}
type User struct {
ID uuid.UUID `db:"id" json:"id"`
Username string `db:"username" json:"username"`
Email string `db:"email" json:"email"`
PasswordHash string `db:"password_hash" json:"passwordHash"`
FullName pgtype.Text `db:"full_name" json:"fullName"`
IsActive pgtype.Bool `db:"is_active" json:"isActive"`
CreatedAt pgtype.Timestamptz `db:"created_at" json:"createdAt"`
UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updatedAt"`
CreatedBy pgtype.Text `db:"created_by" json:"createdBy"`
}
type UserRole struct {
UserID uuid.UUID `db:"user_id" json:"userId"`
RoleID uuid.UUID `db:"role_id" json:"roleId"`
AssignedAt pgtype.Timestamptz `db:"assigned_at" json:"assignedAt"`
}
type Warehouse struct {
ID int64 `db:"id" json:"id"`
Name string `db:"name" json:"name"`
Description pgtype.Text `db:"description" json:"description"`
Address pgtype.Text `db:"address" json:"address"`
CreatedAt time.Time `db:"created_at" json:"createdAt"`
UpdatedAt time.Time `db:"updated_at" json:"updatedAt"`
}

47
sqlc_gen/querier.go Normal file
View File

@@ -0,0 +1,47 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
package db
import (
"context"
"github.com/google/uuid"
)
type Querier interface {
AssignRoleToUser(ctx context.Context, arg AssignRoleToUserParams) (UserRole, error)
CountUsersByRoleID(ctx context.Context, roleID uuid.UUID) (int64, error)
CreateCabinet(ctx context.Context, arg CreateCabinetParams) (Cabinet, error)
CreateRole(ctx context.Context, arg CreateRoleParams) (Role, error)
CreateRoom(ctx context.Context, arg CreateRoomParams) (Room, error)
CreateUser(ctx context.Context, arg CreateUserParams) (uuid.UUID, error)
CreateWarehouse(ctx context.Context, arg CreateWarehouseParams) (Warehouse, error)
DeleteCabinet(ctx context.Context, id int64) error
DeleteRole(ctx context.Context, id uuid.UUID) error
DeleteRoom(ctx context.Context, id int64) error
DeleteWarehouse(ctx context.Context, id int64) error
GetCabinetByID(ctx context.Context, id int64) (Cabinet, error)
GetRoleByID(ctx context.Context, id uuid.UUID) (Role, error)
GetRoomByID(ctx context.Context, id int64) (Room, error)
GetUserByEmail(ctx context.Context, email string) (User, error)
GetUserByID(ctx context.Context, id uuid.UUID) (User, error)
GetUserByUsername(ctx context.Context, username string) (User, error)
GetUserRole(ctx context.Context, arg GetUserRoleParams) (UserRole, error)
GetUserRolesByRoleID(ctx context.Context, roleID uuid.UUID) ([]GetUserRolesByRoleIDRow, error)
GetUserRolesByUserID(ctx context.Context, userID uuid.UUID) ([]GetUserRolesByUserIDRow, error)
GetWarehouseByID(ctx context.Context, id int64) (Warehouse, error)
ListCabinets(ctx context.Context) ([]Cabinet, error)
ListRoles(ctx context.Context) ([]Role, error)
ListRooms(ctx context.Context) ([]Room, error)
ListWarehouses(ctx context.Context) ([]Warehouse, error)
RemoveAllRolesFromUser(ctx context.Context, userID uuid.UUID) error
RemoveRoleFromUser(ctx context.Context, arg RemoveRoleFromUserParams) error
UpdateCabinet(ctx context.Context, arg UpdateCabinetParams) (Cabinet, error)
UpdateRole(ctx context.Context, arg UpdateRoleParams) (Role, error)
UpdateRoom(ctx context.Context, arg UpdateRoomParams) (Room, error)
UpdateWarehouse(ctx context.Context, arg UpdateWarehouseParams) (Warehouse, error)
}
var _ Querier = (*Queries)(nil)

127
sqlc_gen/roles.sql.go Normal file
View File

@@ -0,0 +1,127 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: roles.sql
package db
import (
"context"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgtype"
)
const createRole = `-- name: CreateRole :one
INSERT INTO roles (name, description, created_by)
VALUES (
$1,
$2,
$3)
RETURNING id, name, description, created_at, created_by
`
type CreateRoleParams struct {
Name string `db:"name" json:"name"`
Description pgtype.Text `db:"description" json:"description"`
CreatedBy pgtype.Text `db:"created_by" json:"createdBy"`
}
func (q *Queries) CreateRole(ctx context.Context, arg CreateRoleParams) (Role, error) {
row := q.db.QueryRow(ctx, createRole, arg.Name, arg.Description, arg.CreatedBy)
var i Role
err := row.Scan(
&i.ID,
&i.Name,
&i.Description,
&i.CreatedAt,
&i.CreatedBy,
)
return i, err
}
const deleteRole = `-- name: DeleteRole :exec
DELETE FROM roles
WHERE id = $1
`
func (q *Queries) DeleteRole(ctx context.Context, id uuid.UUID) error {
_, err := q.db.Exec(ctx, deleteRole, id)
return err
}
const getRoleByID = `-- name: GetRoleByID :one
SELECT id, name, description, created_at, created_by FROM roles
WHERE id = $1
`
func (q *Queries) GetRoleByID(ctx context.Context, id uuid.UUID) (Role, error) {
row := q.db.QueryRow(ctx, getRoleByID, id)
var i Role
err := row.Scan(
&i.ID,
&i.Name,
&i.Description,
&i.CreatedAt,
&i.CreatedBy,
)
return i, err
}
const listRoles = `-- name: ListRoles :many
SELECT id, name, description, created_at, created_by FROM roles
ORDER BY created_at DESC
`
func (q *Queries) ListRoles(ctx context.Context) ([]Role, error) {
rows, err := q.db.Query(ctx, listRoles)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Role
for rows.Next() {
var i Role
if err := rows.Scan(
&i.ID,
&i.Name,
&i.Description,
&i.CreatedAt,
&i.CreatedBy,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const updateRole = `-- name: UpdateRole :one
UPDATE roles
SET name = $1,
description = $2
WHERE id = $3
RETURNING id, name, description, created_at, created_by
`
type UpdateRoleParams struct {
Name string `db:"name" json:"name"`
Description pgtype.Text `db:"description" json:"description"`
ID uuid.UUID `db:"id" json:"id"`
}
func (q *Queries) UpdateRole(ctx context.Context, arg UpdateRoleParams) (Role, error) {
row := q.db.QueryRow(ctx, updateRole, arg.Name, arg.Description, arg.ID)
var i Role
err := row.Scan(
&i.ID,
&i.Name,
&i.Description,
&i.CreatedAt,
&i.CreatedBy,
)
return i, err
}

146
sqlc_gen/room.sql.go Normal file
View File

@@ -0,0 +1,146 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: room.sql
package db
import (
"context"
"time"
"github.com/jackc/pgx/v5/pgtype"
)
const createRoom = `-- name: CreateRoom :one
INSERT INTO rooms (warehouse_id,name, description, created_at)
VALUES (
$1,
$2,
$3,
$4
)
RETURNING id, warehouse_id, name, description, created_at, updated_at
`
type CreateRoomParams struct {
WarehouseID int64 `db:"warehouse_id" json:"warehouseId"`
Name string `db:"name" json:"name"`
Description pgtype.Text `db:"description" json:"description"`
CreatedAt time.Time `db:"created_at" json:"createdAt"`
}
func (q *Queries) CreateRoom(ctx context.Context, arg CreateRoomParams) (Room, error) {
row := q.db.QueryRow(ctx, createRoom,
arg.WarehouseID,
arg.Name,
arg.Description,
arg.CreatedAt,
)
var i Room
err := row.Scan(
&i.ID,
&i.WarehouseID,
&i.Name,
&i.Description,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const deleteRoom = `-- name: DeleteRoom :exec
DELETE FROM rooms
WHERE id = $1
`
func (q *Queries) DeleteRoom(ctx context.Context, id int64) error {
_, err := q.db.Exec(ctx, deleteRoom, id)
return err
}
const getRoomByID = `-- name: GetRoomByID :one
SELECT id, warehouse_id, name, description, created_at, updated_at FROM rooms
WHERE id = $1
`
func (q *Queries) GetRoomByID(ctx context.Context, id int64) (Room, error) {
row := q.db.QueryRow(ctx, getRoomByID, id)
var i Room
err := row.Scan(
&i.ID,
&i.WarehouseID,
&i.Name,
&i.Description,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const listRooms = `-- name: ListRooms :many
SELECT id, warehouse_id, name, description, created_at, updated_at FROM rooms
ORDER BY created_at DESC
`
func (q *Queries) ListRooms(ctx context.Context) ([]Room, error) {
rows, err := q.db.Query(ctx, listRooms)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Room
for rows.Next() {
var i Room
if err := rows.Scan(
&i.ID,
&i.WarehouseID,
&i.Name,
&i.Description,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const updateRoom = `-- name: UpdateRoom :one
UPDATE rooms
SET name = coalesce($1, name),
description = coalesce($2, description),
updated_at = $3
WHERE id = $4
RETURNING id, warehouse_id, name, description, created_at, updated_at
`
type UpdateRoomParams struct {
Name string `db:"name" json:"name"`
Description pgtype.Text `db:"description" json:"description"`
UpdatedAt time.Time `db:"updated_at" json:"updatedAt"`
ID int64 `db:"id" json:"id"`
}
func (q *Queries) UpdateRoom(ctx context.Context, arg UpdateRoomParams) (Room, error) {
row := q.db.QueryRow(ctx, updateRoom,
arg.Name,
arg.Description,
arg.UpdatedAt,
arg.ID,
)
var i Room
err := row.Scan(
&i.ID,
&i.WarehouseID,
&i.Name,
&i.Description,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}

173
sqlc_gen/user_roles.sql.go Normal file
View File

@@ -0,0 +1,173 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: user_roles.sql
package db
import (
"context"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgtype"
)
const assignRoleToUser = `-- name: AssignRoleToUser :one
INSERT INTO user_roles (user_id, role_id)
VALUES (
$1,
$2)
RETURNING user_id, role_id, assigned_at
`
type AssignRoleToUserParams struct {
UserID uuid.UUID `db:"user_id" json:"userId"`
RoleID uuid.UUID `db:"role_id" json:"roleId"`
}
func (q *Queries) AssignRoleToUser(ctx context.Context, arg AssignRoleToUserParams) (UserRole, error) {
row := q.db.QueryRow(ctx, assignRoleToUser, arg.UserID, arg.RoleID)
var i UserRole
err := row.Scan(&i.UserID, &i.RoleID, &i.AssignedAt)
return i, err
}
const countUsersByRoleID = `-- name: CountUsersByRoleID :one
SELECT COUNT(*) FROM user_roles
WHERE role_id = $1
`
func (q *Queries) CountUsersByRoleID(ctx context.Context, roleID uuid.UUID) (int64, error) {
row := q.db.QueryRow(ctx, countUsersByRoleID, roleID)
var count int64
err := row.Scan(&count)
return count, err
}
const getUserRole = `-- name: GetUserRole :one
SELECT user_id, role_id, assigned_at FROM user_roles
WHERE user_id = $1 AND role_id = $2
`
type GetUserRoleParams struct {
UserID uuid.UUID `db:"user_id" json:"userId"`
RoleID uuid.UUID `db:"role_id" json:"roleId"`
}
func (q *Queries) GetUserRole(ctx context.Context, arg GetUserRoleParams) (UserRole, error) {
row := q.db.QueryRow(ctx, getUserRole, arg.UserID, arg.RoleID)
var i UserRole
err := row.Scan(&i.UserID, &i.RoleID, &i.AssignedAt)
return i, err
}
const getUserRolesByRoleID = `-- name: GetUserRolesByRoleID :many
SELECT ur.user_id, ur.role_id, ur.assigned_at, u.username, u.email, u.full_name
FROM user_roles ur
JOIN users u ON u.id = ur.user_id
WHERE ur.role_id = $1
ORDER BY ur.assigned_at DESC
`
type GetUserRolesByRoleIDRow struct {
UserID uuid.UUID `db:"user_id" json:"userId"`
RoleID uuid.UUID `db:"role_id" json:"roleId"`
AssignedAt pgtype.Timestamptz `db:"assigned_at" json:"assignedAt"`
Username string `db:"username" json:"username"`
Email string `db:"email" json:"email"`
FullName pgtype.Text `db:"full_name" json:"fullName"`
}
func (q *Queries) GetUserRolesByRoleID(ctx context.Context, roleID uuid.UUID) ([]GetUserRolesByRoleIDRow, error) {
rows, err := q.db.Query(ctx, getUserRolesByRoleID, roleID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetUserRolesByRoleIDRow
for rows.Next() {
var i GetUserRolesByRoleIDRow
if err := rows.Scan(
&i.UserID,
&i.RoleID,
&i.AssignedAt,
&i.Username,
&i.Email,
&i.FullName,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getUserRolesByUserID = `-- name: GetUserRolesByUserID :many
SELECT ur.user_id, ur.role_id, ur.assigned_at, r.name AS role_name, r.description AS role_description
FROM user_roles ur
JOIN roles r ON r.id = ur.role_id
WHERE ur.user_id = $1
ORDER BY ur.assigned_at DESC
`
type GetUserRolesByUserIDRow struct {
UserID uuid.UUID `db:"user_id" json:"userId"`
RoleID uuid.UUID `db:"role_id" json:"roleId"`
AssignedAt pgtype.Timestamptz `db:"assigned_at" json:"assignedAt"`
RoleName string `db:"role_name" json:"roleName"`
RoleDescription pgtype.Text `db:"role_description" json:"roleDescription"`
}
func (q *Queries) GetUserRolesByUserID(ctx context.Context, userID uuid.UUID) ([]GetUserRolesByUserIDRow, error) {
rows, err := q.db.Query(ctx, getUserRolesByUserID, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetUserRolesByUserIDRow
for rows.Next() {
var i GetUserRolesByUserIDRow
if err := rows.Scan(
&i.UserID,
&i.RoleID,
&i.AssignedAt,
&i.RoleName,
&i.RoleDescription,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const removeAllRolesFromUser = `-- name: RemoveAllRolesFromUser :exec
DELETE FROM user_roles
WHERE user_id = $1
`
func (q *Queries) RemoveAllRolesFromUser(ctx context.Context, userID uuid.UUID) error {
_, err := q.db.Exec(ctx, removeAllRolesFromUser, userID)
return err
}
const removeRoleFromUser = `-- name: RemoveRoleFromUser :exec
DELETE FROM user_roles
WHERE user_id = $1 AND role_id = $2
`
type RemoveRoleFromUserParams struct {
UserID uuid.UUID `db:"user_id" json:"userId"`
RoleID uuid.UUID `db:"role_id" json:"roleId"`
}
func (q *Queries) RemoveRoleFromUser(ctx context.Context, arg RemoveRoleFromUserParams) error {
_, err := q.db.Exec(ctx, removeRoleFromUser, arg.UserID, arg.RoleID)
return err
}

113
sqlc_gen/users.sql.go Normal file
View File

@@ -0,0 +1,113 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: users.sql
package db
import (
"context"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgtype"
)
const createUser = `-- name: CreateUser :one
INSERT INTO users (username, email, password_hash, full_name, created_by)
VALUES (
$1,
$2,
$3,
$4,
$5)
RETURNING id
`
type CreateUserParams struct {
Username string `db:"username" json:"username"`
Email string `db:"email" json:"email"`
PasswordHash string `db:"password_hash" json:"passwordHash"`
FullName pgtype.Text `db:"full_name" json:"fullName"`
CreatedBy pgtype.Text `db:"created_by" json:"createdBy"`
}
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (uuid.UUID, error) {
row := q.db.QueryRow(ctx, createUser,
arg.Username,
arg.Email,
arg.PasswordHash,
arg.FullName,
arg.CreatedBy,
)
var id uuid.UUID
err := row.Scan(&id)
return id, err
}
const getUserByEmail = `-- name: GetUserByEmail :one
SELECT id, username, email, password_hash, full_name, is_active, created_at, updated_at, created_by FROM users
WHERE email = $1
LIMIT 1
`
func (q *Queries) GetUserByEmail(ctx context.Context, email string) (User, error) {
row := q.db.QueryRow(ctx, getUserByEmail, email)
var i User
err := row.Scan(
&i.ID,
&i.Username,
&i.Email,
&i.PasswordHash,
&i.FullName,
&i.IsActive,
&i.CreatedAt,
&i.UpdatedAt,
&i.CreatedBy,
)
return i, err
}
const getUserByID = `-- name: GetUserByID :one
SELECT id, username, email, password_hash, full_name, is_active, created_at, updated_at, created_by FROM users
WHERE id = $1
`
func (q *Queries) GetUserByID(ctx context.Context, id uuid.UUID) (User, error) {
row := q.db.QueryRow(ctx, getUserByID, id)
var i User
err := row.Scan(
&i.ID,
&i.Username,
&i.Email,
&i.PasswordHash,
&i.FullName,
&i.IsActive,
&i.CreatedAt,
&i.UpdatedAt,
&i.CreatedBy,
)
return i, err
}
const getUserByUsername = `-- name: GetUserByUsername :one
SELECT id, username, email, password_hash, full_name, is_active, created_at, updated_at, created_by FROM users
WHERE username = $1
LIMIT 1
`
func (q *Queries) GetUserByUsername(ctx context.Context, username string) (User, error) {
row := q.db.QueryRow(ctx, getUserByUsername, username)
var i User
err := row.Scan(
&i.ID,
&i.Username,
&i.Email,
&i.PasswordHash,
&i.FullName,
&i.IsActive,
&i.CreatedAt,
&i.UpdatedAt,
&i.CreatedBy,
)
return i, err
}

149
sqlc_gen/warehouse.sql.go Normal file
View File

@@ -0,0 +1,149 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: warehouse.sql
package db
import (
"context"
"time"
"github.com/jackc/pgx/v5/pgtype"
)
const createWarehouse = `-- name: CreateWarehouse :one
INSERT INTO warehouses (name, description, address, created_at)
VALUES (
$1,
$2,
$3,
$4
)
RETURNING id, name, description, address, created_at, updated_at
`
type CreateWarehouseParams struct {
Name string `db:"name" json:"name"`
Description pgtype.Text `db:"description" json:"description"`
Address pgtype.Text `db:"address" json:"address"`
CreatedAt time.Time `db:"created_at" json:"createdAt"`
}
func (q *Queries) CreateWarehouse(ctx context.Context, arg CreateWarehouseParams) (Warehouse, error) {
row := q.db.QueryRow(ctx, createWarehouse,
arg.Name,
arg.Description,
arg.Address,
arg.CreatedAt,
)
var i Warehouse
err := row.Scan(
&i.ID,
&i.Name,
&i.Description,
&i.Address,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const deleteWarehouse = `-- name: DeleteWarehouse :exec
DELETE FROM warehouses
WHERE id = $1
`
func (q *Queries) DeleteWarehouse(ctx context.Context, id int64) error {
_, err := q.db.Exec(ctx, deleteWarehouse, id)
return err
}
const getWarehouseByID = `-- name: GetWarehouseByID :one
SELECT id, name, description, address, created_at, updated_at FROM warehouses
WHERE id = $1
`
func (q *Queries) GetWarehouseByID(ctx context.Context, id int64) (Warehouse, error) {
row := q.db.QueryRow(ctx, getWarehouseByID, id)
var i Warehouse
err := row.Scan(
&i.ID,
&i.Name,
&i.Description,
&i.Address,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const listWarehouses = `-- name: ListWarehouses :many
SELECT id, name, description, address, created_at, updated_at FROM warehouses
ORDER BY created_at DESC
`
func (q *Queries) ListWarehouses(ctx context.Context) ([]Warehouse, error) {
rows, err := q.db.Query(ctx, listWarehouses)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Warehouse
for rows.Next() {
var i Warehouse
if err := rows.Scan(
&i.ID,
&i.Name,
&i.Description,
&i.Address,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const updateWarehouse = `-- name: UpdateWarehouse :one
UPDATE warehouses
SET name = CASE WHEN $1 = '' THEN name ELSE $1 END,
description = coalesce($2, description),
address = coalesce($3, address),
updated_at = $4
WHERE id = $5
RETURNING id, name, description, address, created_at, updated_at
`
type UpdateWarehouseParams struct {
Name interface{} `db:"name" json:"name"`
Description pgtype.Text `db:"description" json:"description"`
Address pgtype.Text `db:"address" json:"address"`
UpdatedAt time.Time `db:"updated_at" json:"updatedAt"`
ID int64 `db:"id" json:"id"`
}
func (q *Queries) UpdateWarehouse(ctx context.Context, arg UpdateWarehouseParams) (Warehouse, error) {
row := q.db.QueryRow(ctx, updateWarehouse,
arg.Name,
arg.Description,
arg.Address,
arg.UpdatedAt,
arg.ID,
)
var i Warehouse
err := row.Scan(
&i.ID,
&i.Name,
&i.Description,
&i.Address,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}