From 6a4a96e0ca035f711c87ce9b31021b60c0385da1 Mon Sep 17 00:00:00 2001 From: Tran Anh Tuan Date: Fri, 8 May 2026 14:32:24 +0700 Subject: [PATCH] Base Project --- .dockerignore | 50 ++ .env.example | 21 + .gitignore | 36 ++ Makefile | 62 +++ cmd/seed/main.go | 104 ++++ cmd/server/main.go | 18 + configs/config.go | 42 ++ configs/constants/constants.go | 22 + configs/constants/permissions.go | 71 +++ configs/yaml/config.example.yaml | 28 + db/init/init.sql | 485 ++++++++++++++++ db/queries/cabinet.sql | 29 + db/queries/permission.sql | 0 db/queries/roles.sql | 26 + db/queries/room.sql | 29 + db/queries/user_roles.sql | 36 ++ db/queries/users.sql | 23 + db/queries/warehouse.sql | 30 + docker-compose.dev.yml | 47 ++ docs/db/WareHouseDB.md | 427 ++++++++++++++ docs/sqlc/config.md | 170 ++++++ docs/sqlc/query.md | 400 +++++++++++++ docs/swagger/docs.go | 526 ++++++++++++++++++ docs/swagger/swagger.json | 502 +++++++++++++++++ docs/swagger/swagger.yaml | 317 +++++++++++ fsnotify.go | 107 ++++ global/global.go | 45 ++ go.mod | 80 +++ go.sum | 228 ++++++++ internal/initialization/postgresql.go | 47 ++ internal/initialization/redis.go | 39 ++ internal/mapper/role_mapper.go | 28 + internal/mapper/user_mapper.go | 21 + internal/mapper/warehouse_mapper.go | 51 ++ internal/middlewares/auth_middleware.go | 10 + internal/middlewares/logging_middleware.go | 34 ++ internal/models/config_model.go | 43 ++ internal/models/jwt_model.go | 1 + internal/models/requests/auth_request.go | 13 + internal/models/requests/role_request.go | 6 + internal/models/requests/warehouse_request.go | 13 + internal/models/responses/auth_response.go | 8 + internal/models/responses/role_response.go | 5 + .../models/responses/warehouse_response.go | 12 + internal/models/role_model.go | 11 + internal/models/user_model.go | 17 + internal/models/warehouse_model.go | 12 + internal/repositories/auth_repository.go | 45 ++ .../repositories/redis/permission_redis.go | 1 + internal/repositories/role_repository.go | 16 + internal/repositories/warehouse_repository.go | 48 ++ internal/routers/router.go | 45 ++ internal/services/auth_service.go | 128 +++++ internal/services/check_service.go | 18 + internal/services/role_service.go | 58 ++ internal/services/warehouse_service.go | 171 ++++++ pkg/helper/jwt.go | 39 ++ pkg/helper/password.go | 22 + pkg/helper/validator.go | 24 + pkg/log/logger.go | 74 +++ pkg/utils/async_handler.go | 12 + response/error_response.go | 175 ++++++ response/http_reason_phrases.go | 51 ++ response/success_response.go | 50 ++ sqlc.yaml | 36 ++ sqlc_gen/cabinet.sql.go | 146 +++++ sqlc_gen/db.go | 32 ++ sqlc_gen/models.go | 471 ++++++++++++++++ sqlc_gen/querier.go | 47 ++ sqlc_gen/roles.sql.go | 127 +++++ sqlc_gen/room.sql.go | 146 +++++ sqlc_gen/user_roles.sql.go | 173 ++++++ sqlc_gen/users.sql.go | 113 ++++ sqlc_gen/warehouse.sql.go | 149 +++++ 74 files changed, 6749 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 cmd/seed/main.go create mode 100644 cmd/server/main.go create mode 100644 configs/config.go create mode 100644 configs/constants/constants.go create mode 100644 configs/constants/permissions.go create mode 100644 configs/yaml/config.example.yaml create mode 100644 db/init/init.sql create mode 100644 db/queries/cabinet.sql create mode 100644 db/queries/permission.sql create mode 100644 db/queries/roles.sql create mode 100644 db/queries/room.sql create mode 100644 db/queries/user_roles.sql create mode 100644 db/queries/users.sql create mode 100644 db/queries/warehouse.sql create mode 100644 docker-compose.dev.yml create mode 100644 docs/db/WareHouseDB.md create mode 100644 docs/sqlc/config.md create mode 100644 docs/sqlc/query.md create mode 100644 docs/swagger/docs.go create mode 100644 docs/swagger/swagger.json create mode 100644 docs/swagger/swagger.yaml create mode 100644 fsnotify.go create mode 100644 global/global.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/initialization/postgresql.go create mode 100644 internal/initialization/redis.go create mode 100644 internal/mapper/role_mapper.go create mode 100644 internal/mapper/user_mapper.go create mode 100644 internal/mapper/warehouse_mapper.go create mode 100644 internal/middlewares/auth_middleware.go create mode 100644 internal/middlewares/logging_middleware.go create mode 100644 internal/models/config_model.go create mode 100644 internal/models/jwt_model.go create mode 100644 internal/models/requests/auth_request.go create mode 100644 internal/models/requests/role_request.go create mode 100644 internal/models/requests/warehouse_request.go create mode 100644 internal/models/responses/auth_response.go create mode 100644 internal/models/responses/role_response.go create mode 100644 internal/models/responses/warehouse_response.go create mode 100644 internal/models/role_model.go create mode 100644 internal/models/user_model.go create mode 100644 internal/models/warehouse_model.go create mode 100644 internal/repositories/auth_repository.go create mode 100644 internal/repositories/redis/permission_redis.go create mode 100644 internal/repositories/role_repository.go create mode 100644 internal/repositories/warehouse_repository.go create mode 100644 internal/routers/router.go create mode 100644 internal/services/auth_service.go create mode 100644 internal/services/check_service.go create mode 100644 internal/services/role_service.go create mode 100644 internal/services/warehouse_service.go create mode 100644 pkg/helper/jwt.go create mode 100644 pkg/helper/password.go create mode 100644 pkg/helper/validator.go create mode 100644 pkg/log/logger.go create mode 100644 pkg/utils/async_handler.go create mode 100644 response/error_response.go create mode 100644 response/http_reason_phrases.go create mode 100644 response/success_response.go create mode 100644 sqlc.yaml create mode 100644 sqlc_gen/cabinet.sql.go create mode 100644 sqlc_gen/db.go create mode 100644 sqlc_gen/models.go create mode 100644 sqlc_gen/querier.go create mode 100644 sqlc_gen/roles.sql.go create mode 100644 sqlc_gen/room.sql.go create mode 100644 sqlc_gen/user_roles.sql.go create mode 100644 sqlc_gen/users.sql.go create mode 100644 sqlc_gen/warehouse.sql.go diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f696eac --- /dev/null +++ b/.dockerignore @@ -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 + + diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..d5bf4ea --- /dev/null +++ b/.env.example @@ -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= \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1d80f28 --- /dev/null +++ b/.gitignore @@ -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/ \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c8f198f --- /dev/null +++ b/Makefile @@ -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 \ No newline at end of file diff --git a/cmd/seed/main.go b/cmd/seed/main.go new file mode 100644 index 0000000..148bb46 --- /dev/null +++ b/cmd/seed/main.go @@ -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 +} diff --git a/cmd/server/main.go b/cmd/server/main.go new file mode 100644 index 0000000..db7bb25 --- /dev/null +++ b/cmd/server/main.go @@ -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) +} diff --git a/configs/config.go b/configs/config.go new file mode 100644 index 0000000..8741cf7 --- /dev/null +++ b/configs/config.go @@ -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 +} diff --git a/configs/constants/constants.go b/configs/constants/constants.go new file mode 100644 index 0000000..86367cd --- /dev/null +++ b/configs/constants/constants.go @@ -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" +) diff --git a/configs/constants/permissions.go b/configs/constants/permissions.go new file mode 100644 index 0000000..98f6b99 --- /dev/null +++ b/configs/constants/permissions.go @@ -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" +) diff --git a/configs/yaml/config.example.yaml b/configs/yaml/config.example.yaml new file mode 100644 index 0000000..061e917 --- /dev/null +++ b/configs/yaml/config.example.yaml @@ -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: diff --git a/db/init/init.sql b/db/init/init.sql new file mode 100644 index 0000000..a3dc672 --- /dev/null +++ b/db/init/init.sql @@ -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'; \ No newline at end of file diff --git a/db/queries/cabinet.sql b/db/queries/cabinet.sql new file mode 100644 index 0000000..cd21c0b --- /dev/null +++ b/db/queries/cabinet.sql @@ -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); diff --git a/db/queries/permission.sql b/db/queries/permission.sql new file mode 100644 index 0000000..e69de29 diff --git a/db/queries/roles.sql b/db/queries/roles.sql new file mode 100644 index 0000000..2a1f6a4 --- /dev/null +++ b/db/queries/roles.sql @@ -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); diff --git a/db/queries/room.sql b/db/queries/room.sql new file mode 100644 index 0000000..f9d4de2 --- /dev/null +++ b/db/queries/room.sql @@ -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); diff --git a/db/queries/user_roles.sql b/db/queries/user_roles.sql new file mode 100644 index 0000000..93a7a7c --- /dev/null +++ b/db/queries/user_roles.sql @@ -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); diff --git a/db/queries/users.sql b/db/queries/users.sql new file mode 100644 index 0000000..7626339 --- /dev/null +++ b/db/queries/users.sql @@ -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; diff --git a/db/queries/warehouse.sql b/db/queries/warehouse.sql new file mode 100644 index 0000000..946a993 --- /dev/null +++ b/db/queries/warehouse.sql @@ -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); diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..ef85d12 --- /dev/null +++ b/docker-compose.dev.yml @@ -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: diff --git a/docs/db/WareHouseDB.md b/docs/db/WareHouseDB.md new file mode 100644 index 0000000..64760c6 --- /dev/null +++ b/docs/db/WareHouseDB.md @@ -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` | diff --git a/docs/sqlc/config.md b/docs/sqlc/config.md new file mode 100644 index 0000000..bb2d133 --- /dev/null +++ b/docs/sqlc/config.md @@ -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: "" + queries: "" + schema: "" + gen: + go: + package: "" + out: "" +``` + +--- + +## 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..overrides`. | +| `rename` | map | ❌ | (Deprecated v1) — Không dùng trong v2, thay bằng `gen..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.` với `out` + `package` (hoặc tương đương cho ngôn ngữ khác). diff --git a/docs/sqlc/query.md b/docs/sqlc/query.md new file mode 100644 index 0000000..03ccac8 --- /dev/null +++ b/docs/sqlc/query.md @@ -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: <:command> +-- +SELECT * FROM users WHERE id = $1; +``` + +**Cú pháp annotation:** + +``` +-- name: <: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 | diff --git a/docs/swagger/docs.go b/docs/swagger/docs.go new file mode 100644 index 0000000..68fde21 --- /dev/null +++ b/docs/swagger/docs.go @@ -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) +} diff --git a/docs/swagger/swagger.json b/docs/swagger/swagger.json new file mode 100644 index 0000000..5a3f255 --- /dev/null +++ b/docs/swagger/swagger.json @@ -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" + } + } + } + } +} \ No newline at end of file diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml new file mode 100644 index 0000000..12b760a --- /dev/null +++ b/docs/swagger/swagger.yaml @@ -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" diff --git a/fsnotify.go b/fsnotify.go new file mode 100644 index 0000000..7e06432 --- /dev/null +++ b/fsnotify.go @@ -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 +} diff --git a/global/global.go b/global/global.go new file mode 100644 index 0000000..3e81a89 --- /dev/null +++ b/global/global.go @@ -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) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b1aed56 --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..4e99152 --- /dev/null +++ b/go.sum @@ -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= diff --git a/internal/initialization/postgresql.go b/internal/initialization/postgresql.go new file mode 100644 index 0000000..d7b7e1e --- /dev/null +++ b/internal/initialization/postgresql.go @@ -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) +} diff --git a/internal/initialization/redis.go b/internal/initialization/redis.go new file mode 100644 index 0000000..6f1bce2 --- /dev/null +++ b/internal/initialization/redis.go @@ -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) +} diff --git a/internal/mapper/role_mapper.go b/internal/mapper/role_mapper.go new file mode 100644 index 0000000..101d631 --- /dev/null +++ b/internal/mapper/role_mapper.go @@ -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 != ""}, + } +} diff --git a/internal/mapper/user_mapper.go b/internal/mapper/user_mapper.go new file mode 100644 index 0000000..4ebbd99 --- /dev/null +++ b/internal/mapper/user_mapper.go @@ -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, + } +} diff --git a/internal/mapper/warehouse_mapper.go b/internal/mapper/warehouse_mapper.go new file mode 100644 index 0000000..6d8cdd2 --- /dev/null +++ b/internal/mapper/warehouse_mapper.go @@ -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, + } +} diff --git a/internal/middlewares/auth_middleware.go b/internal/middlewares/auth_middleware.go new file mode 100644 index 0000000..00e28bf --- /dev/null +++ b/internal/middlewares/auth_middleware.go @@ -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() + } +} diff --git a/internal/middlewares/logging_middleware.go b/internal/middlewares/logging_middleware.go new file mode 100644 index 0000000..5f7e582 --- /dev/null +++ b/internal/middlewares/logging_middleware.go @@ -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") + } +} diff --git a/internal/models/config_model.go b/internal/models/config_model.go new file mode 100644 index 0000000..96b8586 --- /dev/null +++ b/internal/models/config_model.go @@ -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 +} diff --git a/internal/models/jwt_model.go b/internal/models/jwt_model.go new file mode 100644 index 0000000..2640e7f --- /dev/null +++ b/internal/models/jwt_model.go @@ -0,0 +1 @@ +package models diff --git a/internal/models/requests/auth_request.go b/internal/models/requests/auth_request.go new file mode 100644 index 0000000..0248b96 --- /dev/null +++ b/internal/models/requests/auth_request.go @@ -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"` +} diff --git a/internal/models/requests/role_request.go b/internal/models/requests/role_request.go new file mode 100644 index 0000000..cf99e7b --- /dev/null +++ b/internal/models/requests/role_request.go @@ -0,0 +1,6 @@ +package requests + +type CreateRoleRequest struct { + Name string `json:"name" binding:"required"` + Description string `json:"description"` +} diff --git a/internal/models/requests/warehouse_request.go b/internal/models/requests/warehouse_request.go new file mode 100644 index 0000000..c3a935d --- /dev/null +++ b/internal/models/requests/warehouse_request.go @@ -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"` +} diff --git a/internal/models/responses/auth_response.go b/internal/models/responses/auth_response.go new file mode 100644 index 0000000..a14cda1 --- /dev/null +++ b/internal/models/responses/auth_response.go @@ -0,0 +1,8 @@ +package responses + +type BodyRegisterResponse struct { + ID string `json:"id"` +} +type BodyLoginResponse struct { + Token string `json:"token"` +} diff --git a/internal/models/responses/role_response.go b/internal/models/responses/role_response.go new file mode 100644 index 0000000..dd66366 --- /dev/null +++ b/internal/models/responses/role_response.go @@ -0,0 +1,5 @@ +package responses + +type BodyRoleResponse struct { + ID string `json:"id"` +} diff --git a/internal/models/responses/warehouse_response.go b/internal/models/responses/warehouse_response.go new file mode 100644 index 0000000..6c51f62 --- /dev/null +++ b/internal/models/responses/warehouse_response.go @@ -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"` +} diff --git a/internal/models/role_model.go b/internal/models/role_model.go new file mode 100644 index 0000000..2e56391 --- /dev/null +++ b/internal/models/role_model.go @@ -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"` +} diff --git a/internal/models/user_model.go b/internal/models/user_model.go new file mode 100644 index 0000000..f71a6bd --- /dev/null +++ b/internal/models/user_model.go @@ -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"` +} diff --git a/internal/models/warehouse_model.go b/internal/models/warehouse_model.go new file mode 100644 index 0000000..adc0b6b --- /dev/null +++ b/internal/models/warehouse_model.go @@ -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"` +} diff --git a/internal/repositories/auth_repository.go b/internal/repositories/auth_repository.go new file mode 100644 index 0000000..65739c6 --- /dev/null +++ b/internal/repositories/auth_repository.go @@ -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 +} diff --git a/internal/repositories/redis/permission_redis.go b/internal/repositories/redis/permission_redis.go new file mode 100644 index 0000000..65a229e --- /dev/null +++ b/internal/repositories/redis/permission_redis.go @@ -0,0 +1 @@ +package redis diff --git a/internal/repositories/role_repository.go b/internal/repositories/role_repository.go new file mode 100644 index 0000000..46cc24e --- /dev/null +++ b/internal/repositories/role_repository.go @@ -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 +} diff --git a/internal/repositories/warehouse_repository.go b/internal/repositories/warehouse_repository.go new file mode 100644 index 0000000..f5ff59c --- /dev/null +++ b/internal/repositories/warehouse_repository.go @@ -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) +} diff --git a/internal/routers/router.go b/internal/routers/router.go new file mode 100644 index 0000000..9d9e30a --- /dev/null +++ b/internal/routers/router.go @@ -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 +} diff --git a/internal/services/auth_service.go b/internal/services/auth_service.go new file mode 100644 index 0000000..dc46223 --- /dev/null +++ b/internal/services/auth_service.go @@ -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 +} diff --git a/internal/services/check_service.go b/internal/services/check_service.go new file mode 100644 index 0000000..9db02d9 --- /dev/null +++ b/internal/services/check_service.go @@ -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) +} diff --git a/internal/services/role_service.go b/internal/services/role_service.go new file mode 100644 index 0000000..bfa61a6 --- /dev/null +++ b/internal/services/role_service.go @@ -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 +} diff --git a/internal/services/warehouse_service.go b/internal/services/warehouse_service.go new file mode 100644 index 0000000..d82d476 --- /dev/null +++ b/internal/services/warehouse_service.go @@ -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 +} diff --git a/pkg/helper/jwt.go b/pkg/helper/jwt.go new file mode 100644 index 0000000..f1ce5d0 --- /dev/null +++ b/pkg/helper/jwt.go @@ -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") +} diff --git a/pkg/helper/password.go b/pkg/helper/password.go new file mode 100644 index 0000000..045ddb0 --- /dev/null +++ b/pkg/helper/password.go @@ -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 +} diff --git a/pkg/helper/validator.go b/pkg/helper/validator.go new file mode 100644 index 0000000..3361763 --- /dev/null +++ b/pkg/helper/validator.go @@ -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 +} diff --git a/pkg/log/logger.go b/pkg/log/logger.go new file mode 100644 index 0000000..2264c93 --- /dev/null +++ b/pkg/log/logger.go @@ -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...) +// } diff --git a/pkg/utils/async_handler.go b/pkg/utils/async_handler.go new file mode 100644 index 0000000..5546a1f --- /dev/null +++ b/pkg/utils/async_handler.go @@ -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() + } + } +} diff --git a/response/error_response.go b/response/error_response.go new file mode 100644 index 0000000..9f0667d --- /dev/null +++ b/response/error_response.go @@ -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) +} diff --git a/response/http_reason_phrases.go b/response/http_reason_phrases.go new file mode 100644 index 0000000..051e3cd --- /dev/null +++ b/response/http_reason_phrases.go @@ -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] +} diff --git a/response/success_response.go b/response/success_response.go new file mode 100644 index 0000000..9ba1eb4 --- /dev/null +++ b/response/success_response.go @@ -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) +} diff --git a/sqlc.yaml b/sqlc.yaml new file mode 100644 index 0000000..c19506b --- /dev/null +++ b/sqlc.yaml @@ -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" diff --git a/sqlc_gen/cabinet.sql.go b/sqlc_gen/cabinet.sql.go new file mode 100644 index 0000000..0d84f53 --- /dev/null +++ b/sqlc_gen/cabinet.sql.go @@ -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 +} diff --git a/sqlc_gen/db.go b/sqlc_gen/db.go new file mode 100644 index 0000000..9d485b5 --- /dev/null +++ b/sqlc_gen/db.go @@ -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, + } +} diff --git a/sqlc_gen/models.go b/sqlc_gen/models.go new file mode 100644 index 0000000..efd69c6 --- /dev/null +++ b/sqlc_gen/models.go @@ -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"` +} diff --git a/sqlc_gen/querier.go b/sqlc_gen/querier.go new file mode 100644 index 0000000..5651fc6 --- /dev/null +++ b/sqlc_gen/querier.go @@ -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) diff --git a/sqlc_gen/roles.sql.go b/sqlc_gen/roles.sql.go new file mode 100644 index 0000000..abef067 --- /dev/null +++ b/sqlc_gen/roles.sql.go @@ -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 +} diff --git a/sqlc_gen/room.sql.go b/sqlc_gen/room.sql.go new file mode 100644 index 0000000..68beb62 --- /dev/null +++ b/sqlc_gen/room.sql.go @@ -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 +} diff --git a/sqlc_gen/user_roles.sql.go b/sqlc_gen/user_roles.sql.go new file mode 100644 index 0000000..9454c9d --- /dev/null +++ b/sqlc_gen/user_roles.sql.go @@ -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 +} diff --git a/sqlc_gen/users.sql.go b/sqlc_gen/users.sql.go new file mode 100644 index 0000000..af85f4a --- /dev/null +++ b/sqlc_gen/users.sql.go @@ -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 +} diff --git a/sqlc_gen/warehouse.sql.go b/sqlc_gen/warehouse.sql.go new file mode 100644 index 0000000..0b5f937 --- /dev/null +++ b/sqlc_gen/warehouse.sql.go @@ -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 +}