diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000000000000000000000000000000000000..3df29302e637ba1e6c8637dcfd89ddca881c77a5
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,13 @@
+DB_HOST =
+DB_USER =
+DB_PASSWORD =
+DB_PORT =
+DB_NAME =
+HOST_ADDRESS =
+HOST_PORT =
+SALT =
+LOG_PATH = logs
+JWT_SECRET_KEY = s4b3s076
+SUPABASE_URL=
+SUPABASE_SERVICE_KEY=
+SUPABASE_BUCKET_NAME=
\ No newline at end of file
diff --git a/README.md b/README.md
index 6675a77207f76e875cf638e2404489a36bbdcee9..3f4275ebec0e8c6273605f15b21c07d61cb0ada4 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,8 @@
----
-title: Quzuu Api Dev
-emoji: 🐠
-colorFrom: indigo
-colorTo: gray
-sdk: docker
-pinned: false
----
+---
+title: Quzuu Api Dev
+emoji: 🐠
+colorFrom: indigo
+colorTo: gray
+sdk: docker
+pinned: false
+---
diff --git a/api_spec.txt b/api_spec.txt
index b185a289d6e600d405242bca01d67d47afbe9e71..5cbfb3e5e90f268294d204c43f9847f2dc378c95 100644
--- a/api_spec.txt
+++ b/api_spec.txt
@@ -1,118 +1,118 @@
-
-attempt:{
- id_event:
- id_exam:
- remaining_time:
- answered_question:[]str
- question
-}
-
-question:{
- id:uuid,
- question_type:string,
- content:string,
- options:[]string,
- current_answer:[]string,
-}
-
-Answer Soal -> (1) Request ke Backend untuk attempt
- (2) Rubah UI options
- (3) Fetch Status -> show Status
- (4) Set State question.current_answer update
- (5) Update navigation button soalnya sudah keisi = "hijau", sekarang di nomor berapa = "ungu"
-
-# Isian Singkat, Handle onChange =>
-[kjkajsksaf]
-
-Before:
-def sum(a,b):
- return {code}
-
-print({code})
-
-After :
-def sum(a,b):
- return
-
-print()
-
-# Puzzle:
-
-
-#Payload ngirim jawaban (by default) -> Endpoint Ngirim Answer
-
-answer: {
- id_question:uuid,
- answers:[]str
-}
-
-#mc
-answer: {
- id_question:uuid,
- answers:["A"]str
-}
-
-
-#Complexmc T/F
-answer: {
- id_question:uuid,
- answers:[1,0,0,1]
-}
-+++
-
-
-
-
-++++++++++++
-#code_Puzzle
-
-answer: {
- id_question:uuid,
- answers:["a+b","sum(a,b)"]
-}
-
-# short_answer
-answer: {
- id_question:uuid,
- answers:["jawaban"]
-}
-
-# upload_file
-upload_file -> Form Data
-answer: {
- id_question:uuid,
- answers:["{nama_file}"]
-}
-
-#competitive Programming
-question_type:mock_coding[language]
-upload_file -> Form Data (Generate nama_file)
-answer:{
- id_questions:uuid,
- answers:["{nama_file}"]
-}
-
-#response answer
-response:{
- success_response,
- meta_data{
- verdict: [AC/WA/TLE/RTE]
- score:
- time_exec:
- memory:
- }
-}
-
-#Submit
-{
- id_attempt:
-}
-
-Attempt{
- id_user:
- id_event:
- id_exam:
-}
-
-
+
+attempt:{
+ id_event:
+ id_exam:
+ remaining_time:
+ answered_question:[]str
+ question
+}
+
+question:{
+ id:uuid,
+ question_type:string,
+ content:string,
+ options:[]string,
+ current_answer:[]string,
+}
+
+Answer Soal -> (1) Request ke Backend untuk attempt
+ (2) Rubah UI options
+ (3) Fetch Status -> show Status
+ (4) Set State question.current_answer update
+ (5) Update navigation button soalnya sudah keisi = "hijau", sekarang di nomor berapa = "ungu"
+
+# Isian Singkat, Handle onChange =>
+[kjkajsksaf]
+
+Before:
+def sum(a,b):
+ return {code}
+
+print({code})
+
+After :
+def sum(a,b):
+ return
+
+print()
+
+# Puzzle:
+
+
+#Payload ngirim jawaban (by default) -> Endpoint Ngirim Answer
+
+answer: {
+ id_question:uuid,
+ answers:[]str
+}
+
+#mc
+answer: {
+ id_question:uuid,
+ answers:["A"]str
+}
+
+
+#Complexmc T/F
+answer: {
+ id_question:uuid,
+ answers:[1,0,0,1]
+}
++++
+
+
+
+
+++++++++++++
+#code_Puzzle
+
+answer: {
+ id_question:uuid,
+ answers:["a+b","sum(a,b)"]
+}
+
+# short_answer
+answer: {
+ id_question:uuid,
+ answers:["jawaban"]
+}
+
+# upload_file
+upload_file -> Form Data
+answer: {
+ id_question:uuid,
+ answers:["{nama_file}"]
+}
+
+#competitive Programming
+question_type:mock_coding[language]
+upload_file -> Form Data (Generate nama_file)
+answer:{
+ id_questions:uuid,
+ answers:["{nama_file}"]
+}
+
+#response answer
+response:{
+ success_response,
+ meta_data{
+ verdict: [AC/WA/TLE/RTE]
+ score:
+ time_exec:
+ memory:
+ }
+}
+
+#Submit
+{
+ id_attempt:
+}
+
+Attempt{
+ id_user:
+ id_event:
+ id_exam:
+}
+
+
# Result / Scoreboard
\ No newline at end of file
diff --git a/config/database_config.go b/config/database_config.go
index 378f9b46b65d7858ee5eb0f46ed671daf1c2b87d..39e5057ece266206bde620a8b4a8bd303220e084 100644
--- a/config/database_config.go
+++ b/config/database_config.go
@@ -1,51 +1,56 @@
package config
import (
- "fmt"
- "log"
- "gorm.io/driver/postgres"
- "gorm.io/gorm"
+ "fmt"
+ "log"
+ "gorm.io/driver/postgres"
+ "gorm.io/gorm"
)
type DatabaseConfig interface {
- AutoMigrateAll(entities ...interface{}) error
- GetInstance() *gorm.DB
+ AutoMigrateAll(entities ...interface{}) error
+ GetInstance() *gorm.DB
}
+
type databaseConfig struct {
- db *gorm.DB
+ db *gorm.DB
}
func NewDatabaseConfig(DB_HOST, DB_USER, DB_PASSWORD, DB_NAME, DB_PORT string) DatabaseConfig {
- dsn := fmt.Sprintf(
- "host=%s user=%s password=%s dbname=%s port=%s sslmode=disable TimeZone=Asia/Jakarta ",
- DB_HOST, DB_USER, DB_PASSWORD, DB_NAME, DB_PORT,
- )
+ dsn := fmt.Sprintf(
+ "host=%s user=%s password=%s dbname=%s port=%s sslmode=disable TimeZone=Asia/Jakarta",
+ DB_HOST, DB_USER, DB_PASSWORD, DB_NAME, DB_PORT,
+ )
- db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
- TranslateError: true,
- })
+ db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
+ TranslateError: true,
+ })
- db = db.Session(&gorm.Session{
- PrepareStmt: false,
- })
+ if err != nil {
+ log.Fatal("Failed to connect to database:", err)
+ }
- if err != nil {
- log.Fatal("Failed to connect to database:", err)
- }
+ db = db.Session(&gorm.Session{
+ PrepareStmt: false,
+ })
- return &databaseConfig{db: db}
+ return &databaseConfig{db: db}
}
func (cfg *databaseConfig) AutoMigrateAll(entities ...interface{}) error {
- // Enable UUID extension first
if err := cfg.db.Exec("CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\"").Error; err != nil {
- return err
+ log.Printf("Warning: Could not create uuid-ossp extension: %v", err)
}
-
+
+ // Run migrations
err := cfg.db.AutoMigrate(entities...)
- return err
+ if err != nil {
+ return fmt.Errorf("migration failed: %w", err)
+ }
+
+ return nil
}
func (cfg *databaseConfig) GetInstance() *gorm.DB {
- return cfg.db
-}
+ return cfg.db
+}
\ No newline at end of file
diff --git a/config/env_config.go b/config/env_config.go
index 823de87cac1d5e06138ef46334b4ff0d4e1da41a..667425864cdc8b773111bc06bdf67e50b27a7434 100644
--- a/config/env_config.go
+++ b/config/env_config.go
@@ -18,6 +18,9 @@ type EnvConfig interface {
GetDatabasePassword() string
GetDatabaseName() string
GetSalt() string
+ GetSupabaseURL() string
+ GetSupabaseKey() string
+ GetSupabaseBucket() string
}
type envConfig struct {
@@ -79,7 +82,19 @@ func (e *envConfig) GetDatabaseName() string {
func (e *envConfig) GetSalt() string {
salt := os.Getenv("SALT")
if salt == "" {
- return "Def4u|7" // Default salt value
+ return "Def4u|7"
}
return salt
}
+
+func (e *envConfig) GetSupabaseURL() string {
+ return os.Getenv("SUPABASE_URL")
+}
+
+func (e *envConfig) GetSupabaseKey() string {
+ return os.Getenv("SUPABASE_SERVICE_KEY")
+}
+
+func (e *envConfig) GetSupabaseBucket() string {
+ return os.Getenv("SUPABASE_BUCKET_NAME")
+}
\ No newline at end of file
diff --git a/config/jwt_config.go b/config/jwt_config.go
index d9555ffcba8cc622a18ff4a32b90aff3d3402194..881ab584c60d7f4c06df1699d060005f76b8b544 100644
--- a/config/jwt_config.go
+++ b/config/jwt_config.go
@@ -1,24 +1,24 @@
-package config
-
-type JWTConfig interface {
- SetSecretKey(key string)
- GetSecretKey() string
-}
-
-type jwtConfig struct {
- secretKey string
-}
-
-func NewJWTConfig(secretKey string) JWTConfig {
- return &jwtConfig{
- secretKey: secretKey,
- }
-}
-
-func (cfg *jwtConfig) SetSecretKey(key string) {
- cfg.secretKey = key
-}
-
-func (cfg *jwtConfig) GetSecretKey() string {
- return cfg.secretKey
-}
+package config
+
+type JWTConfig interface {
+ SetSecretKey(key string)
+ GetSecretKey() string
+}
+
+type jwtConfig struct {
+ secretKey string
+}
+
+func NewJWTConfig(secretKey string) JWTConfig {
+ return &jwtConfig{
+ secretKey: secretKey,
+ }
+}
+
+func (cfg *jwtConfig) SetSecretKey(key string) {
+ cfg.secretKey = key
+}
+
+func (cfg *jwtConfig) GetSecretKey() string {
+ return cfg.secretKey
+}
diff --git a/config/supabase.go b/config/supabase.go
new file mode 100644
index 0000000000000000000000000000000000000000..1a5f1bdf5c95290a016abb10c3058b3b4574567a
--- /dev/null
+++ b/config/supabase.go
@@ -0,0 +1,15 @@
+package config
+
+type SupabaseConfig struct {
+ URL string
+ ServiceKey string
+ BucketName string
+}
+
+func NewSupabaseConfig(url, key, bucket string) SupabaseConfig {
+ return SupabaseConfig{
+ URL: url,
+ ServiceKey: key,
+ BucketName: bucket,
+ }
+}
\ No newline at end of file
diff --git a/controllers/academy_controller.go b/controllers/academy_controller.go
index 48267bea7801b30be92f518c060d99b0edf67ff8..bf818d1119910e961268cf509843280dc4d76e78 100644
--- a/controllers/academy_controller.go
+++ b/controllers/academy_controller.go
@@ -1,145 +1,228 @@
-package controllers
-
-import (
- "fmt"
- "net/http"
- "strconv"
-
- "abdanhafidz.com/go-boilerplate/models/dto"
- "abdanhafidz.com/go-boilerplate/services"
- "github.com/gin-gonic/gin"
- "github.com/google/uuid"
-)
-
-type AcademyController interface {
- CreateAcademy(ctx *gin.Context)
- GetAcademy(ctx *gin.Context)
- GetAcademyDetail(ctx *gin.Context)
- ListAcademies(ctx *gin.Context)
- UpdateAcademy(ctx *gin.Context)
- DeleteAcademy(ctx *gin.Context)
-
- GetMaterial(ctx *gin.Context)
- CreateMaterial(ctx *gin.Context)
-
- CreateContent(ctx *gin.Context)
- GetContent(ctx *gin.Context)
-
- UpdateContentProgress(ctx *gin.Context)
-}
-
-type academyController struct {
- academyService services.AcademyService
-}
-
-func NewAcademyController(academyService services.AcademyService) AcademyController {
- return &academyController{academyService}
-}
-
-func (c *academyController) GetAcademy(ctx *gin.Context) {
- academySlug := ctx.Param("academy_slug")
- accountId,_ := uuid.Parse(ctx.Value("account_id").(string))
- res, err := c.academyService.GetAcademy(ctx.Request.Context(), accountId, academySlug)
- ResponseJSON(ctx, gin.H{"academy_slug": academySlug}, res, err)
-}
-
-func (c *academyController) GetAcademyDetail(ctx *gin.Context) {
- id, _ := uuid.Parse(ctx.Param("id"))
- res, err := c.academyService.GetAcademyDetail(ctx.Request.Context(), id)
- ResponseJSON(ctx, gin.H{"id": id}, res, err)
-}
-
-func (c *academyController) ListAcademies(ctx *gin.Context) {
- accountId,_ := uuid.Parse(ctx.Value("account_id").(string))
- fmt.Println("Account ID in ListAcademies:", accountId)
- res, err := c.academyService.ListAcademies(ctx.Request.Context(), accountId)
- ResponseJSON(ctx, gin.H{}, res, err)
-}
-
-func (c *academyController) CreateAcademy(ctx *gin.Context) {
- req := RequestJSON[dto.CreateAcademyRequest](ctx)
- res, err := c.academyService.CreateAcademy(ctx.Request.Context(), req)
- ResponseJSON(ctx, req, res, err)
-}
-
-func (c *academyController) UpdateAcademy(ctx *gin.Context) {
- id, _ := uuid.Parse(ctx.Param("id"))
- req := RequestJSON[dto.UpdateAcademyRequest](ctx)
- res, err := c.academyService.UpdateAcademy(ctx.Request.Context(), id, req)
- ResponseJSON(ctx, req, res, err)
-}
-
-func (c *academyController) DeleteAcademy(ctx *gin.Context) {
- id, _ := uuid.Parse(ctx.Param("id"))
- err := c.academyService.DeleteAcademy(ctx.Request.Context(), id)
-
- ResponseJSON(ctx, gin.H{"id": id}, gin.H{"deleted": true}, err)
-}
-
-// MATERIAL
-func (c *academyController) GetMaterial(ctx *gin.Context) {
- academySlug := ctx.Param("academy_slug")
- materialSlug := ctx.Param("material_slug")
- accountId,_ := uuid.Parse(ctx.Value("account_id").(string))
-
- res, err := c.academyService.GetMaterial(ctx.Request.Context(), accountId, academySlug, materialSlug)
- ResponseJSON(ctx, gin.H{"academy_slug": academySlug, "material_slug": materialSlug}, res, err)
-}
-
-func (c *academyController) CreateMaterial(ctx *gin.Context) {
- req := RequestJSON[dto.CreateMaterialRequest](ctx)
-
- res, err := c.academyService.CreateMaterial(ctx.Request.Context(), req)
- ResponseJSON(ctx, req, res, err)
-}
-
-
-// CONTENT
-func (c *academyController) GetContent(ctx *gin.Context) {
- accountId,_ := uuid.Parse(ctx.Value("account_id").(string))
- academySlug := ctx.Param("academy_slug")
- materialSlug := ctx.Param("material_slug")
- orderString := ctx.Param("order")
-
- orderID64, err := strconv.ParseUint(orderString, 10, 64)
- if err != nil {
- ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid 'order' parameter. Must be a positive integer."})
- return
- }
- order := uint(orderID64)
-
- res, err := c.academyService.GetContent(ctx.Request.Context(),accountId, academySlug, materialSlug, order)
- ResponseJSON(ctx, gin.H{"academy_slug": academySlug, "material_slug": materialSlug, "content_slug": order}, res, err)
-}
-
-func (c *academyController) CreateContent(ctx *gin.Context) {
- req := RequestJSON[dto.CreateContentRequest](ctx)
- res, err := c.academyService.CreateContent(ctx.Request.Context(), req)
- ResponseJSON(ctx, req, res, err)
-}
-
-
-func (c *academyController) UpdateContentProgress(ctx *gin.Context) {
- accountId,_ := uuid.Parse(ctx.Value("account_id").(string))
- academySlug := ctx.Param("academy_slug")
- materialSlug := ctx.Param("material_slug")
- orderString := ctx.Param("order")
-
- orderID64, err := strconv.ParseUint(orderString, 10, 64)
- if err != nil {
- ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid 'order' parameter. Must be a positive integer."})
- return
- }
- order := uint(orderID64)
-
- contentProgress, materialProgress, academyProgress, err := c.academyService.UpdateContentProgress(ctx, accountId, academySlug, materialSlug, order)
- res := gin.H{
- "content_progress": contentProgress,
- "material_progress": materialProgress,
- "academy_progress": academyProgress,
- }
-
- ResponseJSON(ctx, gin.H{"academy_slug": academySlug, "material_slug": materialSlug, "content_slug": order}, res, err)
-}
-
-//! TODO: MAKE FULL CRUD FOR ADMIN USER
+package controllers
+
+import (
+ "fmt"
+ "net/http"
+ "strconv"
+
+ "abdanhafidz.com/go-boilerplate/models/dto"
+ http_error "abdanhafidz.com/go-boilerplate/models/error"
+ "abdanhafidz.com/go-boilerplate/services"
+ "github.com/gin-gonic/gin"
+ "github.com/google/uuid"
+)
+
+type AcademyController interface {
+ // Academy
+ CreateAcademy(ctx *gin.Context)
+ GetAcademy(ctx *gin.Context)
+ GetAcademyDetail(ctx *gin.Context)
+ ListAcademies(ctx *gin.Context)
+ UpdateAcademy(ctx *gin.Context)
+ DeleteAcademy(ctx *gin.Context)
+
+ // Material
+ GetMaterial(ctx *gin.Context)
+ CreateMaterial(ctx *gin.Context)
+ DeleteMaterial(ctx *gin.Context)
+
+ // Content
+ CreateContent(ctx *gin.Context)
+ GetContent(ctx *gin.Context)
+ DeleteContent(ctx *gin.Context)
+
+ // Progress
+ UpdateContentProgress(ctx *gin.Context)
+}
+
+type academyController struct {
+ academyService services.AcademyService
+}
+
+func NewAcademyController(academyService services.AcademyService) AcademyController {
+ return &academyController{academyService}
+}
+
+// ================= ACADEMY =================
+
+func (c *academyController) GetAcademy(ctx *gin.Context) {
+ academySlug := ctx.Param("academy_slug")
+ accountIdStr := ctx.GetString("account_id")
+ accountId, err := uuid.Parse(accountIdStr)
+ if err != nil {
+ // [FIX] Tambahkan [any, any] agar compiler tau tipenya
+ ResponseJSON[any, any](ctx, nil, nil, http_error.UNAUTHORIZED)
+ return
+ }
+
+ res, err := c.academyService.GetAcademy(ctx.Request.Context(), accountId, academySlug)
+ ResponseJSON(ctx, gin.H{"academy_slug": academySlug}, res, err)
+}
+
+func (c *academyController) GetAcademyDetail(ctx *gin.Context) {
+ id, err := uuid.Parse(ctx.Param("id"))
+ if err != nil {
+ // [FIX] Tambahkan [any, any]
+ ResponseJSON[any, any](ctx, nil, nil, http_error.BAD_REQUEST_ERROR)
+ return
+ }
+
+ res, err := c.academyService.GetAcademyDetail(ctx.Request.Context(), id)
+ ResponseJSON(ctx, gin.H{"id": id}, res, err)
+}
+
+func (c *academyController) ListAcademies(ctx *gin.Context) {
+ accountIdStr := ctx.GetString("account_id")
+ accountId, err := uuid.Parse(accountIdStr)
+ if err != nil {
+ // [FIX] Tambahkan [any, any]
+ ResponseJSON[any, any](ctx, nil, nil, http_error.UNAUTHORIZED)
+ return
+ }
+
+ fmt.Println("Account ID in ListAcademies:", accountId)
+ res, err := c.academyService.ListAcademies(ctx.Request.Context(), accountId)
+ ResponseJSON(ctx, gin.H{}, res, err)
+}
+
+func (c *academyController) CreateAcademy(ctx *gin.Context) {
+ req := RequestJSON[dto.CreateAcademyRequest](ctx)
+ res, err := c.academyService.CreateAcademy(ctx.Request.Context(), req)
+ ResponseJSON(ctx, req, res, err)
+}
+
+func (c *academyController) UpdateAcademy(ctx *gin.Context) {
+ id, err := uuid.Parse(ctx.Param("id"))
+ if err != nil {
+ // [FIX] Tambahkan [any, any]
+ ResponseJSON[any, any](ctx, nil, nil, http_error.BAD_REQUEST_ERROR)
+ return
+ }
+
+ req := RequestJSON[dto.UpdateAcademyRequest](ctx)
+ res, err := c.academyService.UpdateAcademy(ctx.Request.Context(), id, req)
+ ResponseJSON(ctx, req, res, err)
+}
+
+func (c *academyController) DeleteAcademy(ctx *gin.Context) {
+ id, err := uuid.Parse(ctx.Param("id"))
+ if err != nil {
+ // [FIX] Tambahkan [any, any]
+ ResponseJSON[any, any](ctx, nil, nil, http_error.BAD_REQUEST_ERROR)
+ return
+ }
+
+ err = c.academyService.DeleteAcademy(ctx.Request.Context(), id)
+ ResponseJSON(ctx, gin.H{"id": id}, gin.H{"deleted": true}, err)
+}
+
+// ================= MATERIAL =================
+
+func (c *academyController) GetMaterial(ctx *gin.Context) {
+ academySlug := ctx.Param("academy_slug")
+ materialSlug := ctx.Param("material_slug")
+
+ accountIdStr := ctx.GetString("account_id")
+ accountId, err := uuid.Parse(accountIdStr)
+ if err != nil {
+ // [FIX] Tambahkan [any, any]
+ ResponseJSON[any, any](ctx, nil, nil, http_error.UNAUTHORIZED)
+ return
+ }
+
+ res, err := c.academyService.GetMaterial(ctx.Request.Context(), accountId, academySlug, materialSlug)
+ ResponseJSON(ctx, gin.H{"academy_slug": academySlug, "material_slug": materialSlug}, res, err)
+}
+
+func (c *academyController) CreateMaterial(ctx *gin.Context) {
+ req := RequestJSON[dto.CreateMaterialRequest](ctx)
+ res, err := c.academyService.CreateMaterial(ctx.Request.Context(), req)
+ ResponseJSON(ctx, req, res, err)
+}
+
+func (c *academyController) DeleteMaterial(ctx *gin.Context) {
+ id, err := uuid.Parse(ctx.Param("id"))
+ if err != nil {
+ // [FIX] Tambahkan [any, any]
+ ResponseJSON[any, any](ctx, nil, nil, http_error.BAD_REQUEST_ERROR)
+ return
+ }
+
+ err = c.academyService.DeleteMaterial(ctx.Request.Context(), id)
+ ResponseJSON(ctx, gin.H{"id": id}, gin.H{"deleted": true}, err)
+}
+
+// ================= CONTENT =================
+
+func (c *academyController) GetContent(ctx *gin.Context) {
+ accountIdStr := ctx.GetString("account_id")
+ accountId, err := uuid.Parse(accountIdStr)
+ if err != nil {
+ // [FIX] Tambahkan [any, any]
+ ResponseJSON[any, any](ctx, nil, nil, http_error.UNAUTHORIZED)
+ return
+ }
+
+ academySlug := ctx.Param("academy_slug")
+ materialSlug := ctx.Param("material_slug")
+
+ orderID64, err := strconv.ParseUint(ctx.Param("order"), 10, 64)
+ if err != nil {
+ ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid 'order' parameter. Must be a positive integer."})
+ return
+ }
+ order := uint(orderID64)
+
+ res, err := c.academyService.GetContent(ctx.Request.Context(), accountId, academySlug, materialSlug, order)
+ ResponseJSON(ctx, gin.H{"academy_slug": academySlug, "material_slug": materialSlug, "content_order": order}, res, err)
+}
+
+func (c *academyController) CreateContent(ctx *gin.Context) {
+ req := RequestJSON[dto.CreateContentRequest](ctx)
+ res, err := c.academyService.CreateContent(ctx.Request.Context(), req)
+ ResponseJSON(ctx, req, res, err)
+}
+
+func (c *academyController) DeleteContent(ctx *gin.Context) {
+ id, err := uuid.Parse(ctx.Param("id"))
+ if err != nil {
+ // [FIX] Tambahkan [any, any]
+ ResponseJSON[any, any](ctx, nil, nil, http_error.BAD_REQUEST_ERROR)
+ return
+ }
+
+ err = c.academyService.DeleteContent(ctx.Request.Context(), id)
+ ResponseJSON(ctx, gin.H{"id": id}, gin.H{"deleted": true}, err)
+}
+
+// ================= PROGRESS =================
+
+func (c *academyController) UpdateContentProgress(ctx *gin.Context) {
+ accountIdStr := ctx.GetString("account_id")
+ accountId, err := uuid.Parse(accountIdStr)
+ if err != nil {
+ // [FIX] Tambahkan [any, any]
+ ResponseJSON[any, any](ctx, nil, nil, http_error.UNAUTHORIZED)
+ return
+ }
+
+ academySlug := ctx.Param("academy_slug")
+ materialSlug := ctx.Param("material_slug")
+
+ orderID64, err := strconv.ParseUint(ctx.Param("order"), 10, 64)
+ if err != nil {
+ ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid 'order' parameter. Must be a positive integer."})
+ return
+ }
+ order := uint(orderID64)
+
+ contentProgress, materialProgress, academyProgress, err := c.academyService.UpdateContentProgress(ctx.Request.Context(), accountId, academySlug, materialSlug, order)
+
+ res := gin.H{
+ "content_progress": contentProgress,
+ "material_progress": materialProgress,
+ "academy_progress": academyProgress,
+ }
+
+ ResponseJSON(ctx, gin.H{"academy_slug": academySlug, "material_slug": materialSlug, "content_order": order}, res, err)
+}
\ No newline at end of file
diff --git a/controllers/upload_controller.go b/controllers/upload_controller.go
new file mode 100644
index 0000000000000000000000000000000000000000..84b1552e792241309658dc601039fba41df89996
--- /dev/null
+++ b/controllers/upload_controller.go
@@ -0,0 +1,222 @@
+package controllers
+
+import (
+ "errors"
+ "net/http"
+ "path/filepath"
+ "strings"
+
+ "github.com/gin-gonic/gin"
+ "github.com/google/uuid"
+
+ "abdanhafidz.com/go-boilerplate/models/dto"
+ http_error "abdanhafidz.com/go-boilerplate/models/error"
+ "abdanhafidz.com/go-boilerplate/services"
+)
+
+type UploadController struct {
+ uploadService *services.UploadService
+}
+
+func NewUploadController(s *services.UploadService) *UploadController {
+ return &UploadController{uploadService: s}
+}
+
+func (c *UploadController) Upload(ctx *gin.Context) {
+ // 1. Parse Multipart Form (Limit 32MB)
+ if err := ctx.Request.ParseMultipartForm(32 << 20); err != nil {
+ if strings.Contains(err.Error(), "http: request body too large") {
+ ctx.JSON(http.StatusBadRequest, gin.H{
+ "status": "error",
+ "message": "File size exceeds the allowed limit of 32MB",
+ })
+ return
+ }
+ ctx.JSON(http.StatusBadRequest, gin.H{
+ "status": "error",
+ "code": "INVALID_FORM",
+ "message": "Failed to parse form data",
+ })
+ return
+ }
+
+ form, err := ctx.MultipartForm()
+ if err != nil {
+ ctx.JSON(http.StatusBadRequest, gin.H{
+ "status": "error",
+ "code": "INVALID_DATA",
+ "message": "Invalid form data",
+ })
+ return
+ }
+
+ files := form.File["files"]
+ if len(files) == 0 {
+ ctx.JSON(http.StatusBadRequest, gin.H{
+ "status": "error",
+ "message": "No files uploaded",
+ })
+ return
+ }
+
+ // 2. Determine Upload Context
+ uploadContext := ctx.PostForm("context")
+ if uploadContext == "" {
+ // Auto-infer context based on first file extension if not provided
+ ext := strings.ToLower(filepath.Ext(files[0].Filename))
+ uploadContext = c.inferContextFromExt(ext)
+ }
+
+ // 3. Get Account ID from Context (Middleware)
+ accountIDStr := ctx.GetString("account_id")
+ if accountIDStr == "" {
+ ctx.JSON(http.StatusUnauthorized, gin.H{
+ "status": "error",
+ "message": "Unauthorized: Missing account ID",
+ })
+ return
+ }
+
+ accountID, err := uuid.Parse(accountIDStr)
+ if err != nil {
+ ctx.JSON(http.StatusUnauthorized, gin.H{
+ "status": "error",
+ "message": "Unauthorized: Invalid UUID format",
+ })
+ return
+ }
+
+ // 4. Call Service
+ uploadedFiles, err := c.uploadService.UploadFiles(ctx, files, uploadContext, accountID)
+ if err != nil {
+ // Map Service Errors to HTTP Status
+ if errors.Is(err, http_error.FILE_TOO_LARGE) ||
+ errors.Is(err, http_error.INVALID_FILE_TYPE) ||
+ errors.Is(err, http_error.BAD_REQUEST_ERROR) {
+ ctx.JSON(http.StatusBadRequest, gin.H{
+ "status": "error",
+ "message": err.Error(),
+ })
+ return
+ }
+
+ if errors.Is(err, http_error.PARTIAL_UPLOAD_FAILURE) {
+ ctx.JSON(http.StatusUnprocessableEntity, gin.H{
+ "status": "error",
+ "message": err.Error(),
+ // Opsional: Anda bisa mengembalikan file yang berhasil di sini jika perlu
+ })
+ return
+ }
+
+ ctx.JSON(http.StatusInternalServerError, gin.H{
+ "status": "error",
+ "message": err.Error(),
+ })
+ return
+ }
+
+ // 5. Prepare Response DTO
+ var fileResponses []dto.FileResponse
+ for _, f := range uploadedFiles {
+ fileResponses = append(fileResponses, dto.FileResponse{
+ Id: f.Id,
+ OriginalName: f.OriginalName,
+ URL: f.Path,
+ MimeType: f.MimeType,
+ Size: f.Size,
+ CreatedAt: f.CreatedAt,
+ })
+ }
+
+ ctx.JSON(http.StatusCreated, dto.FileUploadResponse{
+ Status: "success",
+ Message: "Files uploaded successfully",
+ Data: fileResponses,
+ })
+}
+
+func (c *UploadController) GetFileByID(ctx *gin.Context) {
+ // 1. Validate Param ID
+ fileIDStr := ctx.Param("id")
+ fileID, err := uuid.Parse(fileIDStr)
+ if err != nil {
+ ctx.JSON(http.StatusBadRequest, gin.H{
+ "status": "error",
+ "message": "Invalid file ID format",
+ })
+ return
+ }
+
+ // 2. Validate Account ID
+ accountIDStr := ctx.GetString("account_id")
+ if accountIDStr == "" {
+ ctx.JSON(http.StatusUnauthorized, gin.H{
+ "status": "error",
+ "message": "Unauthorized: Missing account ID",
+ })
+ return
+ }
+
+ accountID, err := uuid.Parse(accountIDStr)
+ if err != nil {
+ ctx.JSON(http.StatusUnauthorized, gin.H{
+ "status": "error",
+ "message": "Unauthorized: Invalid UUID format",
+ })
+ return
+ }
+
+ // 3. Call Service
+ fileData, err := c.uploadService.GetFileByID(ctx, fileID, accountID)
+ if err != nil {
+ if errors.Is(err, http_error.NOT_FOUND_ERROR) {
+ ctx.JSON(http.StatusNotFound, gin.H{
+ "status": "error",
+ "message": "File not found or access denied",
+ })
+ return
+ }
+
+ ctx.JSON(http.StatusInternalServerError, gin.H{
+ "status": "error",
+ "message": err.Error(),
+ })
+ return
+ }
+
+ // 4. Response
+ response := dto.FileResponse{
+ Id: fileData.Id,
+ OriginalName: fileData.OriginalName,
+ URL: fileData.Path,
+ MimeType: fileData.MimeType,
+ Size: fileData.Size,
+ CreatedAt: fileData.CreatedAt,
+ }
+
+ ctx.JSON(http.StatusOK, dto.FileResponseSingle{
+ Status: "success",
+ Message: "File retrieved successfully",
+ Data: response,
+ })
+}
+
+// Helper untuk logika inferensi context (clean code)
+func (c *UploadController) inferContextFromExt(ext string) string {
+ isSourceCode := map[string]bool{
+ ".cpp": true, ".c": true, ".py": true, ".java": true,
+ ".go": true, ".js": true, ".txt": true,
+ }
+ isDocument := map[string]bool{
+ ".pdf": true,
+ }
+
+ if isSourceCode[ext] {
+ return "submission"
+ }
+ if isDocument[ext] {
+ return "material"
+ }
+ return "general"
+}
\ No newline at end of file
diff --git a/do_inject_config.ps1 b/do_inject_config.ps1
index 521b1192e1ede56c460022711ed977a356c17ae3..257f1f4d9dd7cb78e2f7666dc4b6f99d6e4da167 100644
--- a/do_inject_config.ps1
+++ b/do_inject_config.ps1
@@ -1,435 +1,435 @@
-#Requires -Version 5.1
-<#
-.SYNOPSIS
- Automatic Dependency Injection Generator for Go Configuration
-
-.DESCRIPTION
- Scans ./config/ directory, discovers all config constructors, infers their dependencies,
- and generates provider/config_provider.go with full DI wiring. Special handling for
- chained dependencies where configs depend on other configs (e.g., DatabaseConfig depends on EnvConfig).
-
-.EXAMPLE
- .\config_injector.ps1
-
-.NOTES
- - Works with PowerShell 5.1+ and PowerShell 7+
- - No external dependencies required
- - Supports multi-line constructor signatures
- - Handles config-to-config dependencies with topological sorting
- - Special handling for method calls like envConfig.GetDatabaseHost()
-#>
-
-[CmdletBinding()]
-param()
-
-# Configuration
-$ConfigDir = "./config"
-$OutputFile = "provider/config_provider.go"
-$ModulePath = "abdanhafidz.com/go-boilerplate/config"
-
-# ANSI colors for better output
-$script:UseColors = $Host.UI.SupportsVirtualTerminal
-function Write-ColorOutput {
- param([string]$Message, [string]$Color = "White")
- if ($script:UseColors) {
- $colors = @{
- "Green" = "`e[32m"; "Yellow" = "`e[33m"; "Red" = "`e[31m"
- "Cyan" = "`e[36m"; "Blue" = "`e[34m"; "Reset" = "`e[0m"
- }
- Write-Host "$($colors[$Color])$Message$($colors['Reset'])"
- } else {
- Write-Host $Message
- }
-}
-
-# Data structures
-class ConfigInfo {
- [string]$ConstructorName # NewDatabaseConfig
- [string]$Domain # DatabaseConfig
- [string]$VarName # databaseConfig
- [System.Collections.Generic.List[Parameter]]$Parameters
- [System.Collections.Generic.List[string]]$ConfigDependencies
-
- ConfigInfo() {
- $this.Parameters = [System.Collections.Generic.List[Parameter]]::new()
- $this.ConfigDependencies = [System.Collections.Generic.List[string]]::new()
- }
-}
-
-class Parameter {
- [string]$Name
- [string]$RawType
- [string]$NormalizedType
-}
-
-function Get-LowerCamelCase {
- param([string]$Text)
- if ($Text.Length -eq 0) { return $Text }
- return $Text.Substring(0, 1).ToLower() + $Text.Substring(1)
-}
-
-function Normalize-TypeName {
- param([string]$TypeStr)
-
- # Remove leading pointer
- $cleaned = $TypeStr -replace '^\*+', ''
-
- # Remove package prefix (everything before last dot)
- if ($cleaned -match '\.([^.]+)$') {
- $cleaned = $matches[1]
- }
-
- return $cleaned.Trim()
-}
-
-function Parse-GoFiles {
- param([string]$Directory)
-
- Write-ColorOutput "Scanning for config constructors in $Directory..." "Cyan"
-
- if (-not (Test-Path $Directory)) {
- Write-ColorOutput "ERROR: Directory '$Directory' not found!" "Red"
- exit 1
- }
-
- $goFiles = Get-ChildItem -Path $Directory -Filter "*.go" -Recurse -File
- $configs = [System.Collections.Generic.List[ConfigInfo]]::new()
-
- foreach ($file in $goFiles) {
- $content = Get-Content $file.FullName -Raw
-
- # Match function signatures (support multi-line)
- # Pattern: func NewXxxConfig(...) XxxConfig
- $pattern = '(?ms)func\s+(New[a-zA-Z0-9]+Config)\s*\(([^)]*)\)\s+([a-zA-Z0-9*_.]+Config)'
- $matches = [regex]::Matches($content, $pattern)
-
- foreach ($match in $matches) {
- $constructorName = $match.Groups[1].Value
- $paramsStr = $match.Groups[2].Value
- $returnType = $match.Groups[3].Value
-
- # Extract domain name (XxxConfig)
- $domain = Normalize-TypeName $returnType
- $varName = Get-LowerCamelCase $domain
-
- $config = [ConfigInfo]::new()
- $config.ConstructorName = $constructorName
- $config.Domain = $domain
- $config.VarName = $varName
-
- # Parse parameters
- if ($paramsStr.Trim() -ne "") {
- # Split by comma, but be careful with nested types
- $paramList = $paramsStr -split ',\s*(?![^<>]*>)'
-
- foreach ($param in $paramList) {
- $param = $param.Trim()
- if ($param -eq "") { continue }
-
- # Split into name and type
- $parts = $param -split '\s+', 2
-
- $p = [Parameter]::new()
- if ($parts.Count -eq 2) {
- $p.Name = $parts[0]
- $p.RawType = $parts[1]
- } elseif ($parts.Count -eq 1) {
- # Anonymous parameter - synthesize name
- $p.Name = "param$($config.Parameters.Count)"
- $p.RawType = $parts[0]
- } else {
- continue
- }
-
- $p.NormalizedType = Normalize-TypeName $p.RawType
- $config.Parameters.Add($p)
-
- # Track config dependencies (configs that depend on other configs)
- if ($p.NormalizedType -match 'Config$') {
- $config.ConfigDependencies.Add($p.NormalizedType)
- }
- }
- }
-
- $configs.Add($config)
- Write-ColorOutput " Found: $constructorName" "Green"
- }
- }
-
- if ($configs.Count -eq 0) {
- Write-ColorOutput "No config constructors found matching pattern 'NewXxxConfig'!" "Red"
- exit 1
- }
-
- Write-ColorOutput "`nTotal configs discovered: $($configs.Count)" "Blue"
- return $configs
-}
-
-function Get-TopologicalOrder {
- param([System.Collections.Generic.List[ConfigInfo]]$Configs)
-
- Write-ColorOutput "`nBuilding dependency graph..." "Cyan"
-
- # Build adjacency list
- $graph = @{}
- $inDegree = @{}
- $domainToConfig = @{}
-
- foreach ($cfg in $Configs) {
- $graph[$cfg.Domain] = [System.Collections.Generic.List[string]]::new()
- $inDegree[$cfg.Domain] = 0
- $domainToConfig[$cfg.Domain] = $cfg
- }
-
- # Build edges (dependencies)
- foreach ($cfg in $Configs) {
- foreach ($dep in $cfg.ConfigDependencies) {
- if ($graph.ContainsKey($dep)) {
- $graph[$dep].Add($cfg.Domain)
- $inDegree[$cfg.Domain]++
- }
- }
- }
-
- # Kahn's algorithm for topological sort
- $queue = [System.Collections.Generic.Queue[string]]::new()
- foreach ($domain in $inDegree.Keys) {
- if ($inDegree[$domain] -eq 0) {
- $queue.Enqueue($domain)
- }
- }
-
- $sorted = [System.Collections.Generic.List[string]]::new()
-
- while ($queue.Count -gt 0) {
- $current = $queue.Dequeue()
- $sorted.Add($current)
-
- foreach ($neighbor in $graph[$current]) {
- $inDegree[$neighbor]--
- if ($inDegree[$neighbor] -eq 0) {
- $queue.Enqueue($neighbor)
- }
- }
- }
-
- # Check for cycles
- if ($sorted.Count -ne $Configs.Count) {
- $remaining = $inDegree.Keys | Where-Object { $inDegree[$_] -gt 0 }
- Write-ColorOutput "`nERROR: Circular dependency detected in configs!" "Red"
- Write-ColorOutput "Configs involved in cycle: $($remaining -join ', ')" "Yellow"
- exit 1
- }
-
- Write-ColorOutput " Dependency graph validated (no cycles)" "Green"
- Write-ColorOutput " Topological order: $($sorted -join ' -> ')" "Blue"
-
- # Return configs in topological order
- return $sorted | ForEach-Object { $domainToConfig[$_] }
-}
-
-function Resolve-ConfigArgument {
- param(
- [Parameter]$Param,
- [string]$DependentConfigVar
- )
-
- $type = $Param.NormalizedType
- $paramName = $Param.Name
-
- # SPECIAL CASE MAPPINGS FOR CONFIG
- # ============================================
-
- # 1. Config pattern: XxxConfig -> already instantiated config variable
- if ($type -match '^(.+)Config$') {
- $configVarName = Get-LowerCamelCase $type
- return $configVarName
- }
-
- # 2. String parameters - try to infer from parameter name and match with config getter methods
- if ($type -eq "string") {
- # Common patterns for EnvConfig getters
- $getterMappings = @{
- "host" = "GetDatabaseHost()"
- "databaseHost" = "GetDatabaseHost()"
- "user" = "GetDatabaseUser()"
- "databaseUser" = "GetDatabaseUser()"
- "password" = "GetDatabasePassword()"
- "databasePassword" = "GetDatabasePassword()"
- "name" = "GetDatabaseName()"
- "databaseName" = "GetDatabaseName()"
- "dbName" = "GetDatabaseName()"
- "port" = "GetDatabasePort()"
- "databasePort" = "GetDatabasePort()"
- "salt" = "GetSalt()"
- "secret" = "GetSecretKey()"
- "secretKey" = "GetSecretKey()"
- "jwtSecret" = "GetSecretKey()"
- "apiKey" = "GetAPIKey()"
- "timezone" = "GetTimezone()"
- }
-
- # Try to find matching getter
- foreach ($key in $getterMappings.Keys) {
- if ($paramName -like "*$key*") {
- return "envConfig.$($getterMappings[$key])"
- }
- }
-
- # If dependent on a config, try to construct getter name from param name
- if ($DependentConfigVar) {
- # Convert paramName to PascalCase for getter
- $getterName = (Get-Culture).TextInfo.ToTitleCase($paramName)
- $getterName = $getterName -replace '\s', ''
- return "${DependentConfigVar}.Get${getterName}()"
- }
- }
-
- # 3. Int/port parameters
- if ($type -eq "int" -or $type -eq "int32" -or $type -eq "int64") {
- if ($paramName -match "port") {
- return "envConfig.GetDatabasePort()"
- }
- }
-
- # ADD MORE SPECIAL CASES HERE:
- # --------------------------------------------
- # Example: Redis config
- # if ($paramName -match "redis") {
- # return "envConfig.GetRedisURL()"
- # }
- #
- # Example: Mail config
- # if ($paramName -match "smtp") {
- # return "envConfig.GetSMTPHost()"
- # }
- # --------------------------------------------
-
- # 4. Hardcoded constants (timezone example)
- if ($type -eq "string" -and $paramName -match "timezone|location") {
- return "`"Asia/Jakarta`""
- }
-
- # 5. Fallback: unresolved type
- return "/* TODO: provide $($Param.RawType) for $paramName */"
-}
-
-function Generate-ProviderCode {
- param([System.Collections.Generic.List[ConfigInfo]]$ConfigsInOrder)
-
- Write-ColorOutput "`nGenerating config provider code..." "Cyan"
-
- $sb = [System.Text.StringBuilder]::new()
- [void]$sb.AppendLine("package provider")
- [void]$sb.AppendLine()
- [void]$sb.AppendLine("import `"$ModulePath`"")
- [void]$sb.AppendLine()
-
- # Interface
- [void]$sb.AppendLine("type ConfigProvider interface {")
- foreach ($cfg in $ConfigsInOrder) {
- $line = "`tProvide$($cfg.Domain)() config.$($cfg.Domain)"
- [void]$sb.AppendLine($line)
- }
- [void]$sb.AppendLine("}")
- [void]$sb.AppendLine()
-
- # Struct
- [void]$sb.AppendLine("type configProvider struct {")
- foreach ($cfg in $ConfigsInOrder) {
- $line = "`t$($cfg.VarName) config.$($cfg.Domain)"
- [void]$sb.AppendLine($line)
- }
- [void]$sb.AppendLine("}")
- [void]$sb.AppendLine()
-
- # Constructor
- [void]$sb.AppendLine("func NewConfigProvider() ConfigProvider {")
-
- # Initialize configs in topological order
- foreach ($cfg in $ConfigsInOrder) {
- # Check if this config depends on another config
- $dependentConfigVar = $null
- if ($cfg.ConfigDependencies.Count -gt 0) {
- $dependentConfigVar = Get-LowerCamelCase $cfg.ConfigDependencies[0]
- }
-
- $args = @()
- foreach ($param in $cfg.Parameters) {
- $args += Resolve-ConfigArgument -Param $param -DependentConfigVar $dependentConfigVar
- }
- $argsStr = $args -join ", "
- $line = "`t$($cfg.VarName) := config.$($cfg.ConstructorName)($argsStr)"
- [void]$sb.AppendLine($line)
- }
-
- [void]$sb.AppendLine("`treturn &configProvider{")
- foreach ($cfg in $ConfigsInOrder) {
- $line = "`t`t$($cfg.VarName): $($cfg.VarName),"
- [void]$sb.AppendLine($line)
- }
- [void]$sb.AppendLine("`t}")
- [void]$sb.AppendLine("}")
- [void]$sb.AppendLine()
-
- # Getter methods
- foreach ($cfg in $ConfigsInOrder) {
- [void]$sb.AppendLine("func (c *configProvider) Provide$($cfg.Domain)() config.$($cfg.Domain) {")
- [void]$sb.AppendLine("`treturn c.$($cfg.VarName)")
- [void]$sb.AppendLine("}")
- [void]$sb.AppendLine()
- }
-
- return $sb.ToString()
-}
-
-function Write-ProviderFile {
- param([string]$Code, [string]$OutputPath)
-
- Write-ColorOutput "Writing to $OutputPath..." "Cyan"
-
- # Ensure directory exists
- $dir = Split-Path $OutputPath -Parent
- if ($dir -and -not (Test-Path $dir)) {
- New-Item -ItemType Directory -Path $dir -Force | Out-Null
- }
-
- # Write file as UTF-8 without BOM
- $utf8NoBom = [System.Text.UTF8Encoding]::new($false)
- [System.IO.File]::WriteAllText($OutputPath, $Code, $utf8NoBom)
-
- Write-ColorOutput " Successfully generated $OutputPath" "Green"
-}
-
-# ============================================
-# MAIN EXECUTION
-# ============================================
-
-try {
- Write-ColorOutput "`n=========================================" "Blue"
- Write-ColorOutput " Go Config Provider Generator v1.0" "Blue"
- Write-ColorOutput "=========================================`n" "Blue"
-
- # Step 1: Parse all config constructors
- $configs = Parse-GoFiles -Directory $ConfigDir
-
- # Step 2: Perform topological sort (configs can depend on other configs)
- $sortedConfigs = Get-TopologicalOrder -Configs $configs
-
- # Step 3: Generate provider code
- $code = Generate-ProviderCode -ConfigsInOrder $sortedConfigs
-
- # Step 4: Write to file
- Write-ProviderFile -Code $code -OutputPath $OutputFile
-
- Write-ColorOutput "`nSUCCESS! Config provider generated successfully.`n" "Green"
- Write-ColorOutput "Next steps:" "Cyan"
- Write-ColorOutput " 1. Review $OutputFile" "White"
- Write-ColorOutput " 2. Fill any /* TODO: provide ... */ placeholders" "White"
- Write-ColorOutput " 3. Run: go build ./provider" "White"
-
-} catch {
- Write-ColorOutput "`nERROR: $($_.Exception.Message)" "Red"
- Write-ColorOutput "Stack trace: $($_.ScriptStackTrace)" "Yellow"
- exit 1
+#Requires -Version 5.1
+<#
+.SYNOPSIS
+ Automatic Dependency Injection Generator for Go Configuration
+
+.DESCRIPTION
+ Scans ./config/ directory, discovers all config constructors, infers their dependencies,
+ and generates provider/config_provider.go with full DI wiring. Special handling for
+ chained dependencies where configs depend on other configs (e.g., DatabaseConfig depends on EnvConfig).
+
+.EXAMPLE
+ .\config_injector.ps1
+
+.NOTES
+ - Works with PowerShell 5.1+ and PowerShell 7+
+ - No external dependencies required
+ - Supports multi-line constructor signatures
+ - Handles config-to-config dependencies with topological sorting
+ - Special handling for method calls like envConfig.GetDatabaseHost()
+#>
+
+[CmdletBinding()]
+param()
+
+# Configuration
+$ConfigDir = "./config"
+$OutputFile = "provider/config_provider.go"
+$ModulePath = "abdanhafidz.com/go-boilerplate/config"
+
+# ANSI colors for better output
+$script:UseColors = $Host.UI.SupportsVirtualTerminal
+function Write-ColorOutput {
+ param([string]$Message, [string]$Color = "White")
+ if ($script:UseColors) {
+ $colors = @{
+ "Green" = "`e[32m"; "Yellow" = "`e[33m"; "Red" = "`e[31m"
+ "Cyan" = "`e[36m"; "Blue" = "`e[34m"; "Reset" = "`e[0m"
+ }
+ Write-Host "$($colors[$Color])$Message$($colors['Reset'])"
+ } else {
+ Write-Host $Message
+ }
+}
+
+# Data structures
+class ConfigInfo {
+ [string]$ConstructorName # NewDatabaseConfig
+ [string]$Domain # DatabaseConfig
+ [string]$VarName # databaseConfig
+ [System.Collections.Generic.List[Parameter]]$Parameters
+ [System.Collections.Generic.List[string]]$ConfigDependencies
+
+ ConfigInfo() {
+ $this.Parameters = [System.Collections.Generic.List[Parameter]]::new()
+ $this.ConfigDependencies = [System.Collections.Generic.List[string]]::new()
+ }
+}
+
+class Parameter {
+ [string]$Name
+ [string]$RawType
+ [string]$NormalizedType
+}
+
+function Get-LowerCamelCase {
+ param([string]$Text)
+ if ($Text.Length -eq 0) { return $Text }
+ return $Text.Substring(0, 1).ToLower() + $Text.Substring(1)
+}
+
+function Normalize-TypeName {
+ param([string]$TypeStr)
+
+ # Remove leading pointer
+ $cleaned = $TypeStr -replace '^\*+', ''
+
+ # Remove package prefix (everything before last dot)
+ if ($cleaned -match '\.([^.]+)$') {
+ $cleaned = $matches[1]
+ }
+
+ return $cleaned.Trim()
+}
+
+function Parse-GoFiles {
+ param([string]$Directory)
+
+ Write-ColorOutput "Scanning for config constructors in $Directory..." "Cyan"
+
+ if (-not (Test-Path $Directory)) {
+ Write-ColorOutput "ERROR: Directory '$Directory' not found!" "Red"
+ exit 1
+ }
+
+ $goFiles = Get-ChildItem -Path $Directory -Filter "*.go" -Recurse -File
+ $configs = [System.Collections.Generic.List[ConfigInfo]]::new()
+
+ foreach ($file in $goFiles) {
+ $content = Get-Content $file.FullName -Raw
+
+ # Match function signatures (support multi-line)
+ # Pattern: func NewXxxConfig(...) XxxConfig
+ $pattern = '(?ms)func\s+(New[a-zA-Z0-9]+Config)\s*\(([^)]*)\)\s+([a-zA-Z0-9*_.]+Config)'
+ $matches = [regex]::Matches($content, $pattern)
+
+ foreach ($match in $matches) {
+ $constructorName = $match.Groups[1].Value
+ $paramsStr = $match.Groups[2].Value
+ $returnType = $match.Groups[3].Value
+
+ # Extract domain name (XxxConfig)
+ $domain = Normalize-TypeName $returnType
+ $varName = Get-LowerCamelCase $domain
+
+ $config = [ConfigInfo]::new()
+ $config.ConstructorName = $constructorName
+ $config.Domain = $domain
+ $config.VarName = $varName
+
+ # Parse parameters
+ if ($paramsStr.Trim() -ne "") {
+ # Split by comma, but be careful with nested types
+ $paramList = $paramsStr -split ',\s*(?![^<>]*>)'
+
+ foreach ($param in $paramList) {
+ $param = $param.Trim()
+ if ($param -eq "") { continue }
+
+ # Split into name and type
+ $parts = $param -split '\s+', 2
+
+ $p = [Parameter]::new()
+ if ($parts.Count -eq 2) {
+ $p.Name = $parts[0]
+ $p.RawType = $parts[1]
+ } elseif ($parts.Count -eq 1) {
+ # Anonymous parameter - synthesize name
+ $p.Name = "param$($config.Parameters.Count)"
+ $p.RawType = $parts[0]
+ } else {
+ continue
+ }
+
+ $p.NormalizedType = Normalize-TypeName $p.RawType
+ $config.Parameters.Add($p)
+
+ # Track config dependencies (configs that depend on other configs)
+ if ($p.NormalizedType -match 'Config$') {
+ $config.ConfigDependencies.Add($p.NormalizedType)
+ }
+ }
+ }
+
+ $configs.Add($config)
+ Write-ColorOutput " Found: $constructorName" "Green"
+ }
+ }
+
+ if ($configs.Count -eq 0) {
+ Write-ColorOutput "No config constructors found matching pattern 'NewXxxConfig'!" "Red"
+ exit 1
+ }
+
+ Write-ColorOutput "`nTotal configs discovered: $($configs.Count)" "Blue"
+ return $configs
+}
+
+function Get-TopologicalOrder {
+ param([System.Collections.Generic.List[ConfigInfo]]$Configs)
+
+ Write-ColorOutput "`nBuilding dependency graph..." "Cyan"
+
+ # Build adjacency list
+ $graph = @{}
+ $inDegree = @{}
+ $domainToConfig = @{}
+
+ foreach ($cfg in $Configs) {
+ $graph[$cfg.Domain] = [System.Collections.Generic.List[string]]::new()
+ $inDegree[$cfg.Domain] = 0
+ $domainToConfig[$cfg.Domain] = $cfg
+ }
+
+ # Build edges (dependencies)
+ foreach ($cfg in $Configs) {
+ foreach ($dep in $cfg.ConfigDependencies) {
+ if ($graph.ContainsKey($dep)) {
+ $graph[$dep].Add($cfg.Domain)
+ $inDegree[$cfg.Domain]++
+ }
+ }
+ }
+
+ # Kahn's algorithm for topological sort
+ $queue = [System.Collections.Generic.Queue[string]]::new()
+ foreach ($domain in $inDegree.Keys) {
+ if ($inDegree[$domain] -eq 0) {
+ $queue.Enqueue($domain)
+ }
+ }
+
+ $sorted = [System.Collections.Generic.List[string]]::new()
+
+ while ($queue.Count -gt 0) {
+ $current = $queue.Dequeue()
+ $sorted.Add($current)
+
+ foreach ($neighbor in $graph[$current]) {
+ $inDegree[$neighbor]--
+ if ($inDegree[$neighbor] -eq 0) {
+ $queue.Enqueue($neighbor)
+ }
+ }
+ }
+
+ # Check for cycles
+ if ($sorted.Count -ne $Configs.Count) {
+ $remaining = $inDegree.Keys | Where-Object { $inDegree[$_] -gt 0 }
+ Write-ColorOutput "`nERROR: Circular dependency detected in configs!" "Red"
+ Write-ColorOutput "Configs involved in cycle: $($remaining -join ', ')" "Yellow"
+ exit 1
+ }
+
+ Write-ColorOutput " Dependency graph validated (no cycles)" "Green"
+ Write-ColorOutput " Topological order: $($sorted -join ' -> ')" "Blue"
+
+ # Return configs in topological order
+ return $sorted | ForEach-Object { $domainToConfig[$_] }
+}
+
+function Resolve-ConfigArgument {
+ param(
+ [Parameter]$Param,
+ [string]$DependentConfigVar
+ )
+
+ $type = $Param.NormalizedType
+ $paramName = $Param.Name
+
+ # SPECIAL CASE MAPPINGS FOR CONFIG
+ # ============================================
+
+ # 1. Config pattern: XxxConfig -> already instantiated config variable
+ if ($type -match '^(.+)Config$') {
+ $configVarName = Get-LowerCamelCase $type
+ return $configVarName
+ }
+
+ # 2. String parameters - try to infer from parameter name and match with config getter methods
+ if ($type -eq "string") {
+ # Common patterns for EnvConfig getters
+ $getterMappings = @{
+ "host" = "GetDatabaseHost()"
+ "databaseHost" = "GetDatabaseHost()"
+ "user" = "GetDatabaseUser()"
+ "databaseUser" = "GetDatabaseUser()"
+ "password" = "GetDatabasePassword()"
+ "databasePassword" = "GetDatabasePassword()"
+ "name" = "GetDatabaseName()"
+ "databaseName" = "GetDatabaseName()"
+ "dbName" = "GetDatabaseName()"
+ "port" = "GetDatabasePort()"
+ "databasePort" = "GetDatabasePort()"
+ "salt" = "GetSalt()"
+ "secret" = "GetSecretKey()"
+ "secretKey" = "GetSecretKey()"
+ "jwtSecret" = "GetSecretKey()"
+ "apiKey" = "GetAPIKey()"
+ "timezone" = "GetTimezone()"
+ }
+
+ # Try to find matching getter
+ foreach ($key in $getterMappings.Keys) {
+ if ($paramName -like "*$key*") {
+ return "envConfig.$($getterMappings[$key])"
+ }
+ }
+
+ # If dependent on a config, try to construct getter name from param name
+ if ($DependentConfigVar) {
+ # Convert paramName to PascalCase for getter
+ $getterName = (Get-Culture).TextInfo.ToTitleCase($paramName)
+ $getterName = $getterName -replace '\s', ''
+ return "${DependentConfigVar}.Get${getterName}()"
+ }
+ }
+
+ # 3. Int/port parameters
+ if ($type -eq "int" -or $type -eq "int32" -or $type -eq "int64") {
+ if ($paramName -match "port") {
+ return "envConfig.GetDatabasePort()"
+ }
+ }
+
+ # ADD MORE SPECIAL CASES HERE:
+ # --------------------------------------------
+ # Example: Redis config
+ # if ($paramName -match "redis") {
+ # return "envConfig.GetRedisURL()"
+ # }
+ #
+ # Example: Mail config
+ # if ($paramName -match "smtp") {
+ # return "envConfig.GetSMTPHost()"
+ # }
+ # --------------------------------------------
+
+ # 4. Hardcoded constants (timezone example)
+ if ($type -eq "string" -and $paramName -match "timezone|location") {
+ return "`"Asia/Jakarta`""
+ }
+
+ # 5. Fallback: unresolved type
+ return "/* TODO: provide $($Param.RawType) for $paramName */"
+}
+
+function Generate-ProviderCode {
+ param([System.Collections.Generic.List[ConfigInfo]]$ConfigsInOrder)
+
+ Write-ColorOutput "`nGenerating config provider code..." "Cyan"
+
+ $sb = [System.Text.StringBuilder]::new()
+ [void]$sb.AppendLine("package provider")
+ [void]$sb.AppendLine()
+ [void]$sb.AppendLine("import `"$ModulePath`"")
+ [void]$sb.AppendLine()
+
+ # Interface
+ [void]$sb.AppendLine("type ConfigProvider interface {")
+ foreach ($cfg in $ConfigsInOrder) {
+ $line = "`tProvide$($cfg.Domain)() config.$($cfg.Domain)"
+ [void]$sb.AppendLine($line)
+ }
+ [void]$sb.AppendLine("}")
+ [void]$sb.AppendLine()
+
+ # Struct
+ [void]$sb.AppendLine("type configProvider struct {")
+ foreach ($cfg in $ConfigsInOrder) {
+ $line = "`t$($cfg.VarName) config.$($cfg.Domain)"
+ [void]$sb.AppendLine($line)
+ }
+ [void]$sb.AppendLine("}")
+ [void]$sb.AppendLine()
+
+ # Constructor
+ [void]$sb.AppendLine("func NewConfigProvider() ConfigProvider {")
+
+ # Initialize configs in topological order
+ foreach ($cfg in $ConfigsInOrder) {
+ # Check if this config depends on another config
+ $dependentConfigVar = $null
+ if ($cfg.ConfigDependencies.Count -gt 0) {
+ $dependentConfigVar = Get-LowerCamelCase $cfg.ConfigDependencies[0]
+ }
+
+ $args = @()
+ foreach ($param in $cfg.Parameters) {
+ $args += Resolve-ConfigArgument -Param $param -DependentConfigVar $dependentConfigVar
+ }
+ $argsStr = $args -join ", "
+ $line = "`t$($cfg.VarName) := config.$($cfg.ConstructorName)($argsStr)"
+ [void]$sb.AppendLine($line)
+ }
+
+ [void]$sb.AppendLine("`treturn &configProvider{")
+ foreach ($cfg in $ConfigsInOrder) {
+ $line = "`t`t$($cfg.VarName): $($cfg.VarName),"
+ [void]$sb.AppendLine($line)
+ }
+ [void]$sb.AppendLine("`t}")
+ [void]$sb.AppendLine("}")
+ [void]$sb.AppendLine()
+
+ # Getter methods
+ foreach ($cfg in $ConfigsInOrder) {
+ [void]$sb.AppendLine("func (c *configProvider) Provide$($cfg.Domain)() config.$($cfg.Domain) {")
+ [void]$sb.AppendLine("`treturn c.$($cfg.VarName)")
+ [void]$sb.AppendLine("}")
+ [void]$sb.AppendLine()
+ }
+
+ return $sb.ToString()
+}
+
+function Write-ProviderFile {
+ param([string]$Code, [string]$OutputPath)
+
+ Write-ColorOutput "Writing to $OutputPath..." "Cyan"
+
+ # Ensure directory exists
+ $dir = Split-Path $OutputPath -Parent
+ if ($dir -and -not (Test-Path $dir)) {
+ New-Item -ItemType Directory -Path $dir -Force | Out-Null
+ }
+
+ # Write file as UTF-8 without BOM
+ $utf8NoBom = [System.Text.UTF8Encoding]::new($false)
+ [System.IO.File]::WriteAllText($OutputPath, $Code, $utf8NoBom)
+
+ Write-ColorOutput " Successfully generated $OutputPath" "Green"
+}
+
+# ============================================
+# MAIN EXECUTION
+# ============================================
+
+try {
+ Write-ColorOutput "`n=========================================" "Blue"
+ Write-ColorOutput " Go Config Provider Generator v1.0" "Blue"
+ Write-ColorOutput "=========================================`n" "Blue"
+
+ # Step 1: Parse all config constructors
+ $configs = Parse-GoFiles -Directory $ConfigDir
+
+ # Step 2: Perform topological sort (configs can depend on other configs)
+ $sortedConfigs = Get-TopologicalOrder -Configs $configs
+
+ # Step 3: Generate provider code
+ $code = Generate-ProviderCode -ConfigsInOrder $sortedConfigs
+
+ # Step 4: Write to file
+ Write-ProviderFile -Code $code -OutputPath $OutputFile
+
+ Write-ColorOutput "`nSUCCESS! Config provider generated successfully.`n" "Green"
+ Write-ColorOutput "Next steps:" "Cyan"
+ Write-ColorOutput " 1. Review $OutputFile" "White"
+ Write-ColorOutput " 2. Fill any /* TODO: provide ... */ placeholders" "White"
+ Write-ColorOutput " 3. Run: go build ./provider" "White"
+
+} catch {
+ Write-ColorOutput "`nERROR: $($_.Exception.Message)" "Red"
+ Write-ColorOutput "Stack trace: $($_.ScriptStackTrace)" "Yellow"
+ exit 1
}
\ No newline at end of file
diff --git a/do_inject_controllers.ps1 b/do_inject_controllers.ps1
index 74b78796905c9d0bc3d343f531c6ce2fd91003bb..cde7b7a40e5567c5ba145c1c71b7988739594dba 100644
--- a/do_inject_controllers.ps1
+++ b/do_inject_controllers.ps1
@@ -1,310 +1,310 @@
-#Requires -Version 5.1
-<#
-.SYNOPSIS
- Automatic Dependency Injection Generator for Go Controllers
-
-.DESCRIPTION
- Scans ./controllers/ directory, discovers all controller constructors, infers their dependencies,
- and generates provider/controller_provider.go with full DI wiring.
-
-.EXAMPLE
- .\controller_injector.ps1
-
-.NOTES
- - Works with PowerShell 5.1+ and PowerShell 7+
- - No external dependencies required
- - Supports multi-line constructor signatures
- - Controllers depend on services from ServicesProvider
-#>
-
-[CmdletBinding()]
-param()
-
-# Configuration
-$ControllersDir = "./controllers"
-$OutputFile = "provider/controller_provider.go"
-$ModulePath = "abdanhafidz.com/go-boilerplate/controllers"
-
-# ANSI colors for better output
-$script:UseColors = $Host.UI.SupportsVirtualTerminal
-function Write-ColorOutput {
- param([string]$Message, [string]$Color = "White")
- if ($script:UseColors) {
- $colors = @{
- "Green" = "`e[32m"; "Yellow" = "`e[33m"; "Red" = "`e[31m"
- "Cyan" = "`e[36m"; "Blue" = "`e[34m"; "Reset" = "`e[0m"
- }
- Write-Host "$($colors[$Color])$Message$($colors['Reset'])"
- } else {
- Write-Host $Message
- }
-}
-
-# Data structures
-class ControllerInfo {
- [string]$ConstructorName # NewAccountController
- [string]$Domain # AccountController
- [string]$VarName # accountController
- [System.Collections.Generic.List[Parameter]]$Parameters
-
- ControllerInfo() {
- $this.Parameters = [System.Collections.Generic.List[Parameter]]::new()
- }
-}
-
-class Parameter {
- [string]$Name
- [string]$RawType
- [string]$NormalizedType
-}
-
-function Get-LowerCamelCase {
- param([string]$Text)
- if ($Text.Length -eq 0) { return $Text }
- return $Text.Substring(0, 1).ToLower() + $Text.Substring(1)
-}
-
-function Normalize-TypeName {
- param([string]$TypeStr)
-
- # Remove leading pointer
- $cleaned = $TypeStr -replace '^\*+', ''
-
- # Remove package prefix (everything before last dot)
- if ($cleaned -match '\.([^.]+)$') {
- $cleaned = $matches[1]
- }
-
- return $cleaned.Trim()
-}
-
-function Parse-GoFiles {
- param([string]$Directory)
-
- Write-ColorOutput "Scanning for controller constructors in $Directory..." "Cyan"
-
- if (-not (Test-Path $Directory)) {
- Write-ColorOutput "ERROR: Directory '$Directory' not found!" "Red"
- exit 1
- }
-
- $goFiles = Get-ChildItem -Path $Directory -Filter "*.go" -Recurse -File
- $controllers = [System.Collections.Generic.List[ControllerInfo]]::new()
-
- foreach ($file in $goFiles) {
- $content = Get-Content $file.FullName -Raw
-
- # Match function signatures (support multi-line)
- # Pattern: func NewXxxController(...) XxxController
- $pattern = '(?ms)func\s+(New[a-zA-Z0-9]+Controller)\s*\(([^)]*)\)\s+([a-zA-Z0-9*_.]+Controller)'
- $matches = [regex]::Matches($content, $pattern)
-
- foreach ($match in $matches) {
- $constructorName = $match.Groups[1].Value
- $paramsStr = $match.Groups[2].Value
- $returnType = $match.Groups[3].Value
-
- # Extract domain name (XxxController)
- $domain = Normalize-TypeName $returnType
- $varName = Get-LowerCamelCase $domain
-
- $controller = [ControllerInfo]::new()
- $controller.ConstructorName = $constructorName
- $controller.Domain = $domain
- $controller.VarName = $varName
-
- # Parse parameters
- if ($paramsStr.Trim() -ne "") {
- # Split by comma, but be careful with nested types
- $paramList = $paramsStr -split ',\s*(?![^<>]*>)'
-
- foreach ($param in $paramList) {
- $param = $param.Trim()
- if ($param -eq "") { continue }
-
- # Split into name and type
- $parts = $param -split '\s+', 2
-
- $p = [Parameter]::new()
- if ($parts.Count -eq 2) {
- $p.Name = $parts[0]
- $p.RawType = $parts[1]
- } elseif ($parts.Count -eq 1) {
- # Anonymous parameter - synthesize name
- $p.Name = "param$($controller.Parameters.Count)"
- $p.RawType = $parts[0]
- } else {
- continue
- }
-
- $p.NormalizedType = Normalize-TypeName $p.RawType
- $controller.Parameters.Add($p)
- }
- }
-
- $controllers.Add($controller)
- Write-ColorOutput " Found: $constructorName" "Green"
- }
- }
-
- if ($controllers.Count -eq 0) {
- Write-ColorOutput "No controller constructors found matching pattern 'NewXxxController'!" "Red"
- exit 1
- }
-
- Write-ColorOutput "`nTotal controllers discovered: $($controllers.Count)" "Blue"
- return $controllers
-}
-
-function Resolve-ControllerArgument {
- param([Parameter]$Param)
-
- $type = $Param.NormalizedType
-
- # DEPENDENCY RESOLUTION RULES
- # ============================================
-
- # 1. Service pattern: XxxxService -> servicesProvider.ProvideXxxxService()
- if ($type -match '^(.+)Service$') {
- $serviceName = $type
- return "servicesProvider.Provide${serviceName}()"
- }
-
- # ADD MORE SPECIAL CASES HERE:
- # --------------------------------------------
- # Example: Config dependency
- # if ($type -eq "Config") {
- # return "configProvider.ProvideConfig()"
- # }
- #
- # Example: Logger
- # if ($type -eq "Logger") {
- # return "loggerProvider.ProvideLogger()"
- # }
- #
- # Example: Validator
- # if ($type -eq "Validator") {
- # return "validatorProvider.ProvideValidator()"
- # }
- # --------------------------------------------
-
- # 2. Fallback: unresolved type
- return "/* TODO: provide $($Param.RawType) */"
-}
-
-function Generate-ProviderCode {
- param([System.Collections.Generic.List[ControllerInfo]]$Controllers)
-
- Write-ColorOutput "`nGenerating controller provider code..." "Cyan"
-
- # Sort controllers alphabetically for consistent output
- $sortedControllers = $Controllers | Sort-Object -Property Domain
-
- $sb = [System.Text.StringBuilder]::new()
- [void]$sb.AppendLine("package provider")
- [void]$sb.AppendLine()
- [void]$sb.AppendLine("import `"$ModulePath`"")
- [void]$sb.AppendLine()
-
- # Interface
- [void]$sb.AppendLine("type ControllerProvider interface {")
- foreach ($ctrl in $sortedControllers) {
- $line = "`tProvide$($ctrl.Domain)() controllers.$($ctrl.Domain)"
- [void]$sb.AppendLine($line)
- }
- [void]$sb.AppendLine("}")
- [void]$sb.AppendLine()
-
- # Struct
- [void]$sb.AppendLine("type controllerProvider struct {")
- foreach ($ctrl in $sortedControllers) {
- $line = "`t$($ctrl.VarName) controllers.$($ctrl.Domain)"
- [void]$sb.AppendLine($line)
- }
- [void]$sb.AppendLine("}")
- [void]$sb.AppendLine()
-
- # Constructor
- [void]$sb.AppendLine("func NewControllerProvider(servicesProvider ServicesProvider) ControllerProvider {")
- [void]$sb.AppendLine()
-
- # Initialize controllers
- foreach ($ctrl in $sortedControllers) {
- $args = @()
- foreach ($param in $ctrl.Parameters) {
- $args += Resolve-ControllerArgument $param
- }
- $argsStr = $args -join ", "
- $line = "`t$($ctrl.VarName) := controllers.$($ctrl.ConstructorName)($argsStr)"
- [void]$sb.AppendLine($line)
- }
-
- [void]$sb.AppendLine("`treturn &controllerProvider{")
- foreach ($ctrl in $sortedControllers) {
- $line = "`t`t$($ctrl.VarName): $($ctrl.VarName),"
- [void]$sb.AppendLine($line)
- }
- [void]$sb.AppendLine("`t}")
- [void]$sb.AppendLine("}")
- [void]$sb.AppendLine()
-
- # Getter methods
- [void]$sb.AppendLine("// --- Getter Methods ---")
- [void]$sb.AppendLine()
- foreach ($ctrl in $sortedControllers) {
- [void]$sb.AppendLine("func (c *controllerProvider) Provide$($ctrl.Domain)() controllers.$($ctrl.Domain) {")
- [void]$sb.AppendLine("`treturn c.$($ctrl.VarName)")
- [void]$sb.AppendLine("}")
- [void]$sb.AppendLine()
- }
-
- return $sb.ToString()
-}
-
-function Write-ProviderFile {
- param([string]$Code, [string]$OutputPath)
-
- Write-ColorOutput "Writing to $OutputPath..." "Cyan"
-
- # Ensure directory exists
- $dir = Split-Path $OutputPath -Parent
- if ($dir -and -not (Test-Path $dir)) {
- New-Item -ItemType Directory -Path $dir -Force | Out-Null
- }
-
- # Write file as UTF-8 without BOM
- $utf8NoBom = [System.Text.UTF8Encoding]::new($false)
- [System.IO.File]::WriteAllText($OutputPath, $Code, $utf8NoBom)
-
- Write-ColorOutput " Successfully generated $OutputPath" "Green"
-}
-
-# ============================================
-# MAIN EXECUTION
-# ============================================
-
-try {
- Write-ColorOutput "`n=========================================" "Blue"
- Write-ColorOutput " Go Controller Provider Generator v1.0" "Blue"
- Write-ColorOutput "=========================================`n" "Blue"
-
- # Step 1: Parse all controller constructors
- $controllers = Parse-GoFiles -Directory $ControllersDir
-
- # Step 2: Generate provider code
- $code = Generate-ProviderCode -Controllers $controllers
-
- # Step 3: Write to file
- Write-ProviderFile -Code $code -OutputPath $OutputFile
-
- Write-ColorOutput "`nSUCCESS! Controller provider generated successfully.`n" "Green"
- Write-ColorOutput "Next steps:" "Cyan"
- Write-ColorOutput " 1. Review $OutputFile" "White"
- Write-ColorOutput " 2. Fill any /* TODO: provide ... */ placeholders" "White"
- Write-ColorOutput " 3. Run: go build ./provider" "White"
-
-} catch {
- Write-ColorOutput "`nERROR: $($_.Exception.Message)" "Red"
- Write-ColorOutput "Stack trace: $($_.ScriptStackTrace)" "Yellow"
- exit 1
+#Requires -Version 5.1
+<#
+.SYNOPSIS
+ Automatic Dependency Injection Generator for Go Controllers
+
+.DESCRIPTION
+ Scans ./controllers/ directory, discovers all controller constructors, infers their dependencies,
+ and generates provider/controller_provider.go with full DI wiring.
+
+.EXAMPLE
+ .\controller_injector.ps1
+
+.NOTES
+ - Works with PowerShell 5.1+ and PowerShell 7+
+ - No external dependencies required
+ - Supports multi-line constructor signatures
+ - Controllers depend on services from ServicesProvider
+#>
+
+[CmdletBinding()]
+param()
+
+# Configuration
+$ControllersDir = "./controllers"
+$OutputFile = "provider/controller_provider.go"
+$ModulePath = "abdanhafidz.com/go-boilerplate/controllers"
+
+# ANSI colors for better output
+$script:UseColors = $Host.UI.SupportsVirtualTerminal
+function Write-ColorOutput {
+ param([string]$Message, [string]$Color = "White")
+ if ($script:UseColors) {
+ $colors = @{
+ "Green" = "`e[32m"; "Yellow" = "`e[33m"; "Red" = "`e[31m"
+ "Cyan" = "`e[36m"; "Blue" = "`e[34m"; "Reset" = "`e[0m"
+ }
+ Write-Host "$($colors[$Color])$Message$($colors['Reset'])"
+ } else {
+ Write-Host $Message
+ }
+}
+
+# Data structures
+class ControllerInfo {
+ [string]$ConstructorName # NewAccountController
+ [string]$Domain # AccountController
+ [string]$VarName # accountController
+ [System.Collections.Generic.List[Parameter]]$Parameters
+
+ ControllerInfo() {
+ $this.Parameters = [System.Collections.Generic.List[Parameter]]::new()
+ }
+}
+
+class Parameter {
+ [string]$Name
+ [string]$RawType
+ [string]$NormalizedType
+}
+
+function Get-LowerCamelCase {
+ param([string]$Text)
+ if ($Text.Length -eq 0) { return $Text }
+ return $Text.Substring(0, 1).ToLower() + $Text.Substring(1)
+}
+
+function Normalize-TypeName {
+ param([string]$TypeStr)
+
+ # Remove leading pointer
+ $cleaned = $TypeStr -replace '^\*+', ''
+
+ # Remove package prefix (everything before last dot)
+ if ($cleaned -match '\.([^.]+)$') {
+ $cleaned = $matches[1]
+ }
+
+ return $cleaned.Trim()
+}
+
+function Parse-GoFiles {
+ param([string]$Directory)
+
+ Write-ColorOutput "Scanning for controller constructors in $Directory..." "Cyan"
+
+ if (-not (Test-Path $Directory)) {
+ Write-ColorOutput "ERROR: Directory '$Directory' not found!" "Red"
+ exit 1
+ }
+
+ $goFiles = Get-ChildItem -Path $Directory -Filter "*.go" -Recurse -File
+ $controllers = [System.Collections.Generic.List[ControllerInfo]]::new()
+
+ foreach ($file in $goFiles) {
+ $content = Get-Content $file.FullName -Raw
+
+ # Match function signatures (support multi-line)
+ # Pattern: func NewXxxController(...) XxxController
+ $pattern = '(?ms)func\s+(New[a-zA-Z0-9]+Controller)\s*\(([^)]*)\)\s+([a-zA-Z0-9*_.]+Controller)'
+ $matches = [regex]::Matches($content, $pattern)
+
+ foreach ($match in $matches) {
+ $constructorName = $match.Groups[1].Value
+ $paramsStr = $match.Groups[2].Value
+ $returnType = $match.Groups[3].Value
+
+ # Extract domain name (XxxController)
+ $domain = Normalize-TypeName $returnType
+ $varName = Get-LowerCamelCase $domain
+
+ $controller = [ControllerInfo]::new()
+ $controller.ConstructorName = $constructorName
+ $controller.Domain = $domain
+ $controller.VarName = $varName
+
+ # Parse parameters
+ if ($paramsStr.Trim() -ne "") {
+ # Split by comma, but be careful with nested types
+ $paramList = $paramsStr -split ',\s*(?![^<>]*>)'
+
+ foreach ($param in $paramList) {
+ $param = $param.Trim()
+ if ($param -eq "") { continue }
+
+ # Split into name and type
+ $parts = $param -split '\s+', 2
+
+ $p = [Parameter]::new()
+ if ($parts.Count -eq 2) {
+ $p.Name = $parts[0]
+ $p.RawType = $parts[1]
+ } elseif ($parts.Count -eq 1) {
+ # Anonymous parameter - synthesize name
+ $p.Name = "param$($controller.Parameters.Count)"
+ $p.RawType = $parts[0]
+ } else {
+ continue
+ }
+
+ $p.NormalizedType = Normalize-TypeName $p.RawType
+ $controller.Parameters.Add($p)
+ }
+ }
+
+ $controllers.Add($controller)
+ Write-ColorOutput " Found: $constructorName" "Green"
+ }
+ }
+
+ if ($controllers.Count -eq 0) {
+ Write-ColorOutput "No controller constructors found matching pattern 'NewXxxController'!" "Red"
+ exit 1
+ }
+
+ Write-ColorOutput "`nTotal controllers discovered: $($controllers.Count)" "Blue"
+ return $controllers
+}
+
+function Resolve-ControllerArgument {
+ param([Parameter]$Param)
+
+ $type = $Param.NormalizedType
+
+ # DEPENDENCY RESOLUTION RULES
+ # ============================================
+
+ # 1. Service pattern: XxxxService -> servicesProvider.ProvideXxxxService()
+ if ($type -match '^(.+)Service$') {
+ $serviceName = $type
+ return "servicesProvider.Provide${serviceName}()"
+ }
+
+ # ADD MORE SPECIAL CASES HERE:
+ # --------------------------------------------
+ # Example: Config dependency
+ # if ($type -eq "Config") {
+ # return "configProvider.ProvideConfig()"
+ # }
+ #
+ # Example: Logger
+ # if ($type -eq "Logger") {
+ # return "loggerProvider.ProvideLogger()"
+ # }
+ #
+ # Example: Validator
+ # if ($type -eq "Validator") {
+ # return "validatorProvider.ProvideValidator()"
+ # }
+ # --------------------------------------------
+
+ # 2. Fallback: unresolved type
+ return "/* TODO: provide $($Param.RawType) */"
+}
+
+function Generate-ProviderCode {
+ param([System.Collections.Generic.List[ControllerInfo]]$Controllers)
+
+ Write-ColorOutput "`nGenerating controller provider code..." "Cyan"
+
+ # Sort controllers alphabetically for consistent output
+ $sortedControllers = $Controllers | Sort-Object -Property Domain
+
+ $sb = [System.Text.StringBuilder]::new()
+ [void]$sb.AppendLine("package provider")
+ [void]$sb.AppendLine()
+ [void]$sb.AppendLine("import `"$ModulePath`"")
+ [void]$sb.AppendLine()
+
+ # Interface
+ [void]$sb.AppendLine("type ControllerProvider interface {")
+ foreach ($ctrl in $sortedControllers) {
+ $line = "`tProvide$($ctrl.Domain)() controllers.$($ctrl.Domain)"
+ [void]$sb.AppendLine($line)
+ }
+ [void]$sb.AppendLine("}")
+ [void]$sb.AppendLine()
+
+ # Struct
+ [void]$sb.AppendLine("type controllerProvider struct {")
+ foreach ($ctrl in $sortedControllers) {
+ $line = "`t$($ctrl.VarName) controllers.$($ctrl.Domain)"
+ [void]$sb.AppendLine($line)
+ }
+ [void]$sb.AppendLine("}")
+ [void]$sb.AppendLine()
+
+ # Constructor
+ [void]$sb.AppendLine("func NewControllerProvider(servicesProvider ServicesProvider) ControllerProvider {")
+ [void]$sb.AppendLine()
+
+ # Initialize controllers
+ foreach ($ctrl in $sortedControllers) {
+ $args = @()
+ foreach ($param in $ctrl.Parameters) {
+ $args += Resolve-ControllerArgument $param
+ }
+ $argsStr = $args -join ", "
+ $line = "`t$($ctrl.VarName) := controllers.$($ctrl.ConstructorName)($argsStr)"
+ [void]$sb.AppendLine($line)
+ }
+
+ [void]$sb.AppendLine("`treturn &controllerProvider{")
+ foreach ($ctrl in $sortedControllers) {
+ $line = "`t`t$($ctrl.VarName): $($ctrl.VarName),"
+ [void]$sb.AppendLine($line)
+ }
+ [void]$sb.AppendLine("`t}")
+ [void]$sb.AppendLine("}")
+ [void]$sb.AppendLine()
+
+ # Getter methods
+ [void]$sb.AppendLine("// --- Getter Methods ---")
+ [void]$sb.AppendLine()
+ foreach ($ctrl in $sortedControllers) {
+ [void]$sb.AppendLine("func (c *controllerProvider) Provide$($ctrl.Domain)() controllers.$($ctrl.Domain) {")
+ [void]$sb.AppendLine("`treturn c.$($ctrl.VarName)")
+ [void]$sb.AppendLine("}")
+ [void]$sb.AppendLine()
+ }
+
+ return $sb.ToString()
+}
+
+function Write-ProviderFile {
+ param([string]$Code, [string]$OutputPath)
+
+ Write-ColorOutput "Writing to $OutputPath..." "Cyan"
+
+ # Ensure directory exists
+ $dir = Split-Path $OutputPath -Parent
+ if ($dir -and -not (Test-Path $dir)) {
+ New-Item -ItemType Directory -Path $dir -Force | Out-Null
+ }
+
+ # Write file as UTF-8 without BOM
+ $utf8NoBom = [System.Text.UTF8Encoding]::new($false)
+ [System.IO.File]::WriteAllText($OutputPath, $Code, $utf8NoBom)
+
+ Write-ColorOutput " Successfully generated $OutputPath" "Green"
+}
+
+# ============================================
+# MAIN EXECUTION
+# ============================================
+
+try {
+ Write-ColorOutput "`n=========================================" "Blue"
+ Write-ColorOutput " Go Controller Provider Generator v1.0" "Blue"
+ Write-ColorOutput "=========================================`n" "Blue"
+
+ # Step 1: Parse all controller constructors
+ $controllers = Parse-GoFiles -Directory $ControllersDir
+
+ # Step 2: Generate provider code
+ $code = Generate-ProviderCode -Controllers $controllers
+
+ # Step 3: Write to file
+ Write-ProviderFile -Code $code -OutputPath $OutputFile
+
+ Write-ColorOutput "`nSUCCESS! Controller provider generated successfully.`n" "Green"
+ Write-ColorOutput "Next steps:" "Cyan"
+ Write-ColorOutput " 1. Review $OutputFile" "White"
+ Write-ColorOutput " 2. Fill any /* TODO: provide ... */ placeholders" "White"
+ Write-ColorOutput " 3. Run: go build ./provider" "White"
+
+} catch {
+ Write-ColorOutput "`nERROR: $($_.Exception.Message)" "Red"
+ Write-ColorOutput "Stack trace: $($_.ScriptStackTrace)" "Yellow"
+ exit 1
}
\ No newline at end of file
diff --git a/do_inject_middleware.ps1 b/do_inject_middleware.ps1
index a2de970e0565598b5e3ea29f6dd252e700ec195c..856e9ebcfb0f2019496ddbed9cf222acb9f94512 100644
--- a/do_inject_middleware.ps1
+++ b/do_inject_middleware.ps1
@@ -1,312 +1,312 @@
-#Requires -Version 5.1
-<#
-.SYNOPSIS
- Automatic Dependency Injection Generator for Go Middleware
-
-.DESCRIPTION
- Scans ./middleware/ directory, discovers all middleware constructors, infers their dependencies,
- and generates provider/middleware_provider.go with full DI wiring.
-
-.EXAMPLE
- .\middleware_injector.ps1
-
-.NOTES
- - Works with PowerShell 5.1+ and PowerShell 7+
- - No external dependencies required
- - Supports multi-line constructor signatures
- - Middleware depend on services from ServicesProvider
-#>
-
-[CmdletBinding()]
-param()
-
-# Configuration
-$MiddlewareDir = "./middleware"
-$OutputFile = "provider/middleware_provider.go"
-$ModulePath = "abdanhafidz.com/go-boilerplate/middleware"
-
-# ANSI colors for better output
-$script:UseColors = $Host.UI.SupportsVirtualTerminal
-function Write-ColorOutput {
- param([string]$Message, [string]$Color = "White")
- if ($script:UseColors) {
- $colors = @{
- "Green" = "`e[32m"; "Yellow" = "`e[33m"; "Red" = "`e[31m"
- "Cyan" = "`e[36m"; "Blue" = "`e[34m"; "Reset" = "`e[0m"
- }
- Write-Host "$($colors[$Color])$Message$($colors['Reset'])"
- } else {
- Write-Host $Message
- }
-}
-
-# Data structures
-class MiddlewareInfo {
- [string]$ConstructorName # NewAuthenticationMiddleware
- [string]$Domain # AuthenticationMiddleware
- [string]$VarName # authenticationMiddleware
- [System.Collections.Generic.List[Parameter]]$Parameters
-
- MiddlewareInfo() {
- $this.Parameters = [System.Collections.Generic.List[Parameter]]::new()
- }
-}
-
-class Parameter {
- [string]$Name
- [string]$RawType
- [string]$NormalizedType
-}
-
-function Get-LowerCamelCase {
- param([string]$Text)
- if ($Text.Length -eq 0) { return $Text }
- return $Text.Substring(0, 1).ToLower() + $Text.Substring(1)
-}
-
-function Normalize-TypeName {
- param([string]$TypeStr)
-
- # Remove leading pointer
- $cleaned = $TypeStr -replace '^\*+', ''
-
- # Remove package prefix (everything before last dot)
- if ($cleaned -match '\.([^.]+)$') {
- $cleaned = $matches[1]
- }
-
- return $cleaned.Trim()
-}
-
-function Parse-GoFiles {
- param([string]$Directory)
-
- Write-ColorOutput "Scanning for middleware constructors in $Directory..." "Cyan"
-
- if (-not (Test-Path $Directory)) {
- Write-ColorOutput "ERROR: Directory '$Directory' not found!" "Red"
- exit 1
- }
-
- $goFiles = Get-ChildItem -Path $Directory -Filter "*.go" -Recurse -File
- $middlewares = [System.Collections.Generic.List[MiddlewareInfo]]::new()
-
- foreach ($file in $goFiles) {
- $content = Get-Content $file.FullName -Raw
-
- # Match function signatures (support multi-line)
- # Pattern: func NewXxxMiddleware(...) XxxMiddleware
- $pattern = '(?ms)func\s+(New[a-zA-Z0-9]+Middleware)\s*\(([^)]*)\)\s+([a-zA-Z0-9*_.]+Middleware)'
- $matches = [regex]::Matches($content, $pattern)
-
- foreach ($match in $matches) {
- $constructorName = $match.Groups[1].Value
- $paramsStr = $match.Groups[2].Value
- $returnType = $match.Groups[3].Value
-
- # Extract domain name (XxxMiddleware)
- $domain = Normalize-TypeName $returnType
- $varName = Get-LowerCamelCase $domain
-
- $middleware = [MiddlewareInfo]::new()
- $middleware.ConstructorName = $constructorName
- $middleware.Domain = $domain
- $middleware.VarName = $varName
-
- # Parse parameters
- if ($paramsStr.Trim() -ne "") {
- # Split by comma, but be careful with nested types
- $paramList = $paramsStr -split ',\s*(?![^<>]*>)'
-
- foreach ($param in $paramList) {
- $param = $param.Trim()
- if ($param -eq "") { continue }
-
- # Split into name and type
- $parts = $param -split '\s+', 2
-
- $p = [Parameter]::new()
- if ($parts.Count -eq 2) {
- $p.Name = $parts[0]
- $p.RawType = $parts[1]
- } elseif ($parts.Count -eq 1) {
- # Anonymous parameter - synthesize name
- $p.Name = "param$($middleware.Parameters.Count)"
- $p.RawType = $parts[0]
- } else {
- continue
- }
-
- $p.NormalizedType = Normalize-TypeName $p.RawType
- $middleware.Parameters.Add($p)
- }
- }
-
- $middlewares.Add($middleware)
- Write-ColorOutput " Found: $constructorName" "Green"
- }
- }
-
- if ($middlewares.Count -eq 0) {
- Write-ColorOutput "No middleware constructors found matching pattern 'NewXxxMiddleware'!" "Red"
- exit 1
- }
-
- Write-ColorOutput "`nTotal middleware discovered: $($middlewares.Count)" "Blue"
- return $middlewares
-}
-
-function Resolve-MiddlewareArgument {
- param([Parameter]$Param)
-
- $type = $Param.NormalizedType
-
- # DEPENDENCY RESOLUTION RULES
- # ============================================
-
- # 1. Service pattern: XxxxService -> servicesProvider.ProvideXxxxService()
- if ($type -match '^(.+)Service$') {
- $serviceName = $type
- return "servicesProvider.Provide${serviceName}()"
- }
-
- # ADD MORE SPECIAL CASES HERE:
- # --------------------------------------------
- # Example: Config dependency
- # if ($type -eq "Config") {
- # return "configProvider.ProvideConfig()"
- # }
- #
- # Example: Logger
- # if ($type -eq "Logger") {
- # return "loggerProvider.ProvideLogger()"
- # }
- #
- # Example: JWT Config
- # if ($type -eq "JWTConfig") {
- # return "configProvider.ProvideJWTConfig()"
- # }
- #
- # Example: Database
- # if ($type -eq "DB" -or $type -eq "Database") {
- # return "dbProvider.ProvideDatabase()"
- # }
- # --------------------------------------------
-
- # 2. Fallback: unresolved type
- return "/* TODO: provide $($Param.RawType) */"
-}
-
-function Generate-ProviderCode {
- param([System.Collections.Generic.List[MiddlewareInfo]]$Middlewares)
-
- Write-ColorOutput "`nGenerating middleware provider code..." "Cyan"
-
- # Sort middleware alphabetically for consistent output
- $sortedMiddlewares = $Middlewares | Sort-Object -Property Domain
-
- $sb = [System.Text.StringBuilder]::new()
- [void]$sb.AppendLine("package provider")
- [void]$sb.AppendLine()
- [void]$sb.AppendLine("import `"$ModulePath`"")
- [void]$sb.AppendLine()
-
- # Interface
- [void]$sb.AppendLine("type MiddlewareProvider interface {")
- foreach ($mw in $sortedMiddlewares) {
- $line = "`tProvide$($mw.Domain)() middleware.$($mw.Domain)"
- [void]$sb.AppendLine($line)
- }
- [void]$sb.AppendLine("}")
- [void]$sb.AppendLine()
-
- # Struct
- [void]$sb.AppendLine("type middlewareProvider struct {")
- foreach ($mw in $sortedMiddlewares) {
- $line = "`t$($mw.VarName) middleware.$($mw.Domain)"
- [void]$sb.AppendLine($line)
- }
- [void]$sb.AppendLine("}")
- [void]$sb.AppendLine()
-
- # Constructor
- [void]$sb.AppendLine("func NewMiddlewareProvider(servicesProvider ServicesProvider) MiddlewareProvider {")
-
- # Initialize middleware
- foreach ($mw in $sortedMiddlewares) {
- $args = @()
- foreach ($param in $mw.Parameters) {
- $args += Resolve-MiddlewareArgument $param
- }
- $argsStr = $args -join ", "
- $line = "`t$($mw.VarName) := middleware.$($mw.ConstructorName)($argsStr)"
- [void]$sb.AppendLine($line)
- }
-
- [void]$sb.AppendLine("`treturn &middlewareProvider{")
- foreach ($mw in $sortedMiddlewares) {
- $line = "`t`t$($mw.VarName): $($mw.VarName),"
- [void]$sb.AppendLine($line)
- }
- [void]$sb.AppendLine("`t}")
- [void]$sb.AppendLine("}")
- [void]$sb.AppendLine()
-
- # Getter methods
- foreach ($mw in $sortedMiddlewares) {
- [void]$sb.AppendLine("func (p *middlewareProvider) Provide$($mw.Domain)() middleware.$($mw.Domain) {")
- [void]$sb.AppendLine("`treturn p.$($mw.VarName)")
- [void]$sb.AppendLine("}")
- [void]$sb.AppendLine()
- }
-
- return $sb.ToString()
-}
-
-function Write-ProviderFile {
- param([string]$Code, [string]$OutputPath)
-
- Write-ColorOutput "Writing to $OutputPath..." "Cyan"
-
- # Ensure directory exists
- $dir = Split-Path $OutputPath -Parent
- if ($dir -and -not (Test-Path $dir)) {
- New-Item -ItemType Directory -Path $dir -Force | Out-Null
- }
-
- # Write file as UTF-8 without BOM
- $utf8NoBom = [System.Text.UTF8Encoding]::new($false)
- [System.IO.File]::WriteAllText($OutputPath, $Code, $utf8NoBom)
-
- Write-ColorOutput " Successfully generated $OutputPath" "Green"
-}
-
-# ============================================
-# MAIN EXECUTION
-# ============================================
-
-try {
- Write-ColorOutput "`n=========================================" "Blue"
- Write-ColorOutput " Go Middleware Provider Generator v1.0" "Blue"
- Write-ColorOutput "=========================================`n" "Blue"
-
- # Step 1: Parse all middleware constructors
- $middlewares = Parse-GoFiles -Directory $MiddlewareDir
-
- # Step 2: Generate provider code
- $code = Generate-ProviderCode -Middlewares $middlewares
-
- # Step 3: Write to file
- Write-ProviderFile -Code $code -OutputPath $OutputFile
-
- Write-ColorOutput "`nSUCCESS! Middleware provider generated successfully.`n" "Green"
- Write-ColorOutput "Next steps:" "Cyan"
- Write-ColorOutput " 1. Review $OutputFile" "White"
- Write-ColorOutput " 2. Fill any /* TODO: provide ... */ placeholders" "White"
- Write-ColorOutput " 3. Run: go build ./provider" "White"
-
-} catch {
- Write-ColorOutput "`nERROR: $($_.Exception.Message)" "Red"
- Write-ColorOutput "Stack trace: $($_.ScriptStackTrace)" "Yellow"
- exit 1
+#Requires -Version 5.1
+<#
+.SYNOPSIS
+ Automatic Dependency Injection Generator for Go Middleware
+
+.DESCRIPTION
+ Scans ./middleware/ directory, discovers all middleware constructors, infers their dependencies,
+ and generates provider/middleware_provider.go with full DI wiring.
+
+.EXAMPLE
+ .\middleware_injector.ps1
+
+.NOTES
+ - Works with PowerShell 5.1+ and PowerShell 7+
+ - No external dependencies required
+ - Supports multi-line constructor signatures
+ - Middleware depend on services from ServicesProvider
+#>
+
+[CmdletBinding()]
+param()
+
+# Configuration
+$MiddlewareDir = "./middleware"
+$OutputFile = "provider/middleware_provider.go"
+$ModulePath = "abdanhafidz.com/go-boilerplate/middleware"
+
+# ANSI colors for better output
+$script:UseColors = $Host.UI.SupportsVirtualTerminal
+function Write-ColorOutput {
+ param([string]$Message, [string]$Color = "White")
+ if ($script:UseColors) {
+ $colors = @{
+ "Green" = "`e[32m"; "Yellow" = "`e[33m"; "Red" = "`e[31m"
+ "Cyan" = "`e[36m"; "Blue" = "`e[34m"; "Reset" = "`e[0m"
+ }
+ Write-Host "$($colors[$Color])$Message$($colors['Reset'])"
+ } else {
+ Write-Host $Message
+ }
+}
+
+# Data structures
+class MiddlewareInfo {
+ [string]$ConstructorName # NewAuthenticationMiddleware
+ [string]$Domain # AuthenticationMiddleware
+ [string]$VarName # authenticationMiddleware
+ [System.Collections.Generic.List[Parameter]]$Parameters
+
+ MiddlewareInfo() {
+ $this.Parameters = [System.Collections.Generic.List[Parameter]]::new()
+ }
+}
+
+class Parameter {
+ [string]$Name
+ [string]$RawType
+ [string]$NormalizedType
+}
+
+function Get-LowerCamelCase {
+ param([string]$Text)
+ if ($Text.Length -eq 0) { return $Text }
+ return $Text.Substring(0, 1).ToLower() + $Text.Substring(1)
+}
+
+function Normalize-TypeName {
+ param([string]$TypeStr)
+
+ # Remove leading pointer
+ $cleaned = $TypeStr -replace '^\*+', ''
+
+ # Remove package prefix (everything before last dot)
+ if ($cleaned -match '\.([^.]+)$') {
+ $cleaned = $matches[1]
+ }
+
+ return $cleaned.Trim()
+}
+
+function Parse-GoFiles {
+ param([string]$Directory)
+
+ Write-ColorOutput "Scanning for middleware constructors in $Directory..." "Cyan"
+
+ if (-not (Test-Path $Directory)) {
+ Write-ColorOutput "ERROR: Directory '$Directory' not found!" "Red"
+ exit 1
+ }
+
+ $goFiles = Get-ChildItem -Path $Directory -Filter "*.go" -Recurse -File
+ $middlewares = [System.Collections.Generic.List[MiddlewareInfo]]::new()
+
+ foreach ($file in $goFiles) {
+ $content = Get-Content $file.FullName -Raw
+
+ # Match function signatures (support multi-line)
+ # Pattern: func NewXxxMiddleware(...) XxxMiddleware
+ $pattern = '(?ms)func\s+(New[a-zA-Z0-9]+Middleware)\s*\(([^)]*)\)\s+([a-zA-Z0-9*_.]+Middleware)'
+ $matches = [regex]::Matches($content, $pattern)
+
+ foreach ($match in $matches) {
+ $constructorName = $match.Groups[1].Value
+ $paramsStr = $match.Groups[2].Value
+ $returnType = $match.Groups[3].Value
+
+ # Extract domain name (XxxMiddleware)
+ $domain = Normalize-TypeName $returnType
+ $varName = Get-LowerCamelCase $domain
+
+ $middleware = [MiddlewareInfo]::new()
+ $middleware.ConstructorName = $constructorName
+ $middleware.Domain = $domain
+ $middleware.VarName = $varName
+
+ # Parse parameters
+ if ($paramsStr.Trim() -ne "") {
+ # Split by comma, but be careful with nested types
+ $paramList = $paramsStr -split ',\s*(?![^<>]*>)'
+
+ foreach ($param in $paramList) {
+ $param = $param.Trim()
+ if ($param -eq "") { continue }
+
+ # Split into name and type
+ $parts = $param -split '\s+', 2
+
+ $p = [Parameter]::new()
+ if ($parts.Count -eq 2) {
+ $p.Name = $parts[0]
+ $p.RawType = $parts[1]
+ } elseif ($parts.Count -eq 1) {
+ # Anonymous parameter - synthesize name
+ $p.Name = "param$($middleware.Parameters.Count)"
+ $p.RawType = $parts[0]
+ } else {
+ continue
+ }
+
+ $p.NormalizedType = Normalize-TypeName $p.RawType
+ $middleware.Parameters.Add($p)
+ }
+ }
+
+ $middlewares.Add($middleware)
+ Write-ColorOutput " Found: $constructorName" "Green"
+ }
+ }
+
+ if ($middlewares.Count -eq 0) {
+ Write-ColorOutput "No middleware constructors found matching pattern 'NewXxxMiddleware'!" "Red"
+ exit 1
+ }
+
+ Write-ColorOutput "`nTotal middleware discovered: $($middlewares.Count)" "Blue"
+ return $middlewares
+}
+
+function Resolve-MiddlewareArgument {
+ param([Parameter]$Param)
+
+ $type = $Param.NormalizedType
+
+ # DEPENDENCY RESOLUTION RULES
+ # ============================================
+
+ # 1. Service pattern: XxxxService -> servicesProvider.ProvideXxxxService()
+ if ($type -match '^(.+)Service$') {
+ $serviceName = $type
+ return "servicesProvider.Provide${serviceName}()"
+ }
+
+ # ADD MORE SPECIAL CASES HERE:
+ # --------------------------------------------
+ # Example: Config dependency
+ # if ($type -eq "Config") {
+ # return "configProvider.ProvideConfig()"
+ # }
+ #
+ # Example: Logger
+ # if ($type -eq "Logger") {
+ # return "loggerProvider.ProvideLogger()"
+ # }
+ #
+ # Example: JWT Config
+ # if ($type -eq "JWTConfig") {
+ # return "configProvider.ProvideJWTConfig()"
+ # }
+ #
+ # Example: Database
+ # if ($type -eq "DB" -or $type -eq "Database") {
+ # return "dbProvider.ProvideDatabase()"
+ # }
+ # --------------------------------------------
+
+ # 2. Fallback: unresolved type
+ return "/* TODO: provide $($Param.RawType) */"
+}
+
+function Generate-ProviderCode {
+ param([System.Collections.Generic.List[MiddlewareInfo]]$Middlewares)
+
+ Write-ColorOutput "`nGenerating middleware provider code..." "Cyan"
+
+ # Sort middleware alphabetically for consistent output
+ $sortedMiddlewares = $Middlewares | Sort-Object -Property Domain
+
+ $sb = [System.Text.StringBuilder]::new()
+ [void]$sb.AppendLine("package provider")
+ [void]$sb.AppendLine()
+ [void]$sb.AppendLine("import `"$ModulePath`"")
+ [void]$sb.AppendLine()
+
+ # Interface
+ [void]$sb.AppendLine("type MiddlewareProvider interface {")
+ foreach ($mw in $sortedMiddlewares) {
+ $line = "`tProvide$($mw.Domain)() middleware.$($mw.Domain)"
+ [void]$sb.AppendLine($line)
+ }
+ [void]$sb.AppendLine("}")
+ [void]$sb.AppendLine()
+
+ # Struct
+ [void]$sb.AppendLine("type middlewareProvider struct {")
+ foreach ($mw in $sortedMiddlewares) {
+ $line = "`t$($mw.VarName) middleware.$($mw.Domain)"
+ [void]$sb.AppendLine($line)
+ }
+ [void]$sb.AppendLine("}")
+ [void]$sb.AppendLine()
+
+ # Constructor
+ [void]$sb.AppendLine("func NewMiddlewareProvider(servicesProvider ServicesProvider) MiddlewareProvider {")
+
+ # Initialize middleware
+ foreach ($mw in $sortedMiddlewares) {
+ $args = @()
+ foreach ($param in $mw.Parameters) {
+ $args += Resolve-MiddlewareArgument $param
+ }
+ $argsStr = $args -join ", "
+ $line = "`t$($mw.VarName) := middleware.$($mw.ConstructorName)($argsStr)"
+ [void]$sb.AppendLine($line)
+ }
+
+ [void]$sb.AppendLine("`treturn &middlewareProvider{")
+ foreach ($mw in $sortedMiddlewares) {
+ $line = "`t`t$($mw.VarName): $($mw.VarName),"
+ [void]$sb.AppendLine($line)
+ }
+ [void]$sb.AppendLine("`t}")
+ [void]$sb.AppendLine("}")
+ [void]$sb.AppendLine()
+
+ # Getter methods
+ foreach ($mw in $sortedMiddlewares) {
+ [void]$sb.AppendLine("func (p *middlewareProvider) Provide$($mw.Domain)() middleware.$($mw.Domain) {")
+ [void]$sb.AppendLine("`treturn p.$($mw.VarName)")
+ [void]$sb.AppendLine("}")
+ [void]$sb.AppendLine()
+ }
+
+ return $sb.ToString()
+}
+
+function Write-ProviderFile {
+ param([string]$Code, [string]$OutputPath)
+
+ Write-ColorOutput "Writing to $OutputPath..." "Cyan"
+
+ # Ensure directory exists
+ $dir = Split-Path $OutputPath -Parent
+ if ($dir -and -not (Test-Path $dir)) {
+ New-Item -ItemType Directory -Path $dir -Force | Out-Null
+ }
+
+ # Write file as UTF-8 without BOM
+ $utf8NoBom = [System.Text.UTF8Encoding]::new($false)
+ [System.IO.File]::WriteAllText($OutputPath, $Code, $utf8NoBom)
+
+ Write-ColorOutput " Successfully generated $OutputPath" "Green"
+}
+
+# ============================================
+# MAIN EXECUTION
+# ============================================
+
+try {
+ Write-ColorOutput "`n=========================================" "Blue"
+ Write-ColorOutput " Go Middleware Provider Generator v1.0" "Blue"
+ Write-ColorOutput "=========================================`n" "Blue"
+
+ # Step 1: Parse all middleware constructors
+ $middlewares = Parse-GoFiles -Directory $MiddlewareDir
+
+ # Step 2: Generate provider code
+ $code = Generate-ProviderCode -Middlewares $middlewares
+
+ # Step 3: Write to file
+ Write-ProviderFile -Code $code -OutputPath $OutputFile
+
+ Write-ColorOutput "`nSUCCESS! Middleware provider generated successfully.`n" "Green"
+ Write-ColorOutput "Next steps:" "Cyan"
+ Write-ColorOutput " 1. Review $OutputFile" "White"
+ Write-ColorOutput " 2. Fill any /* TODO: provide ... */ placeholders" "White"
+ Write-ColorOutput " 3. Run: go build ./provider" "White"
+
+} catch {
+ Write-ColorOutput "`nERROR: $($_.Exception.Message)" "Red"
+ Write-ColorOutput "Stack trace: $($_.ScriptStackTrace)" "Yellow"
+ exit 1
}
\ No newline at end of file
diff --git a/do_inject_repository.ps1 b/do_inject_repository.ps1
index 2d2bb3baec54ccb34441d6274307f44c8e654c18..a6596787a690a2ca36a583ce0f300163a14cda32 100644
--- a/do_inject_repository.ps1
+++ b/do_inject_repository.ps1
@@ -1,327 +1,327 @@
-#Requires -Version 5.1
-<#
-.SYNOPSIS
- Automatic Dependency Injection Generator for Go Repositories
-
-.DESCRIPTION
- Scans ./repositories/ directory, discovers all repository constructors,
- and generates provider/repositories_provider.go with full DI wiring.
- Repositories typically depend on database connection from ConfigProvider.
-
-.EXAMPLE
- .\repository_injector.ps1
-
-.NOTES
- - Works with PowerShell 5.1+ and PowerShell 7+
- - No external dependencies required
- - Supports multi-line constructor signatures
- - Repositories depend on database instance from ConfigProvider
-#>
-
-[CmdletBinding()]
-param()
-
-# Configuration
-$RepositoriesDir = "./repositories"
-$OutputFile = "provider/repositories_provider.go"
-$ModulePath = "abdanhafidz.com/go-boilerplate/repositories"
-
-# ANSI colors for better output
-$script:UseColors = $Host.UI.SupportsVirtualTerminal
-function Write-ColorOutput {
- param([string]$Message, [string]$Color = "White")
- if ($script:UseColors) {
- $colors = @{
- "Green" = "`e[32m"; "Yellow" = "`e[33m"; "Red" = "`e[31m"
- "Cyan" = "`e[36m"; "Blue" = "`e[34m"; "Reset" = "`e[0m"
- }
- Write-Host "$($colors[$Color])$Message$($colors['Reset'])"
- } else {
- Write-Host $Message
- }
-}
-
-# Data structures
-class RepositoryInfo {
- [string]$ConstructorName # NewAccountRepository
- [string]$Domain # AccountRepository
- [string]$VarName # accountRepository
- [System.Collections.Generic.List[Parameter]]$Parameters
-
- RepositoryInfo() {
- $this.Parameters = [System.Collections.Generic.List[Parameter]]::new()
- }
-}
-
-class Parameter {
- [string]$Name
- [string]$RawType
- [string]$NormalizedType
-}
-
-function Get-LowerCamelCase {
- param([string]$Text)
- if ($Text.Length -eq 0) { return $Text }
- return $Text.Substring(0, 1).ToLower() + $Text.Substring(1)
-}
-
-function Normalize-TypeName {
- param([string]$TypeStr)
-
- # Remove leading pointer
- $cleaned = $TypeStr -replace '^\*+', ''
-
- # Remove package prefix (everything before last dot)
- if ($cleaned -match '\.([^.]+)$') {
- $cleaned = $matches[1]
- }
-
- return $cleaned.Trim()
-}
-
-function Parse-GoFiles {
- param([string]$Directory)
-
- Write-ColorOutput "Scanning for repository constructors in $Directory..." "Cyan"
-
- if (-not (Test-Path $Directory)) {
- Write-ColorOutput "ERROR: Directory '$Directory' not found!" "Red"
- exit 1
- }
-
- $goFiles = Get-ChildItem -Path $Directory -Filter "*.go" -Recurse -File
- $repositories = [System.Collections.Generic.List[RepositoryInfo]]::new()
-
- foreach ($file in $goFiles) {
- $content = Get-Content $file.FullName -Raw
-
- # Match function signatures (support multi-line)
- # Pattern: func NewXxxRepository(...) XxxRepository
- $pattern = '(?ms)func\s+(New[a-zA-Z0-9]+Repository)\s*\(([^)]*)\)\s+([a-zA-Z0-9*_.]+Repository)'
- $matches = [regex]::Matches($content, $pattern)
-
- foreach ($match in $matches) {
- $constructorName = $match.Groups[1].Value
- $paramsStr = $match.Groups[2].Value
- $returnType = $match.Groups[3].Value
-
- # Extract domain name (XxxRepository)
- $domain = Normalize-TypeName $returnType
- $varName = Get-LowerCamelCase $domain
-
- $repo = [RepositoryInfo]::new()
- $repo.ConstructorName = $constructorName
- $repo.Domain = $domain
- $repo.VarName = $varName
-
- # Parse parameters
- if ($paramsStr.Trim() -ne "") {
- # Split by comma, but be careful with nested types
- $paramList = $paramsStr -split ',\s*(?![^<>]*>)'
-
- foreach ($param in $paramList) {
- $param = $param.Trim()
- if ($param -eq "") { continue }
-
- # Split into name and type
- $parts = $param -split '\s+', 2
-
- $p = [Parameter]::new()
- if ($parts.Count -eq 2) {
- $p.Name = $parts[0]
- $p.RawType = $parts[1]
- } elseif ($parts.Count -eq 1) {
- # Anonymous parameter - synthesize name
- $p.Name = "param$($repo.Parameters.Count)"
- $p.RawType = $parts[0]
- } else {
- continue
- }
-
- $p.NormalizedType = Normalize-TypeName $p.RawType
- $repo.Parameters.Add($p)
- }
- }
-
- $repositories.Add($repo)
- Write-ColorOutput " Found: $constructorName" "Green"
- }
- }
-
- if ($repositories.Count -eq 0) {
- Write-ColorOutput "No repository constructors found matching pattern 'NewXxxRepository'!" "Red"
- exit 1
- }
-
- Write-ColorOutput "`nTotal repositories discovered: $($repositories.Count)" "Blue"
- return $repositories
-}
-
-function Resolve-RepositoryArgument {
- param([Parameter]$Param)
-
- $type = $Param.NormalizedType
- $paramName = $Param.Name
-
- # DEPENDENCY RESOLUTION RULES FOR REPOSITORIES
- # ============================================
-
- # 1. Database connection patterns
- if ($type -match '^(DB|Database|Gorm|SqlDB|Connection)$' -or $paramName -match '^(db|database|conn|connection)$') {
- return "db"
- }
-
- # 2. *gorm.DB (most common in Go GORM projects)
- if ($Param.RawType -match 'gorm\.DB' -or $type -eq "DB") {
- return "db"
- }
-
- # 3. *sql.DB (standard library)
- if ($Param.RawType -match 'sql\.DB') {
- return "db"
- }
-
- # ADD MORE SPECIAL CASES HERE:
- # --------------------------------------------
- # Example: Redis connection
- # if ($type -eq "RedisClient" -or $paramName -match "redis") {
- # return "redisClient"
- # }
- #
- # Example: MongoDB connection
- # if ($type -eq "MongoClient" -or $paramName -match "mongo") {
- # return "mongoClient"
- # }
- #
- # Example: Cache
- # if ($type -eq "Cache" -or $paramName -match "cache") {
- # return "cache"
- # }
- #
- # Example: Logger
- # if ($type -eq "Logger" -or $paramName -match "logger") {
- # return "logger"
- # }
- # --------------------------------------------
-
- # 4. Fallback: unresolved type
- return "/* TODO: provide $($Param.RawType) */"
-}
-
-function Generate-ProviderCode {
- param([System.Collections.Generic.List[RepositoryInfo]]$Repositories)
-
- Write-ColorOutput "`nGenerating repositories provider code..." "Cyan"
-
- # Sort repositories alphabetically for consistent output
- $sortedRepos = $Repositories | Sort-Object -Property Domain
-
- $sb = [System.Text.StringBuilder]::new()
- [void]$sb.AppendLine("package provider")
- [void]$sb.AppendLine()
- [void]$sb.AppendLine("import `"$ModulePath`"")
- [void]$sb.AppendLine()
-
- # Interface
- [void]$sb.AppendLine("type RepositoriesProvider interface {")
- foreach ($repo in $sortedRepos) {
- $line = "`tProvide$($repo.Domain)() repositories.$($repo.Domain)"
- [void]$sb.AppendLine($line)
- }
- [void]$sb.AppendLine("}")
- [void]$sb.AppendLine()
-
- # Struct
- [void]$sb.AppendLine("type repositoriesProvider struct {")
- foreach ($repo in $sortedRepos) {
- $line = "`t$($repo.VarName) repositories.$($repo.Domain)"
- [void]$sb.AppendLine($line)
- }
- [void]$sb.AppendLine("}")
- [void]$sb.AppendLine()
-
- # Constructor
- [void]$sb.AppendLine("func NewRepositoriesProvider(cfg ConfigProvider) RepositoriesProvider {")
- [void]$sb.AppendLine("`tdbConfig := cfg.ProvideDatabaseConfig()")
- [void]$sb.AppendLine("`tdb := dbConfig.GetInstance()")
- [void]$sb.AppendLine()
-
- # Initialize repositories
- foreach ($repo in $sortedRepos) {
- $args = @()
- foreach ($param in $repo.Parameters) {
- $args += Resolve-RepositoryArgument $param
- }
- $argsStr = $args -join ", "
- $line = "`t$($repo.VarName) := repositories.$($repo.ConstructorName)($argsStr)"
- [void]$sb.AppendLine($line)
- }
-
- [void]$sb.AppendLine()
- [void]$sb.AppendLine("`treturn &repositoriesProvider{")
- foreach ($repo in $sortedRepos) {
- $line = "`t`t$($repo.VarName): $($repo.VarName),"
- [void]$sb.AppendLine($line)
- }
- [void]$sb.AppendLine("`t}")
- [void]$sb.AppendLine("}")
- [void]$sb.AppendLine()
-
- # Getter methods
- foreach ($repo in $sortedRepos) {
- [void]$sb.AppendLine("func (r *repositoriesProvider) Provide$($repo.Domain)() repositories.$($repo.Domain) {")
- [void]$sb.AppendLine("`treturn r.$($repo.VarName)")
- [void]$sb.AppendLine("}")
- [void]$sb.AppendLine()
- }
-
- return $sb.ToString()
-}
-
-function Write-ProviderFile {
- param([string]$Code, [string]$OutputPath)
-
- Write-ColorOutput "Writing to $OutputPath..." "Cyan"
-
- # Ensure directory exists
- $dir = Split-Path $OutputPath -Parent
- if ($dir -and -not (Test-Path $dir)) {
- New-Item -ItemType Directory -Path $dir -Force | Out-Null
- }
-
- # Write file as UTF-8 without BOM
- $utf8NoBom = [System.Text.UTF8Encoding]::new($false)
- [System.IO.File]::WriteAllText($OutputPath, $Code, $utf8NoBom)
-
- Write-ColorOutput " Successfully generated $OutputPath" "Green"
-}
-
-# ============================================
-# MAIN EXECUTION
-# ============================================
-
-try {
- Write-ColorOutput "`n=========================================" "Blue"
- Write-ColorOutput " Go Repository Provider Generator v1.0" "Blue"
- Write-ColorOutput "=========================================`n" "Blue"
-
- # Step 1: Parse all repository constructors
- $repositories = Parse-GoFiles -Directory $RepositoriesDir
-
- # Step 2: Generate provider code
- $code = Generate-ProviderCode -Repositories $repositories
-
- # Step 3: Write to file
- Write-ProviderFile -Code $code -OutputPath $OutputFile
-
- Write-ColorOutput "`nSUCCESS! Repositories provider generated successfully.`n" "Green"
- Write-ColorOutput "Next steps:" "Cyan"
- Write-ColorOutput " 1. Review $OutputFile" "White"
- Write-ColorOutput " 2. Fill any /* TODO: provide ... */ placeholders" "White"
- Write-ColorOutput " 3. Run: go build ./provider" "White"
-
-} catch {
- Write-ColorOutput "`nERROR: $($_.Exception.Message)" "Red"
- Write-ColorOutput "Stack trace: $($_.ScriptStackTrace)" "Yellow"
- exit 1
+#Requires -Version 5.1
+<#
+.SYNOPSIS
+ Automatic Dependency Injection Generator for Go Repositories
+
+.DESCRIPTION
+ Scans ./repositories/ directory, discovers all repository constructors,
+ and generates provider/repositories_provider.go with full DI wiring.
+ Repositories typically depend on database connection from ConfigProvider.
+
+.EXAMPLE
+ .\repository_injector.ps1
+
+.NOTES
+ - Works with PowerShell 5.1+ and PowerShell 7+
+ - No external dependencies required
+ - Supports multi-line constructor signatures
+ - Repositories depend on database instance from ConfigProvider
+#>
+
+[CmdletBinding()]
+param()
+
+# Configuration
+$RepositoriesDir = "./repositories"
+$OutputFile = "provider/repositories_provider.go"
+$ModulePath = "abdanhafidz.com/go-boilerplate/repositories"
+
+# ANSI colors for better output
+$script:UseColors = $Host.UI.SupportsVirtualTerminal
+function Write-ColorOutput {
+ param([string]$Message, [string]$Color = "White")
+ if ($script:UseColors) {
+ $colors = @{
+ "Green" = "`e[32m"; "Yellow" = "`e[33m"; "Red" = "`e[31m"
+ "Cyan" = "`e[36m"; "Blue" = "`e[34m"; "Reset" = "`e[0m"
+ }
+ Write-Host "$($colors[$Color])$Message$($colors['Reset'])"
+ } else {
+ Write-Host $Message
+ }
+}
+
+# Data structures
+class RepositoryInfo {
+ [string]$ConstructorName # NewAccountRepository
+ [string]$Domain # AccountRepository
+ [string]$VarName # accountRepository
+ [System.Collections.Generic.List[Parameter]]$Parameters
+
+ RepositoryInfo() {
+ $this.Parameters = [System.Collections.Generic.List[Parameter]]::new()
+ }
+}
+
+class Parameter {
+ [string]$Name
+ [string]$RawType
+ [string]$NormalizedType
+}
+
+function Get-LowerCamelCase {
+ param([string]$Text)
+ if ($Text.Length -eq 0) { return $Text }
+ return $Text.Substring(0, 1).ToLower() + $Text.Substring(1)
+}
+
+function Normalize-TypeName {
+ param([string]$TypeStr)
+
+ # Remove leading pointer
+ $cleaned = $TypeStr -replace '^\*+', ''
+
+ # Remove package prefix (everything before last dot)
+ if ($cleaned -match '\.([^.]+)$') {
+ $cleaned = $matches[1]
+ }
+
+ return $cleaned.Trim()
+}
+
+function Parse-GoFiles {
+ param([string]$Directory)
+
+ Write-ColorOutput "Scanning for repository constructors in $Directory..." "Cyan"
+
+ if (-not (Test-Path $Directory)) {
+ Write-ColorOutput "ERROR: Directory '$Directory' not found!" "Red"
+ exit 1
+ }
+
+ $goFiles = Get-ChildItem -Path $Directory -Filter "*.go" -Recurse -File
+ $repositories = [System.Collections.Generic.List[RepositoryInfo]]::new()
+
+ foreach ($file in $goFiles) {
+ $content = Get-Content $file.FullName -Raw
+
+ # Match function signatures (support multi-line)
+ # Pattern: func NewXxxRepository(...) XxxRepository
+ $pattern = '(?ms)func\s+(New[a-zA-Z0-9]+Repository)\s*\(([^)]*)\)\s+([a-zA-Z0-9*_.]+Repository)'
+ $matches = [regex]::Matches($content, $pattern)
+
+ foreach ($match in $matches) {
+ $constructorName = $match.Groups[1].Value
+ $paramsStr = $match.Groups[2].Value
+ $returnType = $match.Groups[3].Value
+
+ # Extract domain name (XxxRepository)
+ $domain = Normalize-TypeName $returnType
+ $varName = Get-LowerCamelCase $domain
+
+ $repo = [RepositoryInfo]::new()
+ $repo.ConstructorName = $constructorName
+ $repo.Domain = $domain
+ $repo.VarName = $varName
+
+ # Parse parameters
+ if ($paramsStr.Trim() -ne "") {
+ # Split by comma, but be careful with nested types
+ $paramList = $paramsStr -split ',\s*(?![^<>]*>)'
+
+ foreach ($param in $paramList) {
+ $param = $param.Trim()
+ if ($param -eq "") { continue }
+
+ # Split into name and type
+ $parts = $param -split '\s+', 2
+
+ $p = [Parameter]::new()
+ if ($parts.Count -eq 2) {
+ $p.Name = $parts[0]
+ $p.RawType = $parts[1]
+ } elseif ($parts.Count -eq 1) {
+ # Anonymous parameter - synthesize name
+ $p.Name = "param$($repo.Parameters.Count)"
+ $p.RawType = $parts[0]
+ } else {
+ continue
+ }
+
+ $p.NormalizedType = Normalize-TypeName $p.RawType
+ $repo.Parameters.Add($p)
+ }
+ }
+
+ $repositories.Add($repo)
+ Write-ColorOutput " Found: $constructorName" "Green"
+ }
+ }
+
+ if ($repositories.Count -eq 0) {
+ Write-ColorOutput "No repository constructors found matching pattern 'NewXxxRepository'!" "Red"
+ exit 1
+ }
+
+ Write-ColorOutput "`nTotal repositories discovered: $($repositories.Count)" "Blue"
+ return $repositories
+}
+
+function Resolve-RepositoryArgument {
+ param([Parameter]$Param)
+
+ $type = $Param.NormalizedType
+ $paramName = $Param.Name
+
+ # DEPENDENCY RESOLUTION RULES FOR REPOSITORIES
+ # ============================================
+
+ # 1. Database connection patterns
+ if ($type -match '^(DB|Database|Gorm|SqlDB|Connection)$' -or $paramName -match '^(db|database|conn|connection)$') {
+ return "db"
+ }
+
+ # 2. *gorm.DB (most common in Go GORM projects)
+ if ($Param.RawType -match 'gorm\.DB' -or $type -eq "DB") {
+ return "db"
+ }
+
+ # 3. *sql.DB (standard library)
+ if ($Param.RawType -match 'sql\.DB') {
+ return "db"
+ }
+
+ # ADD MORE SPECIAL CASES HERE:
+ # --------------------------------------------
+ # Example: Redis connection
+ # if ($type -eq "RedisClient" -or $paramName -match "redis") {
+ # return "redisClient"
+ # }
+ #
+ # Example: MongoDB connection
+ # if ($type -eq "MongoClient" -or $paramName -match "mongo") {
+ # return "mongoClient"
+ # }
+ #
+ # Example: Cache
+ # if ($type -eq "Cache" -or $paramName -match "cache") {
+ # return "cache"
+ # }
+ #
+ # Example: Logger
+ # if ($type -eq "Logger" -or $paramName -match "logger") {
+ # return "logger"
+ # }
+ # --------------------------------------------
+
+ # 4. Fallback: unresolved type
+ return "/* TODO: provide $($Param.RawType) */"
+}
+
+function Generate-ProviderCode {
+ param([System.Collections.Generic.List[RepositoryInfo]]$Repositories)
+
+ Write-ColorOutput "`nGenerating repositories provider code..." "Cyan"
+
+ # Sort repositories alphabetically for consistent output
+ $sortedRepos = $Repositories | Sort-Object -Property Domain
+
+ $sb = [System.Text.StringBuilder]::new()
+ [void]$sb.AppendLine("package provider")
+ [void]$sb.AppendLine()
+ [void]$sb.AppendLine("import `"$ModulePath`"")
+ [void]$sb.AppendLine()
+
+ # Interface
+ [void]$sb.AppendLine("type RepositoriesProvider interface {")
+ foreach ($repo in $sortedRepos) {
+ $line = "`tProvide$($repo.Domain)() repositories.$($repo.Domain)"
+ [void]$sb.AppendLine($line)
+ }
+ [void]$sb.AppendLine("}")
+ [void]$sb.AppendLine()
+
+ # Struct
+ [void]$sb.AppendLine("type repositoriesProvider struct {")
+ foreach ($repo in $sortedRepos) {
+ $line = "`t$($repo.VarName) repositories.$($repo.Domain)"
+ [void]$sb.AppendLine($line)
+ }
+ [void]$sb.AppendLine("}")
+ [void]$sb.AppendLine()
+
+ # Constructor
+ [void]$sb.AppendLine("func NewRepositoriesProvider(cfg ConfigProvider) RepositoriesProvider {")
+ [void]$sb.AppendLine("`tdbConfig := cfg.ProvideDatabaseConfig()")
+ [void]$sb.AppendLine("`tdb := dbConfig.GetInstance()")
+ [void]$sb.AppendLine()
+
+ # Initialize repositories
+ foreach ($repo in $sortedRepos) {
+ $args = @()
+ foreach ($param in $repo.Parameters) {
+ $args += Resolve-RepositoryArgument $param
+ }
+ $argsStr = $args -join ", "
+ $line = "`t$($repo.VarName) := repositories.$($repo.ConstructorName)($argsStr)"
+ [void]$sb.AppendLine($line)
+ }
+
+ [void]$sb.AppendLine()
+ [void]$sb.AppendLine("`treturn &repositoriesProvider{")
+ foreach ($repo in $sortedRepos) {
+ $line = "`t`t$($repo.VarName): $($repo.VarName),"
+ [void]$sb.AppendLine($line)
+ }
+ [void]$sb.AppendLine("`t}")
+ [void]$sb.AppendLine("}")
+ [void]$sb.AppendLine()
+
+ # Getter methods
+ foreach ($repo in $sortedRepos) {
+ [void]$sb.AppendLine("func (r *repositoriesProvider) Provide$($repo.Domain)() repositories.$($repo.Domain) {")
+ [void]$sb.AppendLine("`treturn r.$($repo.VarName)")
+ [void]$sb.AppendLine("}")
+ [void]$sb.AppendLine()
+ }
+
+ return $sb.ToString()
+}
+
+function Write-ProviderFile {
+ param([string]$Code, [string]$OutputPath)
+
+ Write-ColorOutput "Writing to $OutputPath..." "Cyan"
+
+ # Ensure directory exists
+ $dir = Split-Path $OutputPath -Parent
+ if ($dir -and -not (Test-Path $dir)) {
+ New-Item -ItemType Directory -Path $dir -Force | Out-Null
+ }
+
+ # Write file as UTF-8 without BOM
+ $utf8NoBom = [System.Text.UTF8Encoding]::new($false)
+ [System.IO.File]::WriteAllText($OutputPath, $Code, $utf8NoBom)
+
+ Write-ColorOutput " Successfully generated $OutputPath" "Green"
+}
+
+# ============================================
+# MAIN EXECUTION
+# ============================================
+
+try {
+ Write-ColorOutput "`n=========================================" "Blue"
+ Write-ColorOutput " Go Repository Provider Generator v1.0" "Blue"
+ Write-ColorOutput "=========================================`n" "Blue"
+
+ # Step 1: Parse all repository constructors
+ $repositories = Parse-GoFiles -Directory $RepositoriesDir
+
+ # Step 2: Generate provider code
+ $code = Generate-ProviderCode -Repositories $repositories
+
+ # Step 3: Write to file
+ Write-ProviderFile -Code $code -OutputPath $OutputFile
+
+ Write-ColorOutput "`nSUCCESS! Repositories provider generated successfully.`n" "Green"
+ Write-ColorOutput "Next steps:" "Cyan"
+ Write-ColorOutput " 1. Review $OutputFile" "White"
+ Write-ColorOutput " 2. Fill any /* TODO: provide ... */ placeholders" "White"
+ Write-ColorOutput " 3. Run: go build ./provider" "White"
+
+} catch {
+ Write-ColorOutput "`nERROR: $($_.Exception.Message)" "Red"
+ Write-ColorOutput "Stack trace: $($_.ScriptStackTrace)" "Yellow"
+ exit 1
}
\ No newline at end of file
diff --git a/do_inject_services.ps1 b/do_inject_services.ps1
index 27a3d4e599a52ee1b0577ca5d6c2359606261b47..88d06e345a7863dd42cf139f4577066a90738038 100644
--- a/do_inject_services.ps1
+++ b/do_inject_services.ps1
@@ -1,383 +1,383 @@
-#Requires -Version 5.1
-<#
-.SYNOPSIS
- Automatic Dependency Injection Generator for Go Services
-
-.DESCRIPTION
- Scans ./services/ directory, discovers service constructors, resolves dependencies,
- performs topological sorting, and generates provider/services_provider.go with full DI wiring.
-
-.EXAMPLE
- .\service_injector.ps1
-
-.NOTES
- - Works with PowerShell 5.1+ and PowerShell 7+
- - No external dependencies required
- - Supports multi-line constructor signatures
- - Handles dependency cycles with clear error messages
-#>
-
-[CmdletBinding()]
-param()
-
-# Configuration
-$ServicesDir = "./services"
-$OutputFile = "provider/services_provider.go"
-$ModulePath = "abdanhafidz.com/go-boilerplate/services"
-
-# ANSI colors for better output (fallback to plain text if not supported)
-$script:UseColors = $Host.UI.SupportsVirtualTerminal
-function Write-ColorOutput {
- param([string]$Message, [string]$Color = "White")
- if ($script:UseColors) {
- $colors = @{
- "Green" = "`e[32m"; "Yellow" = "`e[33m"; "Red" = "`e[31m"
- "Cyan" = "`e[36m"; "Blue" = "`e[34m"; "Reset" = "`e[0m"
- }
- Write-Host "$($colors[$Color])$Message$($colors['Reset'])"
- } else {
- Write-Host $Message
- }
-}
-
-# Data structures
-class ServiceInfo {
- [string]$ConstructorName # NewAccountService
- [string]$Domain # AccountService
- [string]$VarName # accountService
- [System.Collections.Generic.List[Parameter]]$Parameters
- [System.Collections.Generic.List[string]]$ServiceDependencies
-
- ServiceInfo() {
- $this.Parameters = [System.Collections.Generic.List[Parameter]]::new()
- $this.ServiceDependencies = [System.Collections.Generic.List[string]]::new()
- }
-}
-
-class Parameter {
- [string]$Name
- [string]$RawType
- [string]$NormalizedType
-}
-
-function Get-LowerCamelCase {
- param([string]$Text)
- if ($Text.Length -eq 0) { return $Text }
- return $Text.Substring(0, 1).ToLower() + $Text.Substring(1)
-}
-
-function Normalize-TypeName {
- param([string]$TypeStr)
-
- # Remove leading pointer
- $cleaned = $TypeStr -replace '^\*+', ''
-
- # Remove package prefix (everything before last dot)
- if ($cleaned -match '\.([^.]+)$') {
- $cleaned = $matches[1]
- }
-
- return $cleaned.Trim()
-}
-
-function Parse-GoFiles {
- param([string]$Directory)
-
- Write-ColorOutput "Scanning for service constructors in $Directory..." "Cyan"
-
- if (-not (Test-Path $Directory)) {
- Write-ColorOutput "ERROR: Directory '$Directory' not found!" "Red"
- exit 1
- }
-
- $goFiles = Get-ChildItem -Path $Directory -Filter "*.go" -Recurse -File
- $services = [System.Collections.Generic.List[ServiceInfo]]::new()
-
- foreach ($file in $goFiles) {
- $content = Get-Content $file.FullName -Raw
-
- # Match function signatures (support multi-line)
- # Pattern: func NewXxxService(...) XxxService
- $pattern = '(?ms)func\s+(New[a-zA-Z0-9]+Service)\s*\(([^)]*)\)\s+([a-zA-Z0-9*_.]+Service)'
- $matches = [regex]::Matches($content, $pattern)
-
- foreach ($match in $matches) {
- $constructorName = $match.Groups[1].Value
- $paramsStr = $match.Groups[2].Value
- $returnType = $match.Groups[3].Value
-
- # Extract domain name (XxxService)
- $domain = Normalize-TypeName $returnType
- $varName = Get-LowerCamelCase $domain
-
- $service = [ServiceInfo]::new()
- $service.ConstructorName = $constructorName
- $service.Domain = $domain
- $service.VarName = $varName
-
- # Parse parameters
- if ($paramsStr.Trim() -ne "") {
- # Split by comma, but be careful with nested types
- $paramList = $paramsStr -split ',\s*(?![^<>]*>)'
-
- foreach ($param in $paramList) {
- $param = $param.Trim()
- if ($param -eq "") { continue }
-
- # Split into name and type
- $parts = $param -split '\s+', 2
-
- $p = [Parameter]::new()
- if ($parts.Count -eq 2) {
- $p.Name = $parts[0]
- $p.RawType = $parts[1]
- } elseif ($parts.Count -eq 1) {
- # Anonymous parameter - synthesize name
- $p.Name = "param$($service.Parameters.Count)"
- $p.RawType = $parts[0]
- } else {
- continue
- }
-
- $p.NormalizedType = Normalize-TypeName $p.RawType
- $service.Parameters.Add($p)
-
- # Track service dependencies
- if ($p.NormalizedType -match 'Service$') {
- $service.ServiceDependencies.Add($p.NormalizedType)
- }
- }
- }
-
- $services.Add($service)
- Write-ColorOutput " Found: $constructorName" "Green"
- }
- }
-
- if ($services.Count -eq 0) {
- Write-ColorOutput "No service constructors found matching pattern 'NewXxxService'!" "Red"
- exit 1
- }
-
- Write-ColorOutput "`nTotal services discovered: $($services.Count)" "Blue"
- return $services
-}
-
-function Get-TopologicalOrder {
- param([System.Collections.Generic.List[ServiceInfo]]$Services)
-
- Write-ColorOutput "`nBuilding dependency graph..." "Cyan"
-
- # Build adjacency list
- $graph = @{}
- $inDegree = @{}
- $domainToService = @{}
-
- foreach ($svc in $Services) {
- $graph[$svc.Domain] = [System.Collections.Generic.List[string]]::new()
- $inDegree[$svc.Domain] = 0
- $domainToService[$svc.Domain] = $svc
- }
-
- # Build edges
- foreach ($svc in $Services) {
- foreach ($dep in $svc.ServiceDependencies) {
- if ($graph.ContainsKey($dep)) {
- $graph[$dep].Add($svc.Domain)
- $inDegree[$svc.Domain]++
- }
- }
- }
-
- # Kahn's algorithm for topological sort
- $queue = [System.Collections.Generic.Queue[string]]::new()
- foreach ($domain in $inDegree.Keys) {
- if ($inDegree[$domain] -eq 0) {
- $queue.Enqueue($domain)
- }
- }
-
- $sorted = [System.Collections.Generic.List[string]]::new()
-
- while ($queue.Count -gt 0) {
- $current = $queue.Dequeue()
- $sorted.Add($current)
-
- foreach ($neighbor in $graph[$current]) {
- $inDegree[$neighbor]--
- if ($inDegree[$neighbor] -eq 0) {
- $queue.Enqueue($neighbor)
- }
- }
- }
-
- # Check for cycles
- if ($sorted.Count -ne $Services.Count) {
- $remaining = $inDegree.Keys | Where-Object { $inDegree[$_] -gt 0 }
- Write-ColorOutput "`nERROR: Circular dependency detected!" "Red"
- Write-ColorOutput "Services involved in cycle: $($remaining -join ', ')" "Yellow"
- exit 1
- }
-
- Write-ColorOutput " Dependency graph validated (no cycles)" "Green"
- Write-ColorOutput " Topological order: $($sorted -join ' -> ')" "Blue"
-
- # Return services in topological order
- return $sorted | ForEach-Object { $domainToService[$_] }
-}
-
-function Resolve-ConstructorArgument {
- param([Parameter]$Param)
-
- $type = $Param.NormalizedType
-
- # SPECIAL CASE MAPPINGS - Add more here as needed
- # ============================================
-
- # 1. JWT secret string
- if ($Param.RawType -eq "string" -and $Param.Name -match "secret|key") {
- return "configProvider.ProvideJWTConfig().GetSecretKey()"
- }
-
- # 2. Repository pattern: XxxxRepository -> repoProvider.ProvideXxxxRepository()
- if ($type -match '^(.+)Repository$') {
- $repoName = $matches[1]
- return "repoProvider.Provide${repoName}Repository()"
- }
-
- # 3. Service pattern: XxxxService -> use variable (will be constructed before this)
- if ($type -match 'Service$') {
- return (Get-LowerCamelCase $type)
- }
-
- # ADD MORE SPECIAL CASES HERE:
- # --------------------------------------------
- # Example: Mail config
- # if ($type -eq "MailConfig") {
- # return "configProvider.ProvideMailConfig()"
- # }
- #
- # Example: Redis client
- # if ($type -eq "RedisClient") {
- # return "configProvider.ProvideRedisClient()"
- # }
- # --------------------------------------------
-
- # 4. Fallback: unresolved type
- return "/* TODO: provide $($Param.RawType) */"
-}
-
-function Generate-ProviderCode {
- param([System.Collections.Generic.List[ServiceInfo]]$ServicesInOrder)
-
- Write-ColorOutput "`nGenerating provider code..." "Cyan"
-
- $sb = [System.Text.StringBuilder]::new()
- [void]$sb.AppendLine("package provider")
- [void]$sb.AppendLine()
- [void]$sb.AppendLine("import `"$ModulePath`"")
- [void]$sb.AppendLine()
-
- # Interface
- [void]$sb.AppendLine("type ServicesProvider interface {")
- foreach ($svc in $ServicesInOrder) {
- $line = "`tProvide$($svc.Domain)() services.$($svc.Domain)"
- [void]$sb.AppendLine($line)
- }
- [void]$sb.AppendLine("}")
- [void]$sb.AppendLine()
-
- # Struct
- [void]$sb.AppendLine("type servicesProvider struct {")
- foreach ($svc in $ServicesInOrder) {
- $line = "`t$($svc.VarName) services.$($svc.Domain)"
- [void]$sb.AppendLine($line)
- }
- [void]$sb.AppendLine("}")
- [void]$sb.AppendLine()
-
- # Constructor
- [void]$sb.AppendLine("func NewServicesProvider(repoProvider RepositoriesProvider, configProvider ConfigProvider) ServicesProvider {")
-
- # Initialize services in topological order
- foreach ($svc in $ServicesInOrder) {
- $args = @()
- foreach ($param in $svc.Parameters) {
- $args += Resolve-ConstructorArgument $param
- }
- $argsStr = $args -join ", "
- $line = "`t$($svc.VarName) := services.$($svc.ConstructorName)($argsStr)"
- [void]$sb.AppendLine($line)
- }
-
- [void]$sb.AppendLine()
- [void]$sb.AppendLine("`treturn &servicesProvider{")
- foreach ($svc in $ServicesInOrder) {
- $line = "`t`t$($svc.VarName): $($svc.VarName),"
- [void]$sb.AppendLine($line)
- }
- [void]$sb.AppendLine("`t}")
- [void]$sb.AppendLine("}")
- [void]$sb.AppendLine()
-
- # Getter methods
- foreach ($svc in $ServicesInOrder) {
- [void]$sb.AppendLine("func (s *servicesProvider) Provide$($svc.Domain)() services.$($svc.Domain) {")
- [void]$sb.AppendLine("`treturn s.$($svc.VarName)")
- [void]$sb.AppendLine("}")
- [void]$sb.AppendLine()
- }
-
- return $sb.ToString()
-}
-
-function Write-ProviderFile {
- param([string]$Code, [string]$OutputPath)
-
- Write-ColorOutput "Writing to $OutputPath..." "Cyan"
-
- # Ensure directory exists
- $dir = Split-Path $OutputPath -Parent
- if ($dir -and -not (Test-Path $dir)) {
- New-Item -ItemType Directory -Path $dir -Force | Out-Null
- }
-
- # Write file as UTF-8 without BOM
- $utf8NoBom = [System.Text.UTF8Encoding]::new($false)
- [System.IO.File]::WriteAllText($OutputPath, $Code, $utf8NoBom)
-
- Write-ColorOutput " Successfully generated $OutputPath" "Green"
-}
-
-# ============================================
-# MAIN EXECUTION
-# ============================================
-
-try {
- Write-ColorOutput "`n=========================================" "Blue"
- Write-ColorOutput " Go Service Dependency Injector v1.0" "Blue"
- Write-ColorOutput "=========================================`n" "Blue"
-
- # Step 1: Parse all service constructors
- $services = Parse-GoFiles -Directory $ServicesDir
-
- # Step 2: Perform topological sort
- $sortedServices = Get-TopologicalOrder -Services $services
-
- # Step 3: Generate provider code
- $code = Generate-ProviderCode -ServicesInOrder $sortedServices
-
- # Step 4: Write to file
- Write-ProviderFile -Code $code -OutputPath $OutputFile
-
- Write-ColorOutput "`nSUCCESS! Provider generated successfully.`n" "Green"
- Write-ColorOutput "Next steps:" "Cyan"
- Write-ColorOutput " 1. Review $OutputFile" "White"
- Write-ColorOutput " 2. Fill any /* TODO: provide ... */ placeholders" "White"
- Write-ColorOutput " 3. Run: go build ./provider" "White"
-
-} catch {
- Write-ColorOutput "`nERROR: $($_.Exception.Message)" "Red"
- Write-ColorOutput "Stack trace: $($_.ScriptStackTrace)" "Yellow"
- exit 1
+#Requires -Version 5.1
+<#
+.SYNOPSIS
+ Automatic Dependency Injection Generator for Go Services
+
+.DESCRIPTION
+ Scans ./services/ directory, discovers service constructors, resolves dependencies,
+ performs topological sorting, and generates provider/services_provider.go with full DI wiring.
+
+.EXAMPLE
+ .\service_injector.ps1
+
+.NOTES
+ - Works with PowerShell 5.1+ and PowerShell 7+
+ - No external dependencies required
+ - Supports multi-line constructor signatures
+ - Handles dependency cycles with clear error messages
+#>
+
+[CmdletBinding()]
+param()
+
+# Configuration
+$ServicesDir = "./services"
+$OutputFile = "provider/services_provider.go"
+$ModulePath = "abdanhafidz.com/go-boilerplate/services"
+
+# ANSI colors for better output (fallback to plain text if not supported)
+$script:UseColors = $Host.UI.SupportsVirtualTerminal
+function Write-ColorOutput {
+ param([string]$Message, [string]$Color = "White")
+ if ($script:UseColors) {
+ $colors = @{
+ "Green" = "`e[32m"; "Yellow" = "`e[33m"; "Red" = "`e[31m"
+ "Cyan" = "`e[36m"; "Blue" = "`e[34m"; "Reset" = "`e[0m"
+ }
+ Write-Host "$($colors[$Color])$Message$($colors['Reset'])"
+ } else {
+ Write-Host $Message
+ }
+}
+
+# Data structures
+class ServiceInfo {
+ [string]$ConstructorName # NewAccountService
+ [string]$Domain # AccountService
+ [string]$VarName # accountService
+ [System.Collections.Generic.List[Parameter]]$Parameters
+ [System.Collections.Generic.List[string]]$ServiceDependencies
+
+ ServiceInfo() {
+ $this.Parameters = [System.Collections.Generic.List[Parameter]]::new()
+ $this.ServiceDependencies = [System.Collections.Generic.List[string]]::new()
+ }
+}
+
+class Parameter {
+ [string]$Name
+ [string]$RawType
+ [string]$NormalizedType
+}
+
+function Get-LowerCamelCase {
+ param([string]$Text)
+ if ($Text.Length -eq 0) { return $Text }
+ return $Text.Substring(0, 1).ToLower() + $Text.Substring(1)
+}
+
+function Normalize-TypeName {
+ param([string]$TypeStr)
+
+ # Remove leading pointer
+ $cleaned = $TypeStr -replace '^\*+', ''
+
+ # Remove package prefix (everything before last dot)
+ if ($cleaned -match '\.([^.]+)$') {
+ $cleaned = $matches[1]
+ }
+
+ return $cleaned.Trim()
+}
+
+function Parse-GoFiles {
+ param([string]$Directory)
+
+ Write-ColorOutput "Scanning for service constructors in $Directory..." "Cyan"
+
+ if (-not (Test-Path $Directory)) {
+ Write-ColorOutput "ERROR: Directory '$Directory' not found!" "Red"
+ exit 1
+ }
+
+ $goFiles = Get-ChildItem -Path $Directory -Filter "*.go" -Recurse -File
+ $services = [System.Collections.Generic.List[ServiceInfo]]::new()
+
+ foreach ($file in $goFiles) {
+ $content = Get-Content $file.FullName -Raw
+
+ # Match function signatures (support multi-line)
+ # Pattern: func NewXxxService(...) XxxService
+ $pattern = '(?ms)func\s+(New[a-zA-Z0-9]+Service)\s*\(([^)]*)\)\s+([a-zA-Z0-9*_.]+Service)'
+ $matches = [regex]::Matches($content, $pattern)
+
+ foreach ($match in $matches) {
+ $constructorName = $match.Groups[1].Value
+ $paramsStr = $match.Groups[2].Value
+ $returnType = $match.Groups[3].Value
+
+ # Extract domain name (XxxService)
+ $domain = Normalize-TypeName $returnType
+ $varName = Get-LowerCamelCase $domain
+
+ $service = [ServiceInfo]::new()
+ $service.ConstructorName = $constructorName
+ $service.Domain = $domain
+ $service.VarName = $varName
+
+ # Parse parameters
+ if ($paramsStr.Trim() -ne "") {
+ # Split by comma, but be careful with nested types
+ $paramList = $paramsStr -split ',\s*(?![^<>]*>)'
+
+ foreach ($param in $paramList) {
+ $param = $param.Trim()
+ if ($param -eq "") { continue }
+
+ # Split into name and type
+ $parts = $param -split '\s+', 2
+
+ $p = [Parameter]::new()
+ if ($parts.Count -eq 2) {
+ $p.Name = $parts[0]
+ $p.RawType = $parts[1]
+ } elseif ($parts.Count -eq 1) {
+ # Anonymous parameter - synthesize name
+ $p.Name = "param$($service.Parameters.Count)"
+ $p.RawType = $parts[0]
+ } else {
+ continue
+ }
+
+ $p.NormalizedType = Normalize-TypeName $p.RawType
+ $service.Parameters.Add($p)
+
+ # Track service dependencies
+ if ($p.NormalizedType -match 'Service$') {
+ $service.ServiceDependencies.Add($p.NormalizedType)
+ }
+ }
+ }
+
+ $services.Add($service)
+ Write-ColorOutput " Found: $constructorName" "Green"
+ }
+ }
+
+ if ($services.Count -eq 0) {
+ Write-ColorOutput "No service constructors found matching pattern 'NewXxxService'!" "Red"
+ exit 1
+ }
+
+ Write-ColorOutput "`nTotal services discovered: $($services.Count)" "Blue"
+ return $services
+}
+
+function Get-TopologicalOrder {
+ param([System.Collections.Generic.List[ServiceInfo]]$Services)
+
+ Write-ColorOutput "`nBuilding dependency graph..." "Cyan"
+
+ # Build adjacency list
+ $graph = @{}
+ $inDegree = @{}
+ $domainToService = @{}
+
+ foreach ($svc in $Services) {
+ $graph[$svc.Domain] = [System.Collections.Generic.List[string]]::new()
+ $inDegree[$svc.Domain] = 0
+ $domainToService[$svc.Domain] = $svc
+ }
+
+ # Build edges
+ foreach ($svc in $Services) {
+ foreach ($dep in $svc.ServiceDependencies) {
+ if ($graph.ContainsKey($dep)) {
+ $graph[$dep].Add($svc.Domain)
+ $inDegree[$svc.Domain]++
+ }
+ }
+ }
+
+ # Kahn's algorithm for topological sort
+ $queue = [System.Collections.Generic.Queue[string]]::new()
+ foreach ($domain in $inDegree.Keys) {
+ if ($inDegree[$domain] -eq 0) {
+ $queue.Enqueue($domain)
+ }
+ }
+
+ $sorted = [System.Collections.Generic.List[string]]::new()
+
+ while ($queue.Count -gt 0) {
+ $current = $queue.Dequeue()
+ $sorted.Add($current)
+
+ foreach ($neighbor in $graph[$current]) {
+ $inDegree[$neighbor]--
+ if ($inDegree[$neighbor] -eq 0) {
+ $queue.Enqueue($neighbor)
+ }
+ }
+ }
+
+ # Check for cycles
+ if ($sorted.Count -ne $Services.Count) {
+ $remaining = $inDegree.Keys | Where-Object { $inDegree[$_] -gt 0 }
+ Write-ColorOutput "`nERROR: Circular dependency detected!" "Red"
+ Write-ColorOutput "Services involved in cycle: $($remaining -join ', ')" "Yellow"
+ exit 1
+ }
+
+ Write-ColorOutput " Dependency graph validated (no cycles)" "Green"
+ Write-ColorOutput " Topological order: $($sorted -join ' -> ')" "Blue"
+
+ # Return services in topological order
+ return $sorted | ForEach-Object { $domainToService[$_] }
+}
+
+function Resolve-ConstructorArgument {
+ param([Parameter]$Param)
+
+ $type = $Param.NormalizedType
+
+ # SPECIAL CASE MAPPINGS - Add more here as needed
+ # ============================================
+
+ # 1. JWT secret string
+ if ($Param.RawType -eq "string" -and $Param.Name -match "secret|key") {
+ return "configProvider.ProvideJWTConfig().GetSecretKey()"
+ }
+
+ # 2. Repository pattern: XxxxRepository -> repoProvider.ProvideXxxxRepository()
+ if ($type -match '^(.+)Repository$') {
+ $repoName = $matches[1]
+ return "repoProvider.Provide${repoName}Repository()"
+ }
+
+ # 3. Service pattern: XxxxService -> use variable (will be constructed before this)
+ if ($type -match 'Service$') {
+ return (Get-LowerCamelCase $type)
+ }
+
+ # ADD MORE SPECIAL CASES HERE:
+ # --------------------------------------------
+ # Example: Mail config
+ # if ($type -eq "MailConfig") {
+ # return "configProvider.ProvideMailConfig()"
+ # }
+ #
+ # Example: Redis client
+ # if ($type -eq "RedisClient") {
+ # return "configProvider.ProvideRedisClient()"
+ # }
+ # --------------------------------------------
+
+ # 4. Fallback: unresolved type
+ return "/* TODO: provide $($Param.RawType) */"
+}
+
+function Generate-ProviderCode {
+ param([System.Collections.Generic.List[ServiceInfo]]$ServicesInOrder)
+
+ Write-ColorOutput "`nGenerating provider code..." "Cyan"
+
+ $sb = [System.Text.StringBuilder]::new()
+ [void]$sb.AppendLine("package provider")
+ [void]$sb.AppendLine()
+ [void]$sb.AppendLine("import `"$ModulePath`"")
+ [void]$sb.AppendLine()
+
+ # Interface
+ [void]$sb.AppendLine("type ServicesProvider interface {")
+ foreach ($svc in $ServicesInOrder) {
+ $line = "`tProvide$($svc.Domain)() services.$($svc.Domain)"
+ [void]$sb.AppendLine($line)
+ }
+ [void]$sb.AppendLine("}")
+ [void]$sb.AppendLine()
+
+ # Struct
+ [void]$sb.AppendLine("type servicesProvider struct {")
+ foreach ($svc in $ServicesInOrder) {
+ $line = "`t$($svc.VarName) services.$($svc.Domain)"
+ [void]$sb.AppendLine($line)
+ }
+ [void]$sb.AppendLine("}")
+ [void]$sb.AppendLine()
+
+ # Constructor
+ [void]$sb.AppendLine("func NewServicesProvider(repoProvider RepositoriesProvider, configProvider ConfigProvider) ServicesProvider {")
+
+ # Initialize services in topological order
+ foreach ($svc in $ServicesInOrder) {
+ $args = @()
+ foreach ($param in $svc.Parameters) {
+ $args += Resolve-ConstructorArgument $param
+ }
+ $argsStr = $args -join ", "
+ $line = "`t$($svc.VarName) := services.$($svc.ConstructorName)($argsStr)"
+ [void]$sb.AppendLine($line)
+ }
+
+ [void]$sb.AppendLine()
+ [void]$sb.AppendLine("`treturn &servicesProvider{")
+ foreach ($svc in $ServicesInOrder) {
+ $line = "`t`t$($svc.VarName): $($svc.VarName),"
+ [void]$sb.AppendLine($line)
+ }
+ [void]$sb.AppendLine("`t}")
+ [void]$sb.AppendLine("}")
+ [void]$sb.AppendLine()
+
+ # Getter methods
+ foreach ($svc in $ServicesInOrder) {
+ [void]$sb.AppendLine("func (s *servicesProvider) Provide$($svc.Domain)() services.$($svc.Domain) {")
+ [void]$sb.AppendLine("`treturn s.$($svc.VarName)")
+ [void]$sb.AppendLine("}")
+ [void]$sb.AppendLine()
+ }
+
+ return $sb.ToString()
+}
+
+function Write-ProviderFile {
+ param([string]$Code, [string]$OutputPath)
+
+ Write-ColorOutput "Writing to $OutputPath..." "Cyan"
+
+ # Ensure directory exists
+ $dir = Split-Path $OutputPath -Parent
+ if ($dir -and -not (Test-Path $dir)) {
+ New-Item -ItemType Directory -Path $dir -Force | Out-Null
+ }
+
+ # Write file as UTF-8 without BOM
+ $utf8NoBom = [System.Text.UTF8Encoding]::new($false)
+ [System.IO.File]::WriteAllText($OutputPath, $Code, $utf8NoBom)
+
+ Write-ColorOutput " Successfully generated $OutputPath" "Green"
+}
+
+# ============================================
+# MAIN EXECUTION
+# ============================================
+
+try {
+ Write-ColorOutput "`n=========================================" "Blue"
+ Write-ColorOutput " Go Service Dependency Injector v1.0" "Blue"
+ Write-ColorOutput "=========================================`n" "Blue"
+
+ # Step 1: Parse all service constructors
+ $services = Parse-GoFiles -Directory $ServicesDir
+
+ # Step 2: Perform topological sort
+ $sortedServices = Get-TopologicalOrder -Services $services
+
+ # Step 3: Generate provider code
+ $code = Generate-ProviderCode -ServicesInOrder $sortedServices
+
+ # Step 4: Write to file
+ Write-ProviderFile -Code $code -OutputPath $OutputFile
+
+ Write-ColorOutput "`nSUCCESS! Provider generated successfully.`n" "Green"
+ Write-ColorOutput "Next steps:" "Cyan"
+ Write-ColorOutput " 1. Review $OutputFile" "White"
+ Write-ColorOutput " 2. Fill any /* TODO: provide ... */ placeholders" "White"
+ Write-ColorOutput " 3. Run: go build ./provider" "White"
+
+} catch {
+ Write-ColorOutput "`nERROR: $($_.Exception.Message)" "Red"
+ Write-ColorOutput "Stack trace: $($_.ScriptStackTrace)" "Yellow"
+ exit 1
}
\ No newline at end of file
diff --git a/go.mod b/go.mod
index a28d089bdf73c91b38b62a21881f3e77988f4981..adc5a18df9cba1ce3eaa21a0e1aafa17f95085e4 100644
--- a/go.mod
+++ b/go.mod
@@ -57,6 +57,7 @@ require (
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.55.0 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
+ github.com/supabase-community/storage-go v0.8.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
diff --git a/go.sum b/go.sum
index 87c9b9e43c65b37edbb554e810a10cf01e497128..ccf69bad99ce45e08f7dccbd7aa9d5aba81d0464 100644
--- a/go.sum
+++ b/go.sum
@@ -140,6 +140,8 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
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/supabase-community/storage-go v0.8.1 h1:EwD0vr+ADBIjBWH8G69AxWuvdFhifv64cfE/sjRky6I=
+github.com/supabase-community/storage-go v0.8.1/go.mod h1:oBKcJf5rcUXy3Uj9eS5wR6mvpwbmvkjOtAA+4tGcdvQ=
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.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
diff --git a/middleware/authentication_middleware.go b/middleware/authentication_middleware.go
index 565671be35cc517fba08e3eae2c82b160df4905d..14b81e38d75ff9357fb2872088eaa7a7f03d180a 100644
--- a/middleware/authentication_middleware.go
+++ b/middleware/authentication_middleware.go
@@ -1,48 +1,48 @@
-package middleware
-
-import (
- "errors"
- "fmt"
- "strings"
-
- http_error "abdanhafidz.com/go-boilerplate/models/error"
- "abdanhafidz.com/go-boilerplate/services"
- utils "abdanhafidz.com/go-boilerplate/utils"
- "github.com/gin-gonic/gin"
-)
-
-type AuthenticationMiddleware interface {
- VerifyAccount(ctx *gin.Context)
-}
-type authenticationMiddleware struct {
- jwtService services.JWTService
-}
-
-func NewAuthenticationMiddleware(jwtService services.JWTService) AuthenticationMiddleware {
- return &authenticationMiddleware{
- jwtService: jwtService,
- }
-}
-func (m *authenticationMiddleware) VerifyAccount(c *gin.Context) {
-
- authorizationBearer := c.Request.Header["Authorization"]
-
- if authorizationBearer != nil {
- token := strings.Split(authorizationBearer[0], " ")[1]
- claim, err := m.jwtService.ValidateToken(c.Request.Context(), token)
-
- if err != nil && errors.Is(err, http_error.INVALID_TOKEN) {
- utils.ResponseFAILED(c, claim, http_error.INVALID_TOKEN)
- c.Abort()
- return
- }
- fmt.Println("Claims:", claim)
- c.Set("account_id", claim.AccountId)
- c.Next()
-
- } else {
- utils.ResponseFAILED(c, "Empty Token", http_error.UNAUTHORIZED)
- return
- }
-
-}
+package middleware
+
+import (
+ "errors"
+ "fmt"
+ "strings"
+
+ http_error "abdanhafidz.com/go-boilerplate/models/error"
+ "abdanhafidz.com/go-boilerplate/services"
+ utils "abdanhafidz.com/go-boilerplate/utils"
+ "github.com/gin-gonic/gin"
+)
+
+type AuthenticationMiddleware interface {
+ VerifyAccount(ctx *gin.Context)
+}
+type authenticationMiddleware struct {
+ jwtService services.JWTService
+}
+
+func NewAuthenticationMiddleware(jwtService services.JWTService) AuthenticationMiddleware {
+ return &authenticationMiddleware{
+ jwtService: jwtService,
+ }
+}
+func (m *authenticationMiddleware) VerifyAccount(c *gin.Context) {
+
+ authorizationBearer := c.Request.Header["Authorization"]
+
+ if authorizationBearer != nil {
+ token := strings.Split(authorizationBearer[0], " ")[1]
+ claim, err := m.jwtService.ValidateToken(c.Request.Context(), token)
+
+ if err != nil && errors.Is(err, http_error.INVALID_TOKEN) {
+ utils.ResponseFAILED(c, claim, http_error.INVALID_TOKEN)
+ c.Abort()
+ return
+ }
+ fmt.Println("Claims:", claim)
+ c.Set("account_id", claim.AccountId)
+ c.Next()
+
+ } else {
+ utils.ResponseFAILED(c, "Empty Token", http_error.UNAUTHORIZED)
+ return
+ }
+
+}
diff --git a/middleware/authorization_middleware.go b/middleware/authorization_middleware.go
index 9b615480e8f601d2165c359d97aca2096f5aaf43..80aca719928878262e11e23c1f08066b9fb4d8c7 100644
--- a/middleware/authorization_middleware.go
+++ b/middleware/authorization_middleware.go
@@ -1,43 +1,43 @@
-package middleware
-
-import (
- http_error "abdanhafidz.com/go-boilerplate/models/error"
- "abdanhafidz.com/go-boilerplate/services"
- utils "abdanhafidz.com/go-boilerplate/utils"
- "github.com/gin-gonic/gin"
- "github.com/google/uuid"
-)
-
-type AuthorizationMiddleware interface {
- AuthorizeUserToEvent(ctx *gin.Context)
-}
-type authorizationMiddleware struct {
- eventService services.EventService
-}
-
-func NewAuthorizationMiddleware(eventService services.EventService) AuthorizationMiddleware {
- return &authorizationMiddleware{
- eventService: eventService,
- }
-}
-
-func (m *authorizationMiddleware) AuthorizeUserToEvent(c *gin.Context) {
-
- eventSlug := c.Param("slug")
- accountId, exists := c.Get("account_id")
- if !exists {
- utils.ResponseFAILED(c, eventSlug, http_error.DATA_NOT_FOUND)
- c.Abort()
- return
- }
-
- err := m.eventService.AuthorizeUserToEvent(c.Request.Context(), eventSlug, accountId.(uuid.UUID))
-
- if err != nil {
- utils.ResponseFAILED(c, eventSlug, err)
- c.Abort()
- return
- }
-
- c.Next()
-}
+package middleware
+
+import (
+ http_error "abdanhafidz.com/go-boilerplate/models/error"
+ "abdanhafidz.com/go-boilerplate/services"
+ utils "abdanhafidz.com/go-boilerplate/utils"
+ "github.com/gin-gonic/gin"
+ "github.com/google/uuid"
+)
+
+type AuthorizationMiddleware interface {
+ AuthorizeUserToEvent(ctx *gin.Context)
+}
+type authorizationMiddleware struct {
+ eventService services.EventService
+}
+
+func NewAuthorizationMiddleware(eventService services.EventService) AuthorizationMiddleware {
+ return &authorizationMiddleware{
+ eventService: eventService,
+ }
+}
+
+func (m *authorizationMiddleware) AuthorizeUserToEvent(c *gin.Context) {
+
+ eventSlug := c.Param("slug")
+ accountId, exists := c.Get("account_id")
+ if !exists {
+ utils.ResponseFAILED(c, eventSlug, http_error.NOT_FOUND_ERROR)
+ c.Abort()
+ return
+ }
+
+ err := m.eventService.AuthorizeUserToEvent(c.Request.Context(), eventSlug, accountId.(uuid.UUID))
+
+ if err != nil {
+ utils.ResponseFAILED(c, eventSlug, err)
+ c.Abort()
+ return
+ }
+
+ c.Next()
+}
diff --git a/middleware/middleware.go b/middleware/middleware.go
index 7ddf445b6250424f14f0025a76de0a8c274a18e5..c870d7c16494f4f207bca74baee2f43b298750a3 100644
--- a/middleware/middleware.go
+++ b/middleware/middleware.go
@@ -1 +1 @@
-package middleware
+package middleware
diff --git a/models/dto/academy_dto.go b/models/dto/academy_dto.go
index 82ebbde9e0fc786759b0ab0fb8d4c0763a7dd445..2c463481d9c189f27ce5ac796695aecd2a15dc00 100644
--- a/models/dto/academy_dto.go
+++ b/models/dto/academy_dto.go
@@ -1,30 +1,30 @@
-package dto
-
-import "github.com/google/uuid"
-
-type CreateAcademyRequest struct {
- Title string `json:"title" binding:"required"`
- Slug string `json:"slug"`
- Description string `json:"description"`
- ImageUrl string `json:"image_url"`
-}
-
-type UpdateAcademyRequest struct {
- Title string `json:"title"`
- Slug string `json:"slug"`
- Description string `json:"description"`
- ImageUrl string `json:"image_url"`
-}
-
-type CreateMaterialRequest struct {
- AcademyId uuid.UUID `json:"academy_id" binding:"required"`
- Title string `json:"title" binding:"required"`
- Slug string `json:"slug"`
- Description string `json:"description"`
-}
-
-type CreateContentRequest struct {
- MaterialId uuid.UUID `json:"material_id" binding:"required"`
- Title string `json:"title" binding:"required"`
- Contents string `json:"contents"`
-}
+package dto
+
+import "github.com/google/uuid"
+
+type CreateAcademyRequest struct {
+ Title string `json:"title" binding:"required"`
+ Slug string `json:"slug"`
+ Description string `json:"description"`
+ ImageUrl string `json:"image_url"`
+}
+
+type UpdateAcademyRequest struct {
+ Title string `json:"title"`
+ Slug string `json:"slug"`
+ Description string `json:"description"`
+ ImageUrl string `json:"image_url"`
+}
+
+type CreateMaterialRequest struct {
+ AcademyId uuid.UUID `json:"academy_id" binding:"required"`
+ Title string `json:"title" binding:"required"`
+ Slug string `json:"slug"`
+ Description string `json:"description"`
+}
+
+type CreateContentRequest struct {
+ MaterialId uuid.UUID `json:"material_id" binding:"required"`
+ Title string `json:"title" binding:"required"`
+ Contents string `json:"contents"`
+}
diff --git a/models/dto/account_details_dto.go b/models/dto/account_details_dto.go
index 731ed69f005e5ebbe0de730b8edf08e44be21937..a3b7ae55a9b3dff0a57112d8e91fe733bc1d15b9 100644
--- a/models/dto/account_details_dto.go
+++ b/models/dto/account_details_dto.go
@@ -1,19 +1,19 @@
-package dto
-
-import (
- entity "abdanhafidz.com/go-boilerplate/models/entity"
-)
-
-type AccountDetailResponse struct {
- Account entity.Account `json:"account"`
- Details entity.AccountDetail `json:"details"`
-}
-
-type UpdateAccountDetailRequest struct {
- FullName *string `json:"full_name"`
- SchoolName *string `json:"school_name"`
- Province *string `json:"province"`
- City *string `json:"city"`
- Avatar *string `json:"avatar"`
- PhoneNumber *string `json:"phone_number"`
-}
+package dto
+
+import (
+ entity "abdanhafidz.com/go-boilerplate/models/entity"
+)
+
+type AccountDetailResponse struct {
+ Account entity.Account `json:"account"`
+ Details entity.AccountDetail `json:"details"`
+}
+
+type UpdateAccountDetailRequest struct {
+ FullName *string `json:"full_name"`
+ SchoolName *string `json:"school_name"`
+ Province *string `json:"province"`
+ City *string `json:"city"`
+ Avatar *string `json:"avatar"`
+ PhoneNumber *string `json:"phone_number"`
+}
diff --git a/models/dto/event_dto.go b/models/dto/event_dto.go
index 5e266af0c7f23aa33eafaa35e46a32762b55750e..a9ebbcad3a136a1913a191c1e8a5e2add441ac4b 100644
--- a/models/dto/event_dto.go
+++ b/models/dto/event_dto.go
@@ -1,20 +1,20 @@
-package dto
-
-import (
- entity "abdanhafidz.com/go-boilerplate/models/entity"
-)
-
-type EventDetailResponse struct {
- Data *entity.Events
- RegisterStatus int `json:"register_status" binding:"required"`
-}
-
-type JoinEventRequest struct {
- EventCode string `json:"event_code" binding:"required"`
-}
-
-type EventStatus struct {
- IsHasNotStarted bool
- IsOnGoing bool
- IsFinished bool
-}
+package dto
+
+import (
+ entity "abdanhafidz.com/go-boilerplate/models/entity"
+)
+
+type EventDetailResponse struct {
+ Data *entity.Events
+ RegisterStatus int `json:"register_status" binding:"required"`
+}
+
+type JoinEventRequest struct {
+ EventCode string `json:"event_code" binding:"required"`
+}
+
+type EventStatus struct {
+ IsHasNotStarted bool
+ IsOnGoing bool
+ IsFinished bool
+}
diff --git a/models/dto/exam_dto.go b/models/dto/exam_dto.go
index 52d44b0854ebbc9a3e779ee57be340c6fb0764d9..5d70a29c0778800ad5333ee36a9d1ca8eccf2e5c 100644
--- a/models/dto/exam_dto.go
+++ b/models/dto/exam_dto.go
@@ -1,23 +1,23 @@
-package dto
-
-import (
- entity "abdanhafidz.com/go-boilerplate/models/entity"
- "github.com/google/uuid"
-)
-
-type UserExamStatus struct {
- IsNotAttempt bool
- IsOnAttempt bool
- IsSubmitted bool
- IsTimeOut bool
-}
-
-type AnswerWithQuestion struct {
- Answer entity.ExamEventAnswer `json:"answer"`
- Question entity.Questions `json:"question"`
-}
-
-type AnswerExamEventRequest struct {
- QuestionId uuid.UUID `json:"question_id" binding:"required"`
- Answer []string `json:"answer"`
-}
+package dto
+
+import (
+ entity "abdanhafidz.com/go-boilerplate/models/entity"
+ "github.com/google/uuid"
+)
+
+type UserExamStatus struct {
+ IsNotAttempt bool
+ IsOnAttempt bool
+ IsSubmitted bool
+ IsTimeOut bool
+}
+
+type AnswerWithQuestion struct {
+ Answer entity.ExamEventAnswer `json:"answer"`
+ Question entity.Questions `json:"question"`
+}
+
+type AnswerExamEventRequest struct {
+ QuestionId uuid.UUID `json:"question_id" binding:"required"`
+ Answer []string `json:"answer"`
+}
diff --git a/models/dto/option_dto.go b/models/dto/option_dto.go
index bfa023eb7ffcfee3e7d6dd0c59ca1549106000c6..3a73f473e3b459b132d37133bf18d8cf5686cd6c 100644
--- a/models/dto/option_dto.go
+++ b/models/dto/option_dto.go
@@ -1,12 +1,12 @@
-package dto
-
-import entity "abdanhafidz.com/go-boilerplate/models/entity"
-
-type OptionsRequest struct {
- OptionName string `json:"option_name" binding:"required"`
- OptionValue []string `json:"option_values" binding:"required"`
-}
-
-type OptionsResponse struct {
- Options []entity.Options `json:"options"`
-}
+package dto
+
+import entity "abdanhafidz.com/go-boilerplate/models/entity"
+
+type OptionsRequest struct {
+ OptionName string `json:"option_name" binding:"required"`
+ OptionValue []string `json:"option_values" binding:"required"`
+}
+
+type OptionsResponse struct {
+ Options []entity.Options `json:"options"`
+}
diff --git a/models/dto/upload_dto.go b/models/dto/upload_dto.go
new file mode 100644
index 0000000000000000000000000000000000000000..984ee8baa53af974135dbba119e5a1964d895e8a
--- /dev/null
+++ b/models/dto/upload_dto.go
@@ -0,0 +1,43 @@
+package dto
+
+import (
+ // Gunakan path yang SAMA PERSIS dengan yang ada di OptionsRequest
+ entity "abdanhafidz.com/go-boilerplate/models/entity"
+
+ "github.com/google/uuid"
+ "time"
+)
+
+type FileResponse struct {
+ Id uuid.UUID `json:"id"`
+ OriginalName string `json:"original_name"`
+ URL string `json:"url"`
+ MimeType string `json:"mime_type"`
+ Size int64 `json:"size"`
+ CreatedAt time.Time `json:"created_at"`
+}
+
+type FileUploadResponse struct {
+ Status string `json:"status"`
+ Message string `json:"message"`
+ Data []FileResponse `json:"data"`
+}
+
+type FileResponseSingle struct {
+ Status string `json:"status"`
+ Message string `json:"message"`
+ Data FileResponse `json:"data"`
+}
+
+func FormatFileResponse(f *entity.File, baseURL string) FileResponse {
+ fullURL := baseURL + f.Path
+
+ return FileResponse{
+ Id: f.Id,
+ OriginalName: f.OriginalName,
+ URL: fullURL,
+ MimeType: f.MimeType,
+ Size: f.Size,
+ CreatedAt: f.CreatedAt,
+ }
+}
\ No newline at end of file
diff --git a/models/entity/contant.go b/models/entity/contant.go
new file mode 100644
index 0000000000000000000000000000000000000000..bee6b70110540f24ef9e81647e6311d1411b23d5
--- /dev/null
+++ b/models/entity/contant.go
@@ -0,0 +1,7 @@
+package models
+
+const (
+ StatusNotStarted = "NOT_STARTED"
+ StatusInProgress = "IN_PROGRESS"
+ StatusCompleted = "COMPLETED"
+)
\ No newline at end of file
diff --git a/models/entity/entity.go b/models/entity/entity.go
index 6d36b5a6cacf83b2672e80f5cffd9c50ac8680ca..1e6aaafa5c29ec320865d525483a5d0cf9bfce9b 100644
--- a/models/entity/entity.go
+++ b/models/entity/entity.go
@@ -332,3 +332,18 @@ type AcademyContentProgress struct {
}
func (AcademyContentProgress) TableName() string { return "academy_content_progress" }
+
+type File struct {
+ Id uuid.UUID `gorm:"type:uuid;primary_key;default:uuid_generate_v4()" json:"id"`
+ OriginalName string `json:"original_name,omitempty"`
+ StoredName string `json:"stored_name,omitempty"`
+ MimeType string `json:"mime_type,omitempty"`
+ Size int64 `json:"size,omitempty"`
+ Path string `json:"path,omitempty"`
+ Context string `json:"context,omitempty"`
+ AccountId uuid.UUID `json:"account_id,omitempty"`
+ CreatedAt time.Time `json:"created_at,omitempty"`
+ Account *Account `gorm:"foreignKey:AccountId" json:"account,omitempty"`
+}
+
+func (File) TableName() string { return "files" }
\ No newline at end of file
diff --git a/models/error/error.go b/models/error/error.go
index 6f3fed66396d34a202f1f5c5f8edde98a82f1644..6b5e89d1e8145c9a548d6df0a9b7d9fd5ff8254d 100644
--- a/models/error/error.go
+++ b/models/error/error.go
@@ -3,25 +3,49 @@ package http_error
import "errors"
var (
- BAD_REQUEST_ERROR = errors.New("Invalid Request Format !")
- INTERNAL_SERVER_ERROR = errors.New("Internal Server Error!")
- UNAUTHORIZED = errors.New("Unauthorized, you don't have permission to access this service!")
- DATA_NOT_FOUND = errors.New("There is not data with given credential / given parameter!")
- TIMEOUT = errors.New("Server took to long respond!")
- EXISTING_ACCOUNT = errors.New("There is existing account!")
- INVALID_TOKEN = errors.New("Invalid Authentication Payload!")
- DUPLICATE_DATA = errors.New("Duplicate data !")
- ACCOUNT_NOT_FOUND = errors.New("There is no account with given credential!")
- WRONG_PASSWORD = errors.New("Your password is wrong for given account credential, please recheck!")
- INVALID_ACCOUNT_DIGITS = errors.New("Your account 3 digits is not found in account number data")
- EXPIRED_TOKEN = errors.New("Token expired")
+ // ================= GENERAL =================
+ BAD_REQUEST_ERROR = errors.New("Invalid Request Format !")
+ INTERNAL_SERVER_ERROR = errors.New("Internal Server Error!")
+ TIMEOUT = errors.New("Server took to long respond!")
+ NOT_FOUND_ERROR = errors.New("Resource not found.")
+ DUPLICATE_DATA = errors.New("Duplicate data !")
+ INVALID_DATA_PAYLOAD = errors.New("Invalid data payload provided.")
+
+ // ================= AUTH & ACCOUNT =================
+ UNAUTHORIZED = errors.New("Unauthorized, you don't have permission to access this service!")
+ EXISTING_ACCOUNT = errors.New("There is existing account!")
+ INVALID_TOKEN = errors.New("Invalid Authentication Payload!")
+ ACCOUNT_NOT_FOUND = errors.New("There is no account with given credential!")
+ WRONG_PASSWORD = errors.New("Your password is wrong for given account credential, please recheck!")
+ INVALID_ACCOUNT_DIGITS = errors.New("Your account 3 digits is not found in account number data")
+ EXPIRED_TOKEN = errors.New("Token expired")
+ INVALID_OTP = errors.New("Invalid OTP Code")
+ EMAIL_ALREADY_EXISTS = errors.New("Email already registered")
+
+ // ================= EVENT & EXAM =================
ALREADY_REGISTERED_TO_EVENT = errors.New("Account already registered to this event")
- EMAIL_ALREADY_EXISTS = errors.New("Email already registered")
NOT_REGISTERED_TO_EVENT = errors.New("Account is not registered to this event")
- INVALID_OTP = errors.New("Invalid OTP Code")
ERR_PROBLEM_SET_NOT_FOUND = errors.New("problem set not found")
ERR_QUESTION_NOT_FOUND = errors.New("question not found")
EVENT_FINISHED = errors.New("The event has ended, you were disallowed to do the exam!")
EVENT_NOT_STARTED = errors.New("Take it easy, event hasn't starting yet! you cannot do the exam!")
EXAMS_SUBMITTED = errors.New("You've submitted the exam, you were diasallowed to answer the question!")
-)
+
+ // ================= FILE UPLOAD =================
+ FILE_TOO_LARGE = errors.New("File size exceeds the maximum limit!")
+ INVALID_FILE_TYPE = errors.New("File type is not permitted for the selected context.")
+ UPLOAD_FAILED = errors.New("Failed to upload file to storage provider.")
+ PARTIAL_UPLOAD_FAILURE = errors.New("Some files failed validation or upload.")
+
+ // ================= ACADEMY =================
+ TITLE_REQUIRED = errors.New("Title cannot be empty!")
+ SLUG_REQUIRED = errors.New("Slug cannot be empty!")
+ ACADEMY_ID_REQUIRED = errors.New("Academy ID is required!")
+ MATERIAL_ID_REQUIRED = errors.New("Material ID is required!")
+
+ ACADEMY_NOT_FOUND = errors.New("Academy not found!")
+ MATERIAL_NOT_FOUND = errors.New("Material not found!")
+ CONTENT_NOT_FOUND = errors.New("Content not found!")
+ ACADEMY_HAS_MATERIALS = errors.New("Cannot delete academy because it still has materials!")
+ MATERIAL_HAS_CONTENTS = errors.New("Cannot delete material because it still has contents!")
+)
\ No newline at end of file
diff --git a/provider/config_provider.go b/provider/config_provider.go
index b9959d69cb41998989b59343baac2b9e86da57cb..6132186d47a5359074d46436a8f9234091551814 100644
--- a/provider/config_provider.go
+++ b/provider/config_provider.go
@@ -1,38 +1,59 @@
-package provider
-
-import "abdanhafidz.com/go-boilerplate/config"
-
-type ConfigProvider interface {
- ProvideJWTConfig() config.JWTConfig
- ProvideEnvConfig() config.EnvConfig
- ProvideDatabaseConfig() config.DatabaseConfig
-}
-
-type configProvider struct {
- jWTConfig config.JWTConfig
- envConfig config.EnvConfig
- databaseConfig config.DatabaseConfig
-}
-
-func NewConfigProvider() ConfigProvider {
- envConfig := config.NewEnvConfig("Asia/Jakarta")
- jWTConfig := config.NewJWTConfig(envConfig.GetSalt())
- databaseConfig := config.NewDatabaseConfig(envConfig.GetDatabaseHost(), envConfig.GetDatabaseUser(), envConfig.GetDatabasePassword(), envConfig.GetDatabaseName(), envConfig.GetDatabasePort())
- return &configProvider{
- jWTConfig: jWTConfig,
- envConfig: envConfig,
- databaseConfig: databaseConfig,
- }
-}
-
-func (c *configProvider) ProvideJWTConfig() config.JWTConfig {
- return c.jWTConfig
-}
-
-func (c *configProvider) ProvideEnvConfig() config.EnvConfig {
- return c.envConfig
-}
-
-func (c *configProvider) ProvideDatabaseConfig() config.DatabaseConfig {
- return c.databaseConfig
-}
+package provider
+
+import "abdanhafidz.com/go-boilerplate/config"
+
+type ConfigProvider interface {
+ ProvideJWTConfig() config.JWTConfig
+ ProvideEnvConfig() config.EnvConfig
+ ProvideDatabaseConfig() config.DatabaseConfig
+ ProvideSupabaseConfig() config.SupabaseConfig
+}
+
+type configProvider struct {
+ jWTConfig config.JWTConfig
+ envConfig config.EnvConfig
+ databaseConfig config.DatabaseConfig
+ supabaseConfig config.SupabaseConfig
+}
+
+func NewConfigProvider() ConfigProvider {
+ envConfig := config.NewEnvConfig("Asia/Jakarta")
+ jWTConfig := config.NewJWTConfig(envConfig.GetSalt())
+
+ databaseConfig := config.NewDatabaseConfig(
+ envConfig.GetDatabaseHost(),
+ envConfig.GetDatabaseUser(),
+ envConfig.GetDatabasePassword(),
+ envConfig.GetDatabaseName(),
+ envConfig.GetDatabasePort(),
+ )
+
+ supabaseConfig := config.NewSupabaseConfig(
+ envConfig.GetSupabaseURL(),
+ envConfig.GetSupabaseKey(),
+ envConfig.GetSupabaseBucket(),
+ )
+
+ return &configProvider{
+ jWTConfig: jWTConfig,
+ envConfig: envConfig,
+ databaseConfig: databaseConfig,
+ supabaseConfig: supabaseConfig,
+ }
+}
+
+func (c *configProvider) ProvideJWTConfig() config.JWTConfig {
+ return c.jWTConfig
+}
+
+func (c *configProvider) ProvideEnvConfig() config.EnvConfig {
+ return c.envConfig
+}
+
+func (c *configProvider) ProvideDatabaseConfig() config.DatabaseConfig {
+ return c.databaseConfig
+}
+
+func (c *configProvider) ProvideSupabaseConfig() config.SupabaseConfig {
+ return c.supabaseConfig
+}
\ No newline at end of file
diff --git a/provider/controller_provider.go b/provider/controller_provider.go
index e72ef634ecf5774d4a6cf16abd6c868e6d2c54ae..6a5ba54e909b119d73f73e2406f67bc5299c9eb9 100644
--- a/provider/controller_provider.go
+++ b/provider/controller_provider.go
@@ -1,90 +1,106 @@
-package provider
-
-import "abdanhafidz.com/go-boilerplate/controllers"
-
-type ControllerProvider interface {
- ProvideAcademyController() controllers.AcademyController
- ProvideAccountDetailController() controllers.AccountDetailController
- ProvideAuthenticationController() controllers.AuthenticationController
- ProvideEmailVerificationController() controllers.EmailVerificationController
- ProvideEventController() controllers.EventController
- ProvideExamController() controllers.ExamController
- ProvideForgotPasswordController() controllers.ForgotPasswordController
- ProvideOptionController() controllers.OptionController
- ProvideRegionController() controllers.RegionController
-}
-
-type controllerProvider struct {
- academyController controllers.AcademyController
- accountDetailController controllers.AccountDetailController
- authenticationController controllers.AuthenticationController
- emailVerificationController controllers.EmailVerificationController
- eventController controllers.EventController
- examController controllers.ExamController
- forgotPasswordController controllers.ForgotPasswordController
- optionController controllers.OptionController
- regionController controllers.RegionController
-}
-
-func NewControllerProvider(servicesProvider ServicesProvider) ControllerProvider {
-
- academyController := controllers.NewAcademyController(servicesProvider.ProvideAcademyService())
- accountDetailController := controllers.NewAccountDetailController(servicesProvider.ProvideAccountService())
- authenticationController := controllers.NewAuthenticationController(servicesProvider.ProvideAccountService(), servicesProvider.ProvideExternalAuthService())
- emailVerificationController := controllers.NewEmailVerificationController(servicesProvider.ProvideEmailVerificationService())
- eventController := controllers.NewEventController(servicesProvider.ProvideEventService())
- examController := controllers.NewExamController(servicesProvider.ProvideExamService())
- forgotPasswordController := controllers.NewForgotPasswordController(servicesProvider.ProvideForgotPasswordService())
- optionController := controllers.NewOptionController(servicesProvider.ProvideOptionService())
- regionController := controllers.NewRegionController(servicesProvider.ProvideRegionService())
- return &controllerProvider{
- academyController: academyController,
- accountDetailController: accountDetailController,
- authenticationController: authenticationController,
- emailVerificationController: emailVerificationController,
- eventController: eventController,
- examController: examController,
- forgotPasswordController: forgotPasswordController,
- optionController: optionController,
- regionController: regionController,
- }
-}
-
-// --- Getter Methods ---
-
-func (c *controllerProvider) ProvideAcademyController() controllers.AcademyController {
- return c.academyController
-}
-
-func (c *controllerProvider) ProvideAccountDetailController() controllers.AccountDetailController {
- return c.accountDetailController
-}
-
-func (c *controllerProvider) ProvideAuthenticationController() controllers.AuthenticationController {
- return c.authenticationController
-}
-
-func (c *controllerProvider) ProvideEmailVerificationController() controllers.EmailVerificationController {
- return c.emailVerificationController
-}
-
-func (c *controllerProvider) ProvideEventController() controllers.EventController {
- return c.eventController
-}
-
-func (c *controllerProvider) ProvideExamController() controllers.ExamController {
- return c.examController
-}
-
-func (c *controllerProvider) ProvideForgotPasswordController() controllers.ForgotPasswordController {
- return c.forgotPasswordController
-}
-
-func (c *controllerProvider) ProvideOptionController() controllers.OptionController {
- return c.optionController
-}
-
-func (c *controllerProvider) ProvideRegionController() controllers.RegionController {
- return c.regionController
-}
-
+package provider
+
+import "abdanhafidz.com/go-boilerplate/controllers"
+
+type ControllerProvider interface {
+ ProvideAcademyController() controllers.AcademyController
+ ProvideAccountDetailController() controllers.AccountDetailController
+ ProvideAuthenticationController() controllers.AuthenticationController
+ ProvideEmailVerificationController() controllers.EmailVerificationController
+ ProvideEventController() controllers.EventController
+ ProvideExamController() controllers.ExamController
+ ProvideForgotPasswordController() controllers.ForgotPasswordController
+ ProvideOptionController() controllers.OptionController
+ ProvideRegionController() controllers.RegionController
+
+ // UPDATE: Menggunakan Pointer (*)
+ ProvideUploadController() *controllers.UploadController
+}
+
+type controllerProvider struct {
+ academyController controllers.AcademyController
+ accountDetailController controllers.AccountDetailController
+ authenticationController controllers.AuthenticationController
+ emailVerificationController controllers.EmailVerificationController
+ eventController controllers.EventController
+ examController controllers.ExamController
+ forgotPasswordController controllers.ForgotPasswordController
+ optionController controllers.OptionController
+ regionController controllers.RegionController
+
+ // UPDATE: Menggunakan Pointer (*)
+ uploadController *controllers.UploadController
+}
+
+func NewControllerProvider(servicesProvider ServicesProvider) ControllerProvider {
+
+ academyController := controllers.NewAcademyController(servicesProvider.ProvideAcademyService())
+ accountDetailController := controllers.NewAccountDetailController(servicesProvider.ProvideAccountService())
+ authenticationController := controllers.NewAuthenticationController(servicesProvider.ProvideAccountService(), servicesProvider.ProvideExternalAuthService())
+ emailVerificationController := controllers.NewEmailVerificationController(servicesProvider.ProvideEmailVerificationService())
+ eventController := controllers.NewEventController(servicesProvider.ProvideEventService())
+ examController := controllers.NewExamController(servicesProvider.ProvideExamService())
+ forgotPasswordController := controllers.NewForgotPasswordController(servicesProvider.ProvideForgotPasswordService())
+ optionController := controllers.NewOptionController(servicesProvider.ProvideOptionService())
+ regionController := controllers.NewRegionController(servicesProvider.ProvideRegionService())
+
+ // UPDATE: Inisialisasi Upload Controller
+ // servicesProvider.ProvideUploadService() sekarang sudah return Pointer (*), jadi aman.
+ uploadController := controllers.NewUploadController(servicesProvider.ProvideUploadService())
+
+ return &controllerProvider{
+ academyController: academyController,
+ accountDetailController: accountDetailController,
+ authenticationController: authenticationController,
+ emailVerificationController: emailVerificationController,
+ eventController: eventController,
+ examController: examController,
+ forgotPasswordController: forgotPasswordController,
+ optionController: optionController,
+ regionController: regionController,
+ uploadController: uploadController, // Pointer assign ke Pointer
+ }
+}
+
+// --- Getter Methods ---
+
+func (c *controllerProvider) ProvideAcademyController() controllers.AcademyController {
+ return c.academyController
+}
+
+func (c *controllerProvider) ProvideAccountDetailController() controllers.AccountDetailController {
+ return c.accountDetailController
+}
+
+func (c *controllerProvider) ProvideAuthenticationController() controllers.AuthenticationController {
+ return c.authenticationController
+}
+
+func (c *controllerProvider) ProvideEmailVerificationController() controllers.EmailVerificationController {
+ return c.emailVerificationController
+}
+
+func (c *controllerProvider) ProvideEventController() controllers.EventController {
+ return c.eventController
+}
+
+func (c *controllerProvider) ProvideExamController() controllers.ExamController {
+ return c.examController
+}
+
+func (c *controllerProvider) ProvideForgotPasswordController() controllers.ForgotPasswordController {
+ return c.forgotPasswordController
+}
+
+func (c *controllerProvider) ProvideOptionController() controllers.OptionController {
+ return c.optionController
+}
+
+func (c *controllerProvider) ProvideRegionController() controllers.RegionController {
+ return c.regionController
+}
+
+// UPDATE: Return Pointer (*)
+func (c *controllerProvider) ProvideUploadController() *controllers.UploadController {
+ return c.uploadController
+}
\ No newline at end of file
diff --git a/provider/middleware_provider.go b/provider/middleware_provider.go
index e4bf3e9046d7a55c3de97dee184e55b452c6a17f..4cd7c152595d2b8501d23feb7fc11e548e99b21d 100644
--- a/provider/middleware_provider.go
+++ b/provider/middleware_provider.go
@@ -1,30 +1,30 @@
-package provider
-
-import "abdanhafidz.com/go-boilerplate/middleware"
-
-type MiddlewareProvider interface {
- ProvideAuthenticationMiddleware() middleware.AuthenticationMiddleware
- ProvideAuthorizationMiddleware() middleware.AuthorizationMiddleware
-}
-
-type middlewareProvider struct {
- authenticationMiddleware middleware.AuthenticationMiddleware
- authorizationMiddleware middleware.AuthorizationMiddleware
-}
-
-func NewMiddlewareProvider(servicesProvider ServicesProvider) MiddlewareProvider {
- authenticationMiddleware := middleware.NewAuthenticationMiddleware(servicesProvider.ProvideJWTService())
- authorizationMiddleware := middleware.NewAuthorizationMiddleware(servicesProvider.ProvideEventService())
- return &middlewareProvider{
- authenticationMiddleware: authenticationMiddleware,
- authorizationMiddleware: authorizationMiddleware,
- }
-}
-
-func (p *middlewareProvider) ProvideAuthenticationMiddleware() middleware.AuthenticationMiddleware {
- return p.authenticationMiddleware
-}
-
-func (p *middlewareProvider) ProvideAuthorizationMiddleware() middleware.AuthorizationMiddleware {
- return p.authorizationMiddleware
-}
+package provider
+
+import "abdanhafidz.com/go-boilerplate/middleware"
+
+type MiddlewareProvider interface {
+ ProvideAuthenticationMiddleware() middleware.AuthenticationMiddleware
+ ProvideAuthorizationMiddleware() middleware.AuthorizationMiddleware
+}
+
+type middlewareProvider struct {
+ authenticationMiddleware middleware.AuthenticationMiddleware
+ authorizationMiddleware middleware.AuthorizationMiddleware
+}
+
+func NewMiddlewareProvider(servicesProvider ServicesProvider) MiddlewareProvider {
+ authenticationMiddleware := middleware.NewAuthenticationMiddleware(servicesProvider.ProvideJWTService())
+ authorizationMiddleware := middleware.NewAuthorizationMiddleware(servicesProvider.ProvideEventService())
+ return &middlewareProvider{
+ authenticationMiddleware: authenticationMiddleware,
+ authorizationMiddleware: authorizationMiddleware,
+ }
+}
+
+func (p *middlewareProvider) ProvideAuthenticationMiddleware() middleware.AuthenticationMiddleware {
+ return p.authenticationMiddleware
+}
+
+func (p *middlewareProvider) ProvideAuthorizationMiddleware() middleware.AuthorizationMiddleware {
+ return p.authorizationMiddleware
+}
diff --git a/provider/provider.go b/provider/provider.go
index 4ee49f13f049b034382d403585ddc986a5e8b887..30de4ab30a3db053cfb92fbf14356971f8991dc1 100644
--- a/provider/provider.go
+++ b/provider/provider.go
@@ -1,87 +1,102 @@
package provider
import (
- entity "abdanhafidz.com/go-boilerplate/models/entity"
- "github.com/gin-gonic/gin"
+ "log"
+ entity "abdanhafidz.com/go-boilerplate/models/entity"
+ "github.com/gin-gonic/gin"
)
type AppProvider interface {
- ProvideRouter() *gin.Engine
- ProvideConfig() ConfigProvider
- ProvideRepositories() RepositoriesProvider
- ProvideServices() ServicesProvider
- ProvideControllers() ControllerProvider
- ProvideMiddlewares() MiddlewareProvider
+ ProvideRouter() *gin.Engine
+ ProvideConfig() ConfigProvider
+ ProvideRepositories() RepositoriesProvider
+ ProvideServices() ServicesProvider
+ ProvideControllers() ControllerProvider
+ ProvideMiddlewares() MiddlewareProvider
}
+
type appProvider struct {
- ginRouter *gin.Engine
- configProvider ConfigProvider
- repositoriesProvider RepositoriesProvider
- servicesProvider ServicesProvider
- controllerProvider ControllerProvider
- middlewareProvider MiddlewareProvider
+ ginRouter *gin.Engine
+ configProvider ConfigProvider
+ repositoriesProvider RepositoriesProvider
+ servicesProvider ServicesProvider
+ controllerProvider ControllerProvider
+ middlewareProvider MiddlewareProvider
}
func NewAppProvider() AppProvider {
- ginRouter := gin.Default()
- configProvider := NewConfigProvider()
- repositoriesProvider := NewRepositoriesProvider(configProvider)
- servicesProvider := NewServicesProvider(repositoriesProvider, configProvider)
- controllerProvider := NewControllerProvider(servicesProvider)
- middlewareProvider := NewMiddlewareProvider(servicesProvider)
- configProvider.ProvideDatabaseConfig().AutoMigrateAll(
- // Accounts & Auth
- &entity.Account{},
- &entity.AccountDetail{},
- &entity.EmailVerification{},
- &entity.ExternalAuth{},
- &entity.FCM{},
- &entity.ForgotPassword{},
-
- // Events
- &entity.Events{},
- &entity.EventAssign{},
- &entity.Announcement{},
-
- // Problemset & Exam
- &entity.ProblemSet{},
- &entity.Questions{},
- &entity.Exam{},
- &entity.ProblemSetExamAssign{},
- &entity.ExamEventAssign{},
-
- // Exam Attempt & Result
- &entity.ExamEventAnswer{},
- &entity.ExamEventAttempt{},
- &entity.Result{},
-
- // Academy LMS
- &entity.Academy{},
- &entity.AcademyMaterial{},
- &entity.AcademyContent{},
- &entity.AcademyMaterialProgress{},
- &entity.AcademyContentProgress{},
- &entity.AcademyProgress{},
-
- // Options & Regions
- &entity.OptionCategory{},
- &entity.OptionValues{},
- &entity.RegionProvince{},
- &entity.RegionCity{},
- )
-
- return &appProvider{
- ginRouter: ginRouter,
- configProvider: configProvider,
- repositoriesProvider: repositoriesProvider,
- servicesProvider: servicesProvider,
- controllerProvider: controllerProvider,
- middlewareProvider: middlewareProvider,
- }
+ ginRouter := gin.Default()
+ configProvider := NewConfigProvider()
+ repositoriesProvider := NewRepositoriesProvider(configProvider)
+ supabaseCfg := configProvider.ProvideSupabaseConfig()
+ storageDriver := NewSupabaseStorage(supabaseCfg.URL, supabaseCfg.ServiceKey, supabaseCfg.BucketName)
+ servicesProvider := NewServicesProvider(repositoriesProvider, configProvider, storageDriver)
+ controllerProvider := NewControllerProvider(servicesProvider)
+ middlewareProvider := NewMiddlewareProvider(servicesProvider)
+
+ // Database Migrations with error handling
+ err := configProvider.ProvideDatabaseConfig().AutoMigrateAll(
+ // Accounts & Auth
+ &entity.Account{},
+ &entity.AccountDetail{},
+ &entity.EmailVerification{},
+ &entity.ExternalAuth{},
+ &entity.FCM{},
+ &entity.ForgotPassword{},
+
+ // Events
+ &entity.Events{},
+ &entity.EventAssign{},
+ &entity.Announcement{},
+
+ // Problemset & Exam
+ &entity.ProblemSet{},
+ &entity.Questions{},
+ &entity.Exam{},
+ &entity.ProblemSetExamAssign{},
+ &entity.ExamEventAssign{},
+
+ // Exam Attempt & Result
+ &entity.ExamEventAnswer{},
+ &entity.ExamEventAttempt{},
+ &entity.Result{},
+
+ // Academy LMS
+ &entity.Academy{},
+ &entity.AcademyMaterial{},
+ &entity.AcademyContent{},
+ &entity.AcademyMaterialProgress{},
+ &entity.AcademyContentProgress{},
+ &entity.AcademyProgress{},
+
+ // Options & Regions
+ &entity.OptionCategory{},
+ &entity.OptionValues{},
+ &entity.RegionProvince{},
+ &entity.RegionCity{},
+
+ // Files Storage
+ &entity.File{},
+ )
+
+ if err != nil {
+ log.Fatalf("Database migration failed: %v", err)
+ }
+
+ return &appProvider{
+ ginRouter: ginRouter,
+ configProvider: configProvider,
+ repositoriesProvider: repositoriesProvider,
+ servicesProvider: servicesProvider,
+ controllerProvider: controllerProvider,
+ middlewareProvider: middlewareProvider,
+ }
}
+
func (a *appProvider) ProvideRouter() *gin.Engine {
return a.ginRouter
}
+
func (a *appProvider) ProvideConfig() ConfigProvider {
return a.configProvider
}
@@ -100,4 +115,4 @@ func (a *appProvider) ProvideControllers() ControllerProvider {
func (a *appProvider) ProvideMiddlewares() MiddlewareProvider {
return a.middlewareProvider
-}
+}
\ No newline at end of file
diff --git a/provider/repositories_provider.go b/provider/repositories_provider.go
index 5e90251b6f6f8bd2ad0b827d79ca7e56f39d5094..9d3dcc729cfca00f510e8fba9ac0a679452fa5bc 100644
--- a/provider/repositories_provider.go
+++ b/provider/repositories_provider.go
@@ -1,170 +1,178 @@
-package provider
-
-import "abdanhafidz.com/go-boilerplate/repositories"
-
-type RepositoriesProvider interface {
- ProvideAcademyRepository() repositories.AcademyRepository
- ProvideAccountDetailRepository() repositories.AccountDetailRepository
- ProvideAccountRepository() repositories.AccountRepository
- ProvideEmailVerificationRepository() repositories.EmailVerificationRepository
- ProvideEventAssignRepository() repositories.EventAssignRepository
- ProvideEventsRepository() repositories.EventsRepository
- ProvideExamEventAnswerRepository() repositories.ExamEventAnswerRepository
- ProvideExamEventAssignRepository() repositories.ExamEventAssignRepository
- ProvideExamEventAttemptRepository() repositories.ExamEventAttemptRepository
- ProvideExamRepository() repositories.ExamRepository
- ProvideExternalAuthRepository() repositories.ExternalAuthRepository
- ProvideFCMRepository() repositories.FCMRepository
- ProvideForgotPasswordRepository() repositories.ForgotPasswordRepository
- ProvideOptionRepository() repositories.OptionRepository
- ProvideProblemSetExamAssignRepository() repositories.ProblemSetExamAssignRepository
- ProvideProblemSetRepository() repositories.ProblemSetRepository
- ProvideQuestionsRepository() repositories.QuestionsRepository
- ProvideRegionRepository() repositories.RegionRepository
- ProvideResultRepository() repositories.ResultRepository
-}
-
-type repositoriesProvider struct {
- academyRepository repositories.AcademyRepository
- accountDetailRepository repositories.AccountDetailRepository
- accountRepository repositories.AccountRepository
- emailVerificationRepository repositories.EmailVerificationRepository
- eventAssignRepository repositories.EventAssignRepository
- eventsRepository repositories.EventsRepository
- examEventAnswerRepository repositories.ExamEventAnswerRepository
- examEventAssignRepository repositories.ExamEventAssignRepository
- examEventAttemptRepository repositories.ExamEventAttemptRepository
- examRepository repositories.ExamRepository
- externalAuthRepository repositories.ExternalAuthRepository
- fCMRepository repositories.FCMRepository
- forgotPasswordRepository repositories.ForgotPasswordRepository
- optionRepository repositories.OptionRepository
- problemSetExamAssignRepository repositories.ProblemSetExamAssignRepository
- problemSetRepository repositories.ProblemSetRepository
- questionsRepository repositories.QuestionsRepository
- regionRepository repositories.RegionRepository
- resultRepository repositories.ResultRepository
-}
-
-func NewRepositoriesProvider(cfg ConfigProvider) RepositoriesProvider {
- dbConfig := cfg.ProvideDatabaseConfig()
- db := dbConfig.GetInstance()
-
- academyRepository := repositories.NewAcademyRepository(db)
- accountDetailRepository := repositories.NewAccountDetailRepository(db)
- accountRepository := repositories.NewAccountRepository(db)
- emailVerificationRepository := repositories.NewEmailVerificationRepository(db)
- eventAssignRepository := repositories.NewEventAssignRepository(db)
- eventsRepository := repositories.NewEventsRepository(db)
- examEventAnswerRepository := repositories.NewExamEventAnswerRepository(db)
- examEventAssignRepository := repositories.NewExamEventAssignRepository(db)
- examEventAttemptRepository := repositories.NewExamEventAttemptRepository(db)
- examRepository := repositories.NewExamRepository(db)
- externalAuthRepository := repositories.NewExternalAuthRepository(db)
- fCMRepository := repositories.NewFCMRepository(db)
- forgotPasswordRepository := repositories.NewForgotPasswordRepository(db)
- optionRepository := repositories.NewOptionRepository(db)
- problemSetExamAssignRepository := repositories.NewProblemSetExamAssignRepository(db)
- problemSetRepository := repositories.NewProblemSetRepository(db)
- questionsRepository := repositories.NewQuestionsRepository(db)
- regionRepository := repositories.NewRegionRepository(db)
- resultRepository := repositories.NewResultRepository(db)
-
- return &repositoriesProvider{
- academyRepository: academyRepository,
- accountDetailRepository: accountDetailRepository,
- accountRepository: accountRepository,
- emailVerificationRepository: emailVerificationRepository,
- eventAssignRepository: eventAssignRepository,
- eventsRepository: eventsRepository,
- examEventAnswerRepository: examEventAnswerRepository,
- examEventAssignRepository: examEventAssignRepository,
- examEventAttemptRepository: examEventAttemptRepository,
- examRepository: examRepository,
- externalAuthRepository: externalAuthRepository,
- fCMRepository: fCMRepository,
- forgotPasswordRepository: forgotPasswordRepository,
- optionRepository: optionRepository,
- problemSetExamAssignRepository: problemSetExamAssignRepository,
- problemSetRepository: problemSetRepository,
- questionsRepository: questionsRepository,
- regionRepository: regionRepository,
- resultRepository: resultRepository,
- }
-}
-
-func (r *repositoriesProvider) ProvideAcademyRepository() repositories.AcademyRepository {
- return r.academyRepository
-}
-
-func (r *repositoriesProvider) ProvideAccountDetailRepository() repositories.AccountDetailRepository {
- return r.accountDetailRepository
-}
-
-func (r *repositoriesProvider) ProvideAccountRepository() repositories.AccountRepository {
- return r.accountRepository
-}
-
-func (r *repositoriesProvider) ProvideEmailVerificationRepository() repositories.EmailVerificationRepository {
- return r.emailVerificationRepository
-}
-
-func (r *repositoriesProvider) ProvideEventAssignRepository() repositories.EventAssignRepository {
- return r.eventAssignRepository
-}
-
-func (r *repositoriesProvider) ProvideEventsRepository() repositories.EventsRepository {
- return r.eventsRepository
-}
-
-func (r *repositoriesProvider) ProvideExamEventAnswerRepository() repositories.ExamEventAnswerRepository {
- return r.examEventAnswerRepository
-}
-
-func (r *repositoriesProvider) ProvideExamEventAssignRepository() repositories.ExamEventAssignRepository {
- return r.examEventAssignRepository
-}
-
-func (r *repositoriesProvider) ProvideExamEventAttemptRepository() repositories.ExamEventAttemptRepository {
- return r.examEventAttemptRepository
-}
-
-func (r *repositoriesProvider) ProvideExamRepository() repositories.ExamRepository {
- return r.examRepository
-}
-
-func (r *repositoriesProvider) ProvideExternalAuthRepository() repositories.ExternalAuthRepository {
- return r.externalAuthRepository
-}
-
-func (r *repositoriesProvider) ProvideFCMRepository() repositories.FCMRepository {
- return r.fCMRepository
-}
-
-func (r *repositoriesProvider) ProvideForgotPasswordRepository() repositories.ForgotPasswordRepository {
- return r.forgotPasswordRepository
-}
-
-func (r *repositoriesProvider) ProvideOptionRepository() repositories.OptionRepository {
- return r.optionRepository
-}
-
-func (r *repositoriesProvider) ProvideProblemSetExamAssignRepository() repositories.ProblemSetExamAssignRepository {
- return r.problemSetExamAssignRepository
-}
-
-func (r *repositoriesProvider) ProvideProblemSetRepository() repositories.ProblemSetRepository {
- return r.problemSetRepository
-}
-
-func (r *repositoriesProvider) ProvideQuestionsRepository() repositories.QuestionsRepository {
- return r.questionsRepository
-}
-
-func (r *repositoriesProvider) ProvideRegionRepository() repositories.RegionRepository {
- return r.regionRepository
-}
-
-func (r *repositoriesProvider) ProvideResultRepository() repositories.ResultRepository {
- return r.resultRepository
-}
+package provider
+
+import "abdanhafidz.com/go-boilerplate/repositories"
+
+type RepositoriesProvider interface {
+ ProvideAcademyRepository() repositories.AcademyRepository
+ ProvideAccountDetailRepository() repositories.AccountDetailRepository
+ ProvideAccountRepository() repositories.AccountRepository
+ ProvideEmailVerificationRepository() repositories.EmailVerificationRepository
+ ProvideEventAssignRepository() repositories.EventAssignRepository
+ ProvideEventsRepository() repositories.EventsRepository
+ ProvideExamEventAnswerRepository() repositories.ExamEventAnswerRepository
+ ProvideExamEventAssignRepository() repositories.ExamEventAssignRepository
+ ProvideExamEventAttemptRepository() repositories.ExamEventAttemptRepository
+ ProvideExamRepository() repositories.ExamRepository
+ ProvideExternalAuthRepository() repositories.ExternalAuthRepository
+ ProvideFCMRepository() repositories.FCMRepository
+ ProvideForgotPasswordRepository() repositories.ForgotPasswordRepository
+ ProvideOptionRepository() repositories.OptionRepository
+ ProvideProblemSetExamAssignRepository() repositories.ProblemSetExamAssignRepository
+ ProvideProblemSetRepository() repositories.ProblemSetRepository
+ ProvideQuestionsRepository() repositories.QuestionsRepository
+ ProvideRegionRepository() repositories.RegionRepository
+ ProvideResultRepository() repositories.ResultRepository
+ ProvideFileRepository() repositories.FileRepository
+}
+
+type repositoriesProvider struct {
+ academyRepository repositories.AcademyRepository
+ accountDetailRepository repositories.AccountDetailRepository
+ accountRepository repositories.AccountRepository
+ emailVerificationRepository repositories.EmailVerificationRepository
+ eventAssignRepository repositories.EventAssignRepository
+ eventsRepository repositories.EventsRepository
+ examEventAnswerRepository repositories.ExamEventAnswerRepository
+ examEventAssignRepository repositories.ExamEventAssignRepository
+ examEventAttemptRepository repositories.ExamEventAttemptRepository
+ examRepository repositories.ExamRepository
+ externalAuthRepository repositories.ExternalAuthRepository
+ fCMRepository repositories.FCMRepository
+ forgotPasswordRepository repositories.ForgotPasswordRepository
+ optionRepository repositories.OptionRepository
+ problemSetExamAssignRepository repositories.ProblemSetExamAssignRepository
+ problemSetRepository repositories.ProblemSetRepository
+ questionsRepository repositories.QuestionsRepository
+ regionRepository repositories.RegionRepository
+ resultRepository repositories.ResultRepository
+ fileRepository repositories.FileRepository // Added field
+}
+
+func NewRepositoriesProvider(cfg ConfigProvider) RepositoriesProvider {
+ dbConfig := cfg.ProvideDatabaseConfig()
+ db := dbConfig.GetInstance()
+
+ academyRepository := repositories.NewAcademyRepository(db)
+ accountDetailRepository := repositories.NewAccountDetailRepository(db)
+ accountRepository := repositories.NewAccountRepository(db)
+ emailVerificationRepository := repositories.NewEmailVerificationRepository(db)
+ eventAssignRepository := repositories.NewEventAssignRepository(db)
+ eventsRepository := repositories.NewEventsRepository(db)
+ examEventAnswerRepository := repositories.NewExamEventAnswerRepository(db)
+ examEventAssignRepository := repositories.NewExamEventAssignRepository(db)
+ examEventAttemptRepository := repositories.NewExamEventAttemptRepository(db)
+ examRepository := repositories.NewExamRepository(db)
+ externalAuthRepository := repositories.NewExternalAuthRepository(db)
+ fCMRepository := repositories.NewFCMRepository(db)
+ forgotPasswordRepository := repositories.NewForgotPasswordRepository(db)
+ optionRepository := repositories.NewOptionRepository(db)
+ problemSetExamAssignRepository := repositories.NewProblemSetExamAssignRepository(db)
+ problemSetRepository := repositories.NewProblemSetRepository(db)
+ questionsRepository := repositories.NewQuestionsRepository(db)
+ regionRepository := repositories.NewRegionRepository(db)
+ resultRepository := repositories.NewResultRepository(db)
+ fileRepository := repositories.NewFileRepository(db) // Init here
+
+ return &repositoriesProvider{
+ academyRepository: academyRepository,
+ accountDetailRepository: accountDetailRepository,
+ accountRepository: accountRepository,
+ emailVerificationRepository: emailVerificationRepository,
+ eventAssignRepository: eventAssignRepository,
+ eventsRepository: eventsRepository,
+ examEventAnswerRepository: examEventAnswerRepository,
+ examEventAssignRepository: examEventAssignRepository,
+ examEventAttemptRepository: examEventAttemptRepository,
+ examRepository: examRepository,
+ externalAuthRepository: externalAuthRepository,
+ fCMRepository: fCMRepository,
+ forgotPasswordRepository: forgotPasswordRepository,
+ optionRepository: optionRepository,
+ problemSetExamAssignRepository: problemSetExamAssignRepository,
+ problemSetRepository: problemSetRepository,
+ questionsRepository: questionsRepository,
+ regionRepository: regionRepository,
+ resultRepository: resultRepository,
+ fileRepository: fileRepository, // Assign here
+ }
+}
+
+func (r *repositoriesProvider) ProvideAcademyRepository() repositories.AcademyRepository {
+ return r.academyRepository
+}
+
+func (r *repositoriesProvider) ProvideAccountDetailRepository() repositories.AccountDetailRepository {
+ return r.accountDetailRepository
+}
+
+func (r *repositoriesProvider) ProvideAccountRepository() repositories.AccountRepository {
+ return r.accountRepository
+}
+
+func (r *repositoriesProvider) ProvideEmailVerificationRepository() repositories.EmailVerificationRepository {
+ return r.emailVerificationRepository
+}
+
+func (r *repositoriesProvider) ProvideEventAssignRepository() repositories.EventAssignRepository {
+ return r.eventAssignRepository
+}
+
+func (r *repositoriesProvider) ProvideEventsRepository() repositories.EventsRepository {
+ return r.eventsRepository
+}
+
+func (r *repositoriesProvider) ProvideExamEventAnswerRepository() repositories.ExamEventAnswerRepository {
+ return r.examEventAnswerRepository
+}
+
+func (r *repositoriesProvider) ProvideExamEventAssignRepository() repositories.ExamEventAssignRepository {
+ return r.examEventAssignRepository
+}
+
+func (r *repositoriesProvider) ProvideExamEventAttemptRepository() repositories.ExamEventAttemptRepository {
+ return r.examEventAttemptRepository
+}
+
+func (r *repositoriesProvider) ProvideExamRepository() repositories.ExamRepository {
+ return r.examRepository
+}
+
+func (r *repositoriesProvider) ProvideExternalAuthRepository() repositories.ExternalAuthRepository {
+ return r.externalAuthRepository
+}
+
+func (r *repositoriesProvider) ProvideFCMRepository() repositories.FCMRepository {
+ return r.fCMRepository
+}
+
+func (r *repositoriesProvider) ProvideForgotPasswordRepository() repositories.ForgotPasswordRepository {
+ return r.forgotPasswordRepository
+}
+
+func (r *repositoriesProvider) ProvideOptionRepository() repositories.OptionRepository {
+ return r.optionRepository
+}
+
+func (r *repositoriesProvider) ProvideProblemSetExamAssignRepository() repositories.ProblemSetExamAssignRepository {
+ return r.problemSetExamAssignRepository
+}
+
+func (r *repositoriesProvider) ProvideProblemSetRepository() repositories.ProblemSetRepository {
+ return r.problemSetRepository
+}
+
+func (r *repositoriesProvider) ProvideQuestionsRepository() repositories.QuestionsRepository {
+ return r.questionsRepository
+}
+
+func (r *repositoriesProvider) ProvideRegionRepository() repositories.RegionRepository {
+ return r.regionRepository
+}
+
+func (r *repositoriesProvider) ProvideResultRepository() repositories.ResultRepository {
+ return r.resultRepository
+}
+
+func (r *repositoriesProvider) ProvideFileRepository() repositories.FileRepository {
+ return r.fileRepository
+}
\ No newline at end of file
diff --git a/provider/services_provider.go b/provider/services_provider.go
index 5488475d4150b521fe911d9b9cdb396ad9a51571..c26c99d39456a9e6c04d9f62036c5bb612a0fd60 100644
--- a/provider/services_provider.go
+++ b/provider/services_provider.go
@@ -1,103 +1,121 @@
-package provider
-
-import "abdanhafidz.com/go-boilerplate/services"
-
-type ServicesProvider interface {
- ProvideEventService() services.EventService
- ProvideAcademyService() services.AcademyService
- ProvideProblemSetService() services.ProblemSetService
- ProvideJWTService() services.JWTService
- ProvideRegionService() services.RegionService
- ProvideOptionService() services.OptionService
- ProvideExamService() services.ExamService
- ProvideAccountService() services.AccountService
- ProvideForgotPasswordService() services.ForgotPasswordService
- ProvideEmailVerificationService() services.EmailVerificationService
- ProvideExternalAuthService() services.ExternalAuthService
-}
-
-type servicesProvider struct {
- eventService services.EventService
- academyService services.AcademyService
- problemSetService services.ProblemSetService
- jWTService services.JWTService
- regionService services.RegionService
- optionService services.OptionService
- examService services.ExamService
- accountService services.AccountService
- forgotPasswordService services.ForgotPasswordService
- emailVerificationService services.EmailVerificationService
- externalAuthService services.ExternalAuthService
-}
-
-func NewServicesProvider(repoProvider RepositoriesProvider, configProvider ConfigProvider) ServicesProvider {
- eventService := services.NewEventService(repoProvider.ProvideEventsRepository(), repoProvider.ProvideEventAssignRepository())
- academyService := services.NewAcademyService(repoProvider.ProvideAcademyRepository())
- problemSetService := services.NewProblemSetService(repoProvider.ProvideProblemSetRepository(), repoProvider.ProvideQuestionsRepository(), repoProvider.ProvideProblemSetExamAssignRepository())
- jWTService := services.NewJWTService(configProvider.ProvideJWTConfig().GetSecretKey())
- regionService := services.NewRegionService(repoProvider.ProvideRegionRepository())
- optionService := services.NewOptionService(repoProvider.ProvideOptionRepository())
- examService := services.NewExamService(eventService, problemSetService, repoProvider.ProvideProblemSetExamAssignRepository(), repoProvider.ProvideExamRepository(), repoProvider.ProvideExamEventAttemptRepository(), repoProvider.ProvideExamEventAssignRepository(), repoProvider.ProvideExamEventAnswerRepository(), repoProvider.ProvideResultRepository())
- accountService := services.NewAccountService(jWTService, repoProvider.ProvideAccountRepository(), repoProvider.ProvideAccountDetailRepository())
- forgotPasswordService := services.NewForgotPasswordService(jWTService, repoProvider.ProvideAccountRepository(), repoProvider.ProvideForgotPasswordRepository())
- emailVerificationService := services.NewEmailVerificationService(accountService, repoProvider.ProvideEmailVerificationRepository())
- externalAuthService := services.NewExternalAuthService(jWTService, accountService, repoProvider.ProvideExternalAuthRepository())
-
- return &servicesProvider{
- eventService: eventService,
- academyService: academyService,
- problemSetService: problemSetService,
- jWTService: jWTService,
- regionService: regionService,
- optionService: optionService,
- examService: examService,
- accountService: accountService,
- forgotPasswordService: forgotPasswordService,
- emailVerificationService: emailVerificationService,
- externalAuthService: externalAuthService,
- }
-}
-
-func (s *servicesProvider) ProvideEventService() services.EventService {
- return s.eventService
-}
-
-func (s *servicesProvider) ProvideAcademyService() services.AcademyService {
- return s.academyService
-}
-
-func (s *servicesProvider) ProvideProblemSetService() services.ProblemSetService {
- return s.problemSetService
-}
-
-func (s *servicesProvider) ProvideJWTService() services.JWTService {
- return s.jWTService
-}
-
-func (s *servicesProvider) ProvideRegionService() services.RegionService {
- return s.regionService
-}
-
-func (s *servicesProvider) ProvideOptionService() services.OptionService {
- return s.optionService
-}
-
-func (s *servicesProvider) ProvideExamService() services.ExamService {
- return s.examService
-}
-
-func (s *servicesProvider) ProvideAccountService() services.AccountService {
- return s.accountService
-}
-
-func (s *servicesProvider) ProvideForgotPasswordService() services.ForgotPasswordService {
- return s.forgotPasswordService
-}
-
-func (s *servicesProvider) ProvideEmailVerificationService() services.EmailVerificationService {
- return s.emailVerificationService
-}
-
-func (s *servicesProvider) ProvideExternalAuthService() services.ExternalAuthService {
- return s.externalAuthService
-}
+package provider
+
+import (
+ // Pastikan path ini sesuai dengan go.mod kamu (misal: quzuu-backend-v2/services)
+ "abdanhafidz.com/go-boilerplate/services"
+)
+
+type ServicesProvider interface {
+ ProvideEventService() services.EventService
+ ProvideAcademyService() services.AcademyService
+ ProvideProblemSetService() services.ProblemSetService
+ ProvideJWTService() services.JWTService
+ ProvideRegionService() services.RegionService
+ ProvideOptionService() services.OptionService
+ ProvideExamService() services.ExamService
+ ProvideAccountService() services.AccountService
+ ProvideForgotPasswordService() services.ForgotPasswordService
+ ProvideEmailVerificationService() services.EmailVerificationService
+ ProvideExternalAuthService() services.ExternalAuthService
+
+ // UPDATE: Mengembalikan pointer karena UploadService adalah struct, bukan interface
+ ProvideUploadService() *services.UploadService
+}
+
+type servicesProvider struct {
+ eventService services.EventService
+ academyService services.AcademyService
+ problemSetService services.ProblemSetService
+ jWTService services.JWTService
+ regionService services.RegionService
+ optionService services.OptionService
+ examService services.ExamService
+ accountService services.AccountService
+ forgotPasswordService services.ForgotPasswordService
+ emailVerificationService services.EmailVerificationService
+ externalAuthService services.ExternalAuthService
+
+ // Field untuk menyimpan instance UploadService
+ uploadService *services.UploadService
+}
+
+func NewServicesProvider(
+ repoProvider RepositoriesProvider,
+ configProvider ConfigProvider,
+ storageProvider services.StorageProvider, // Didapat dari main/wire
+) ServicesProvider {
+
+ // Inisialisasi service lain...
+ eventService := services.NewEventService(repoProvider.ProvideEventsRepository(), repoProvider.ProvideEventAssignRepository())
+ academyService := services.NewAcademyService(repoProvider.ProvideAcademyRepository())
+ problemSetService := services.NewProblemSetService(repoProvider.ProvideProblemSetRepository(), repoProvider.ProvideQuestionsRepository(), repoProvider.ProvideProblemSetExamAssignRepository())
+ jWTService := services.NewJWTService(configProvider.ProvideJWTConfig().GetSecretKey())
+ regionService := services.NewRegionService(repoProvider.ProvideRegionRepository())
+ optionService := services.NewOptionService(repoProvider.ProvideOptionRepository())
+ examService := services.NewExamService(eventService, problemSetService, repoProvider.ProvideProblemSetExamAssignRepository(), repoProvider.ProvideExamRepository(), repoProvider.ProvideExamEventAttemptRepository(), repoProvider.ProvideExamEventAssignRepository(), repoProvider.ProvideExamEventAnswerRepository(), repoProvider.ProvideResultRepository())
+ accountService := services.NewAccountService(jWTService, repoProvider.ProvideAccountRepository(), repoProvider.ProvideAccountDetailRepository())
+ forgotPasswordService := services.NewForgotPasswordService(jWTService, repoProvider.ProvideAccountRepository(), repoProvider.ProvideForgotPasswordRepository())
+ emailVerificationService := services.NewEmailVerificationService(accountService, repoProvider.ProvideEmailVerificationRepository())
+ externalAuthService := services.NewExternalAuthService(jWTService, accountService, repoProvider.ProvideExternalAuthRepository())
+
+
+ uploadService := services.NewUploadService(
+ storageProvider,
+ repoProvider.ProvideFileRepository(),
+ )
+
+ return &servicesProvider{
+ eventService: eventService,
+ academyService: academyService,
+ problemSetService: problemSetService,
+ jWTService: jWTService,
+ regionService: regionService,
+ optionService: optionService,
+ examService: examService,
+ accountService: accountService,
+ forgotPasswordService: forgotPasswordService,
+ emailVerificationService: emailVerificationService,
+ externalAuthService: externalAuthService,
+ uploadService: uploadService,
+ }
+}
+
+// ... (Getter method lainnya tetap sama) ...
+
+func (s *servicesProvider) ProvideEventService() services.EventService {
+ return s.eventService
+}
+func (s *servicesProvider) ProvideAcademyService() services.AcademyService {
+ return s.academyService
+}
+func (s *servicesProvider) ProvideProblemSetService() services.ProblemSetService {
+ return s.problemSetService
+}
+func (s *servicesProvider) ProvideJWTService() services.JWTService {
+ return s.jWTService
+}
+func (s *servicesProvider) ProvideRegionService() services.RegionService {
+ return s.regionService
+}
+func (s *servicesProvider) ProvideOptionService() services.OptionService {
+ return s.optionService
+}
+func (s *servicesProvider) ProvideExamService() services.ExamService {
+ return s.examService
+}
+func (s *servicesProvider) ProvideAccountService() services.AccountService {
+ return s.accountService
+}
+func (s *servicesProvider) ProvideForgotPasswordService() services.ForgotPasswordService {
+ return s.forgotPasswordService
+}
+func (s *servicesProvider) ProvideEmailVerificationService() services.EmailVerificationService {
+ return s.emailVerificationService
+}
+func (s *servicesProvider) ProvideExternalAuthService() services.ExternalAuthService {
+ return s.externalAuthService
+}
+
+func (s *servicesProvider) ProvideUploadService() *services.UploadService {
+ return s.uploadService
+}
\ No newline at end of file
diff --git a/provider/storage_interface.go b/provider/storage_interface.go
new file mode 100644
index 0000000000000000000000000000000000000000..8e69c3020d16fb808d04f304c2e4794c2f8ffba4
--- /dev/null
+++ b/provider/storage_interface.go
@@ -0,0 +1,13 @@
+package provider
+
+import (
+ "context"
+ "mime/multipart"
+)
+
+// StorageProvider defines the behavior for file operations
+type StorageProvider interface {
+ UploadFile(ctx context.Context, file multipart.File, header *multipart.FileHeader) (string, error)
+ GetFileURL(path string) (string, error)
+ DeleteFile(path string) error
+}
\ No newline at end of file
diff --git a/provider/supabase_storage.go b/provider/supabase_storage.go
new file mode 100644
index 0000000000000000000000000000000000000000..768867da16be95538a1221961d2212ced0feb30d
--- /dev/null
+++ b/provider/supabase_storage.go
@@ -0,0 +1,42 @@
+package provider
+
+import (
+ "context"
+ "fmt"
+ "io"
+
+ // Pastikan import library supabase client yang Anda pakai benar
+ storage_go "github.com/supabase-community/storage-go"
+)
+
+type SupabaseStorage struct {
+ client *storage_go.Client
+ bucketName string
+ url string
+}
+
+func NewSupabaseStorage(url string, key string, bucketName string) *SupabaseStorage {
+ client := storage_go.NewClient(url+"/storage/v1", key, nil)
+ return &SupabaseStorage{
+ client: client,
+ bucketName: bucketName,
+ url: url,
+ }
+}
+
+func (s *SupabaseStorage) UploadFile(ctx context.Context, file io.Reader, destinationPath string, contentType string) (string, error) {
+ _, err := s.client.UploadFile(s.bucketName, destinationPath, file, storage_go.FileOptions{
+ ContentType: &contentType,
+ Upsert: new(bool), // Use new(bool) to create a pointer to false
+ })
+
+ if err != nil {
+ return "", err
+ }
+ publicURL := s.client.GetPublicUrl(s.bucketName, destinationPath).SignedURL
+ if publicURL == "" {
+ publicURL = fmt.Sprintf("%s/storage/v1/object/public/%s/%s", s.url, s.bucketName, destinationPath)
+ }
+
+ return publicURL, nil
+}
diff --git a/repositories/academy_repository.go b/repositories/academy_repository.go
index b1f38f66e952d44f196882611bd774e41219ec06..4b02d0ca15f32505d6fe81b12bb9d96e21547185 100644
--- a/repositories/academy_repository.go
+++ b/repositories/academy_repository.go
@@ -7,56 +7,58 @@ import (
entity "abdanhafidz.com/go-boilerplate/models/entity"
"github.com/google/uuid"
"gorm.io/gorm"
+ "gorm.io/gorm/clause"
)
type AcademyRepository interface {
- // Academy
+ Atomic(ctx context.Context, fn func(r AcademyRepository) error) error
+
GetAcademyByID(ctx context.Context, id uuid.UUID) (entity.Academy, error)
GetAcademyBySlug(ctx context.Context, slug string) (entity.Academy, error)
GetAcademyWithProgress(ctx context.Context, accountId uuid.UUID, slug string) (entity.Academy, error)
-
CreateAcademy(ctx context.Context, a entity.Academy) (entity.Academy, error)
UpdateAcademy(ctx context.Context, a entity.Academy) (entity.Academy, error)
DeleteAcademy(ctx context.Context, id uuid.UUID) error
-
ListAcademy(ctx context.Context, accountId uuid.UUID) ([]entity.Academy, error)
GetAcademyWithMaterials(ctx context.Context, id uuid.UUID) (entity.Academy, []entity.AcademyMaterial, error)
CountMaterialsByAcademyID(ctx context.Context, academyId uuid.UUID) (int64, error)
- // Material
GetMaterialBySlug(ctx context.Context, academy_id uuid.UUID, materialSlug string) (entity.AcademyMaterial, error)
GetMaterialByID(ctx context.Context, id uuid.UUID) (entity.AcademyMaterial, error)
GetMaterialWithProgress(ctx context.Context, accountId uuid.UUID, academyId uuid.UUID, slug string) (entity.AcademyMaterial, error)
-
CreateMaterial(ctx context.Context, m entity.AcademyMaterial) (entity.AcademyMaterial, error)
UpdateMaterial(ctx context.Context, m entity.AcademyMaterial) (entity.AcademyMaterial, error)
DeleteMaterial(ctx context.Context, id uuid.UUID) error
-
ListMaterials(ctx context.Context, academyId uuid.UUID) ([]entity.AcademyMaterial, error)
GetMaterialWithContents(ctx context.Context, id uuid.UUID) (entity.AcademyMaterial, []entity.AcademyContent, error)
- // Content
GetContentBySlug(ctx context.Context, materialId uuid.UUID, order uint) (entity.AcademyContent, error)
GetContentByID(ctx context.Context, id uuid.UUID) (entity.AcademyContent, error)
- GetContentWithProgress(ctx context.Context, accountId uuid.UUID, academyId uuid.UUID,materialId uuid.UUID, order uint) (entity.AcademyContent, error)
-
+ GetContentWithProgress(ctx context.Context, accountId uuid.UUID, academyId uuid.UUID, materialId uuid.UUID, order uint) (entity.AcademyContent, error)
CreateContent(ctx context.Context, c entity.AcademyContent) (entity.AcademyContent, error)
UpdateContent(ctx context.Context, c entity.AcademyContent) (entity.AcademyContent, error)
DeleteContent(ctx context.Context, id uuid.UUID) error
-
CountContentsByMaterialID(ctx context.Context, materialId uuid.UUID) (int64, error)
- // Progress
GetAcademyProgress(ctx context.Context, accountId uuid.UUID, academyId uuid.UUID) (entity.AcademyProgress, error)
GetMaterialProgress(ctx context.Context, accountId uuid.UUID, academyId uuid.UUID, materialId uuid.UUID) (entity.AcademyMaterialProgress, error)
GetContentProgress(ctx context.Context, accountId uuid.UUID, academyId uuid.UUID, materialId uuid.UUID, contentId uuid.UUID) (entity.AcademyContentProgress, error)
-
UpsertAcademyProgress(ctx context.Context, p entity.AcademyProgress) (entity.AcademyProgress, error)
UpsertMaterialProgress(ctx context.Context, p entity.AcademyMaterialProgress) (entity.AcademyMaterialProgress, error)
UpsertContentProgress(ctx context.Context, p entity.AcademyContentProgress) (entity.AcademyContentProgress, error)
+ DeleteContentProgressByContentID(ctx context.Context, contentId uuid.UUID) error
+ DeleteContentProgressByMaterialID(ctx context.Context, materialId uuid.UUID) error
+ DeleteMaterialProgressByMaterialID(ctx context.Context, materialId uuid.UUID) error
CountCompletedContentsByMaterialAndAccount(ctx context.Context, accountId uuid.UUID, materialId uuid.UUID) (int64, error)
CountCompletedMaterialsByAcademyAndAccount(ctx context.Context, accountId uuid.UUID, academyId uuid.UUID) (int64, error)
+ CountStartedMaterialsByAcademyAndAccount(ctx context.Context, accountId uuid.UUID, academyId uuid.UUID) (int64, error)
+ DecrementMaterialOrdersGreaterThan(ctx context.Context, academyId uuid.UUID, order uint) error
+ DecrementContentOrdersGreaterThan(ctx context.Context, materialId uuid.UUID, order uint) error
+ GetAccumulatedMaterialProgress(ctx context.Context, accountId uuid.UUID, academyId uuid.UUID) (float64, error)
+
+ BatchRecalculateMaterialProgress(ctx context.Context, materialId uuid.UUID) error
+ BatchRecalculateAcademyProgress(ctx context.Context, academyId uuid.UUID) error
}
type academyRepository struct{ db *gorm.DB }
@@ -65,16 +67,21 @@ func NewAcademyRepository(db *gorm.DB) AcademyRepository {
return &academyRepository{db: db}
}
-// ========== ACADEMY ==========
+func (r *academyRepository) Atomic(ctx context.Context, fn func(r AcademyRepository) error) error {
+ return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
+ txRepo := NewAcademyRepository(tx)
+ return fn(txRepo)
+ })
+}
+
func (r *academyRepository) GetAcademyWithMaterials(ctx context.Context, id uuid.UUID) (entity.Academy, []entity.AcademyMaterial, error) {
var a entity.Academy
- err := r.db.WithContext(ctx).First(&a, "id = ?", id).Error
- if err != nil {
+ if err := r.db.WithContext(ctx).First(&a, "id = ?", id).Error; err != nil {
return entity.Academy{}, nil, err
}
-
var m []entity.AcademyMaterial
- return a, m, r.db.WithContext(ctx).Where("academy_id = ?", id).Find(&m).Error
+ err := r.db.WithContext(ctx).Where("academy_id = ?", id).Order("\"order\" ASC").Find(&m).Error
+ return a, m, err
}
func (r *academyRepository) GetAcademyByID(ctx context.Context, id uuid.UUID) (entity.Academy, error) {
@@ -88,23 +95,15 @@ func (r *academyRepository) GetAcademyBySlug(ctx context.Context, slug string) (
}
func (r *academyRepository) GetAcademyWithProgress(ctx context.Context, accountId uuid.UUID, slug string) (entity.Academy, error) {
- var a entity.Academy
- var err error
- a, err = r.GetAcademyBySlug(ctx, slug)
+ a, err := r.GetAcademyBySlug(ctx, slug)
if err != nil {
return a, err
}
-
- academyId := a.Id
-
- var ap entity.AcademyProgress
-
- ap, err = r.GetAcademyProgress(ctx, accountId, academyId)
- if err != nil && err != gorm.ErrRecordNotFound {
+ ap, err := r.GetAcademyProgress(ctx, accountId, a.Id)
+ if err != nil {
return a, err
}
a.AcademyProgresss = ap
-
return a, nil
}
@@ -122,37 +121,51 @@ func (r *academyRepository) DeleteAcademy(ctx context.Context, id uuid.UUID) err
func (r *academyRepository) ListAcademy(ctx context.Context, accountId uuid.UUID) ([]entity.Academy, error) {
var list []entity.Academy
-
if err := r.db.WithContext(ctx).Find(&list).Error; err != nil {
return nil, err
}
+ if len(list) == 0 {
+ return list, nil
+ }
+
+ academyIDs := make([]uuid.UUID, len(list))
+ for i, ac := range list {
+ academyIDs[i] = ac.Id
+ }
+
+ var progressList []entity.AcademyProgress
+ if err := r.db.WithContext(ctx).
+ Where("account_id = ?", accountId).
+ Where("academy_id IN ?", academyIDs).
+ Find(&progressList).Error; err != nil {
+ return nil, err
+ }
+
+ progressMap := make(map[uuid.UUID]entity.AcademyProgress)
+ for _, p := range progressList {
+ progressMap[p.AcademyId] = p
+ }
for i := range list {
- academyId := list[i].Id
- ap, err := r.GetAcademyProgress(ctx, accountId, academyId)
- if err != nil && err != gorm.ErrRecordNotFound {
- return nil, err
+ if p, exists := progressMap[list[i].Id]; exists {
+ list[i].AcademyProgresss = p
+ } else {
+ list[i].AcademyProgresss = entity.AcademyProgress{
+ AccountId: accountId,
+ AcademyId: list[i].Id,
+ Status: entity.StatusNotStarted,
+ }
}
- list[i].AcademyProgresss = ap
}
-
return list, nil
}
func (r *academyRepository) CountMaterialsByAcademyID(ctx context.Context, academyId uuid.UUID) (int64, error) {
var count int64
-
- query := r.db.WithContext(ctx).
- Where("academy_id = ?", academyId)
-
- err := query.Model(&entity.AcademyMaterial{}).
- Count(&count).
- Error
-
+ err := r.db.WithContext(ctx).Model(&entity.AcademyMaterial{}).Where("academy_id = ?", academyId).Count(&count).Error
return count, err
}
-// ========== MATERIAL ==========
func (r *academyRepository) GetMaterialByID(ctx context.Context, id uuid.UUID) (entity.AcademyMaterial, error) {
var m entity.AcademyMaterial
return m, r.db.WithContext(ctx).First(&m, "id = ?", id).Error
@@ -165,33 +178,24 @@ func (r *academyRepository) GetMaterialBySlug(ctx context.Context, academy_id uu
func (r *academyRepository) GetMaterialWithContents(ctx context.Context, id uuid.UUID) (entity.AcademyMaterial, []entity.AcademyContent, error) {
var m entity.AcademyMaterial
- err := r.db.WithContext(ctx).First(&m, "id = ?", id).Error
- if err != nil {
+ if err := r.db.WithContext(ctx).First(&m, "id = ?", id).Error; err != nil {
return entity.AcademyMaterial{}, nil, err
}
-
var c []entity.AcademyContent
- return m, c, r.db.WithContext(ctx).Where("material_id = ?", id).Order("order asc").Find(&c).Error
+ err := r.db.WithContext(ctx).Where("material_id = ?", id).Order("\"order\" ASC").Find(&c).Error
+ return m, c, err
}
-func (r *academyRepository) GetMaterialWithProgress(ctx context.Context, accountId uuid.UUID, academyId uuid.UUID, slug string) (entity.AcademyMaterial, error){
- var m entity.AcademyMaterial
- var err error
- m, err = r.GetMaterialBySlug(ctx, academyId, slug)
+func (r *academyRepository) GetMaterialWithProgress(ctx context.Context, accountId uuid.UUID, academyId uuid.UUID, slug string) (entity.AcademyMaterial, error) {
+ m, err := r.GetMaterialBySlug(ctx, academyId, slug)
if err != nil {
return m, err
}
-
- MaterialId := m.Id
-
- var ap entity.AcademyMaterialProgress
-
- ap, err = r.GetMaterialProgress(ctx, accountId,academyId, MaterialId)
- if err != nil && err != gorm.ErrRecordNotFound {
+ ap, err := r.GetMaterialProgress(ctx, accountId, academyId, m.Id)
+ if err != nil {
return m, err
}
m.AcademyMaterialProgress = ap
-
return m, nil
}
@@ -201,7 +205,7 @@ func (r *academyRepository) CreateMaterial(ctx context.Context, m entity.Academy
func (r *academyRepository) ListMaterials(ctx context.Context, academyId uuid.UUID) ([]entity.AcademyMaterial, error) {
var list []entity.AcademyMaterial
- return list, r.db.WithContext(ctx).Where("academy_id = ?", academyId).Find(&list).Error
+ return list, r.db.WithContext(ctx).Where("academy_id = ?", academyId).Order("\"order\" ASC").Find(&list).Error
}
func (r *academyRepository) UpdateMaterial(ctx context.Context, m entity.AcademyMaterial) (entity.AcademyMaterial, error) {
@@ -212,8 +216,6 @@ func (r *academyRepository) DeleteMaterial(ctx context.Context, id uuid.UUID) er
return r.db.WithContext(ctx).Delete(&entity.AcademyMaterial{}, "id = ?", id).Error
}
-// ========== CONTENT ==========
-
func (r *academyRepository) GetContentByID(ctx context.Context, id uuid.UUID) (entity.AcademyContent, error) {
var c entity.AcademyContent
return c, r.db.WithContext(ctx).First(&c, "id = ?", id).Error
@@ -221,31 +223,20 @@ func (r *academyRepository) GetContentByID(ctx context.Context, id uuid.UUID) (e
func (r *academyRepository) GetContentBySlug(ctx context.Context, materialId uuid.UUID, order uint) (entity.AcademyContent, error) {
var c entity.AcademyContent
- result := r.db.WithContext(ctx).
- // Escape "order" with backslashes and double quotes: \"order\"
- Where("\"order\" = ?", order).
- Where("material_id = ?", materialId).
- First(&c)
-
- return c, result.Error
+ err := r.db.WithContext(ctx).Where("\"order\" = ? AND material_id = ?", order, materialId).First(&c).Error
+ return c, err
}
-func (r *academyRepository) GetContentWithProgress(ctx context.Context, accountId uuid.UUID, academyId uuid.UUID,materialId uuid.UUID, order uint) (entity.AcademyContent, error){
- var c entity.AcademyContent
- var err error
- c, err = r.GetContentBySlug(ctx,materialId,order)
+func (r *academyRepository) GetContentWithProgress(ctx context.Context, accountId uuid.UUID, academyId uuid.UUID, materialId uuid.UUID, order uint) (entity.AcademyContent, error) {
+ c, err := r.GetContentBySlug(ctx, materialId, order)
if err != nil {
return c, err
}
-
- var ap entity.AcademyContentProgress
-
- ap, err = r.GetContentProgress(ctx, accountId, academyId, materialId,c.Id)
- if err != nil && err != gorm.ErrRecordNotFound {
+ ap, err := r.GetContentProgress(ctx, accountId, academyId, materialId, c.Id)
+ if err != nil {
return c, err
}
c.AcademyContentProgress = ap
-
return c, nil
}
@@ -263,89 +254,56 @@ func (r *academyRepository) DeleteContent(ctx context.Context, id uuid.UUID) err
func (r *academyRepository) CountContentsByMaterialID(ctx context.Context, materialId uuid.UUID) (int64, error) {
var count int64
-
- query := r.db.WithContext(ctx).
- Where("material_id = ?", materialId)
-
- err := query.Model(&entity.AcademyContent{}).
- Count(&count).
- Error
-
+ err := r.db.WithContext(ctx).Model(&entity.AcademyContent{}).Where("material_id = ?", materialId).Count(&count).Error
return count, err
}
-// ========== PROGRESS ==========
-
func (r *academyRepository) GetAcademyProgress(ctx context.Context, accountId uuid.UUID, academyId uuid.UUID) (entity.AcademyProgress, error) {
var existing entity.AcademyProgress
-
- err := r.db.WithContext(ctx).
- Where("account_id = ? AND academy_id = ?", accountId, academyId).
- First(&existing).Error
+ err := r.db.WithContext(ctx).Where("account_id = ? AND academy_id = ?", accountId, academyId).First(&existing).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return entity.AcademyProgress{
- AccountId: accountId,
- AcademyId: academyId,
- Status: "NOT_STARTED",
- Progress: 0,
- TotalCompletedMaterials: 0,
+ AccountId: accountId,
+ AcademyId: academyId,
+ Status: entity.StatusNotStarted,
}, nil
}
-
- if err != nil {
- return existing, err
- }
- return existing, nil
+ return existing, err
}
func (r *academyRepository) UpsertAcademyProgress(ctx context.Context, p entity.AcademyProgress) (entity.AcademyProgress, error) {
- var existing entity.AcademyProgress
- err := r.db.WithContext(ctx).First(&existing, "account_id = ? AND academy_id = ?", p.AccountId, p.AcademyId).Error
- if err == gorm.ErrRecordNotFound {
- return p, r.db.WithContext(ctx).Create(&p).Error
- }
- return p, r.db.WithContext(ctx).Model(&existing).Updates(p).Error
+ return p, r.db.WithContext(ctx).Clauses(clause.OnConflict{
+ Columns: []clause.Column{{Name: "id"}},
+ UpdateAll: true,
+ }).Save(&p).Error
}
func (r *academyRepository) GetMaterialProgress(ctx context.Context, accountId uuid.UUID, academyId uuid.UUID, materialId uuid.UUID) (entity.AcademyMaterialProgress, error) {
var existing entity.AcademyMaterialProgress
-
- err := r.db.WithContext(ctx).First(&existing, "account_id = ? AND academy_id = ? AND material_id = ?", accountId, academyId, materialId).Error
+ err := r.db.WithContext(ctx).Where("account_id = ? AND material_id = ?", accountId, materialId).First(&existing).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
-
return entity.AcademyMaterialProgress{
- AccountId: accountId,
- AcademyId: academyId,
- MaterialId: materialId,
- Progress: 0,
- TotalCompletedContents: 0,
- Status: "NOT_STARTED",
+ AccountId: accountId,
+ AcademyId: academyId,
+ MaterialId: materialId,
+ Status: entity.StatusNotStarted,
}, nil
}
-
- if err != nil {
- return existing, err
- }
- return existing, nil
+ return existing, err
}
func (r *academyRepository) UpsertMaterialProgress(ctx context.Context, p entity.AcademyMaterialProgress) (entity.AcademyMaterialProgress, error) {
- var existing entity.AcademyMaterialProgress
- err := r.db.WithContext(ctx).First(&existing, "account_id = ? AND academy_id = ? AND material_id = ?", p.AccountId, p.AcademyId, p.MaterialId).Error
-
- if err == gorm.ErrRecordNotFound {
- return p, r.db.WithContext(ctx).Create(&p).Error
- }
-
- return p, r.db.WithContext(ctx).Model(&existing).Updates(p).Error
+ return p, r.db.WithContext(ctx).Clauses(clause.OnConflict{
+ Columns: []clause.Column{{Name: "id"}},
+ UpdateAll: true,
+ }).Save(&p).Error
}
func (r *academyRepository) GetContentProgress(ctx context.Context, accountId uuid.UUID, academyId uuid.UUID, materialId uuid.UUID, contentId uuid.UUID) (entity.AcademyContentProgress, error) {
var existing entity.AcademyContentProgress
-
- err := r.db.WithContext(ctx).First(&existing, "account_id = ? AND academy_id = ? AND material_id = ? AND content_id = ?", accountId, academyId, materialId,contentId).Error
+ err := r.db.WithContext(ctx).Where("account_id = ? AND content_id = ?", accountId, contentId).First(&existing).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return entity.AcademyContentProgress{
@@ -353,45 +311,162 @@ func (r *academyRepository) GetContentProgress(ctx context.Context, accountId uu
AcademyId: academyId,
MaterialId: materialId,
ContentId: contentId,
- Status: "NOT_STARTED",
+ Status: entity.StatusNotStarted,
}, nil
}
-
- if err != nil {
- return existing, err
- }
- return existing, nil
+ return existing, err
}
func (r *academyRepository) UpsertContentProgress(ctx context.Context, p entity.AcademyContentProgress) (entity.AcademyContentProgress, error) {
- var existing entity.AcademyContentProgress
- err := r.db.WithContext(ctx).First(&existing, "account_id = ? AND academy_id = ? AND material_id = ? AND content_id = ?", p.AccountId, p.AcademyId, p.MaterialId, p.ContentId).Error
+ return p, r.db.WithContext(ctx).Clauses(clause.OnConflict{
+ Columns: []clause.Column{{Name: "id"}},
+ UpdateAll: true,
+ }).Save(&p).Error
+}
- if err == gorm.ErrRecordNotFound {
- return p, r.db.WithContext(ctx).Create(&p).Error
- }
+func (r *academyRepository) DeleteContentProgressByContentID(ctx context.Context, contentId uuid.UUID) error {
+ return r.db.WithContext(ctx).Where("content_id = ?", contentId).Delete(&entity.AcademyContentProgress{}).Error
+}
- return p, r.db.WithContext(ctx).Model(&existing).Updates(p).Error
+func (r *academyRepository) DeleteContentProgressByMaterialID(ctx context.Context, materialId uuid.UUID) error {
+ return r.db.WithContext(ctx).Where("material_id = ?", materialId).Delete(&entity.AcademyContentProgress{}).Error
}
-// UTILS
+func (r *academyRepository) DeleteMaterialProgressByMaterialID(ctx context.Context, materialId uuid.UUID) error {
+ return r.db.WithContext(ctx).Where("material_id = ?", materialId).Delete(&entity.AcademyMaterialProgress{}).Error
+}
func (r *academyRepository) CountCompletedContentsByMaterialAndAccount(ctx context.Context, accountId uuid.UUID, materialId uuid.UUID) (int64, error) {
var count int64
- query := r.db.WithContext(ctx).
- Where("account_id = ? AND material_id = ? AND status = ?", accountId, materialId, "COMPLETED")
- err := query.Model(&entity.AcademyContentProgress{}).
- Count(&count).
- Error
+ err := r.db.WithContext(ctx).Model(&entity.AcademyContentProgress{}).
+ Where("account_id = ? AND material_id = ? AND status = ?", accountId, materialId, entity.StatusCompleted).
+ Count(&count).Error
return count, err
}
func (r *academyRepository) CountCompletedMaterialsByAcademyAndAccount(ctx context.Context, accountId uuid.UUID, academyId uuid.UUID) (int64, error) {
var count int64
- query := r.db.WithContext(ctx).
- Where("account_id = ? AND academy_id = ? AND status = ?", accountId, academyId, "COMPLETED")
- err := query.Model(&entity.AcademyMaterialProgress{}).
- Count(&count).
- Error
+ err := r.db.WithContext(ctx).Model(&entity.AcademyMaterialProgress{}).
+ Where("account_id = ? AND academy_id = ? AND status = ?", accountId, academyId, entity.StatusCompleted).
+ Count(&count).Error
+ return count, err
+}
+
+func (r *academyRepository) CountStartedMaterialsByAcademyAndAccount(ctx context.Context, accountId uuid.UUID, academyId uuid.UUID) (int64, error) {
+ var count int64
+ err := r.db.WithContext(ctx).Model(&entity.AcademyMaterialProgress{}).
+ Where("account_id = ? AND academy_id = ? AND status IN ?", accountId, academyId, []string{entity.StatusInProgress, entity.StatusCompleted}).
+ Count(&count).Error
return count, err
}
+
+func (r *academyRepository) DecrementMaterialOrdersGreaterThan(ctx context.Context, academyId uuid.UUID, order uint) error {
+ return r.db.WithContext(ctx).Model(&entity.AcademyMaterial{}).
+ Where("academy_id = ? AND \"order\" > ?", academyId, order).
+ Update("\"order\"", gorm.Expr("\"order\" - 1")).Error
+}
+
+func (r *academyRepository) DecrementContentOrdersGreaterThan(ctx context.Context, materialId uuid.UUID, order uint) error {
+ return r.db.WithContext(ctx).Model(&entity.AcademyContent{}).
+ Where("material_id = ? AND \"order\" > ?", materialId, order).
+ Update("\"order\"", gorm.Expr("\"order\" - 1")).Error
+}
+
+func (r *academyRepository) GetAccumulatedMaterialProgress(ctx context.Context, accountId uuid.UUID, academyId uuid.UUID) (float64, error) {
+ var total float64
+ err := r.db.WithContext(ctx).Model(&entity.AcademyMaterialProgress{}).
+ Where("account_id = ? AND academy_id = ?", accountId, academyId).
+ Select("COALESCE(SUM(progress), 0)").
+ Scan(&total).Error
+ return total, err
+}
+
+func (r *academyRepository) BatchRecalculateMaterialProgress(ctx context.Context, materialId uuid.UUID) error {
+ totalContents, err := r.CountContentsByMaterialID(ctx, materialId)
+ if err != nil {
+ return err
+ }
+
+ if totalContents == 0 {
+ return r.db.WithContext(ctx).Model(&entity.AcademyMaterialProgress{}).
+ Where("material_id = ?", materialId).
+ Updates(map[string]interface{}{
+ "progress": 100,
+ "status": entity.StatusCompleted,
+ "total_completed_contents": 0,
+ }).Error
+ }
+
+ return r.db.WithContext(ctx).Exec(`
+ UPDATE academy_material_progresses amp
+ SET
+ total_completed_contents = (
+ SELECT COUNT(id) FROM academy_content_progresses acp
+ WHERE acp.material_id = amp.material_id AND acp.account_id = amp.account_id AND acp.status = 'COMPLETED'
+ ),
+ progress = (
+ SELECT COUNT(id) FROM academy_content_progresses acp
+ WHERE acp.material_id = amp.material_id AND acp.account_id = amp.account_id AND acp.status = 'COMPLETED'
+ )::float / ? * 100,
+ status = CASE
+ WHEN (
+ SELECT COUNT(id) FROM academy_content_progresses acp
+ WHERE acp.material_id = amp.material_id AND acp.account_id = amp.account_id AND acp.status = 'COMPLETED'
+ ) >= ? THEN 'COMPLETED'
+ ELSE 'IN_PROGRESS'
+ END,
+ completed_at = CASE
+ WHEN (
+ SELECT COUNT(id) FROM academy_content_progresses acp
+ WHERE acp.material_id = amp.material_id AND acp.account_id = amp.account_id AND acp.status = 'COMPLETED'
+ ) >= ? THEN NOW()
+ ELSE NULL
+ END
+ WHERE amp.material_id = ?
+ `, totalContents, totalContents, totalContents, materialId).Error
+}
+
+func (r *academyRepository) BatchRecalculateAcademyProgress(ctx context.Context, academyId uuid.UUID) error {
+ totalMaterials, err := r.CountMaterialsByAcademyID(ctx, academyId)
+ if err != nil {
+ return err
+ }
+
+ if totalMaterials == 0 {
+ return r.db.WithContext(ctx).Model(&entity.AcademyProgress{}).
+ Where("academy_id = ?", academyId).
+ Updates(map[string]interface{}{
+ "progress": 100,
+ "status": entity.StatusCompleted,
+ "total_completed_materials": 0,
+ }).Error
+ }
+
+ return r.db.WithContext(ctx).Exec(`
+ UPDATE academy_progress ap
+ SET
+ progress = (
+ SELECT COALESCE(SUM(amp.progress), 0) FROM academy_material_progresses amp
+ WHERE amp.academy_id = ap.academy_id AND amp.account_id = ap.account_id
+ )::float / ?,
+ total_completed_materials = (
+ SELECT COUNT(id) FROM academy_material_progresses amp
+ WHERE amp.academy_id = ap.academy_id AND amp.account_id = ap.account_id AND amp.status = 'COMPLETED'
+ ),
+ status = CASE
+ WHEN (
+ SELECT COUNT(id) FROM academy_material_progresses amp
+ WHERE amp.academy_id = ap.academy_id AND amp.account_id = ap.account_id AND amp.status = 'COMPLETED'
+ ) >= ? THEN 'COMPLETED'
+ ELSE 'IN_PROGRESS'
+ END,
+ completed_at = CASE
+ WHEN (
+ SELECT COUNT(id) FROM academy_material_progresses amp
+ WHERE amp.academy_id = ap.academy_id AND amp.account_id = ap.account_id AND amp.status = 'COMPLETED'
+ ) >= ? THEN NOW()
+ ELSE NULL
+ END
+ WHERE ap.academy_id = ?
+ `, totalMaterials, totalMaterials, totalMaterials, academyId).Error
+}
\ No newline at end of file
diff --git a/repositories/exam_event_answer_repository.go b/repositories/exam_event_answer_repository.go
index 040f1f2701d3e45500f503e1c89e75c985a9eacd..e7046c5417a68a4444c38ea3e1d04c75e6d1bb1a 100644
--- a/repositories/exam_event_answer_repository.go
+++ b/repositories/exam_event_answer_repository.go
@@ -1,59 +1,59 @@
-package repositories
-
-import (
- "context"
-
- entity "abdanhafidz.com/go-boilerplate/models/entity"
- "github.com/google/uuid"
- "gorm.io/gorm"
-)
-
-type ExamEventAnswerRepository interface {
- Create(ctx context.Context, ans *entity.ExamEventAnswer) error
- Update(ctx context.Context, ans *entity.ExamEventAnswer) error
- GetByAttemptAndQuestion(ctx context.Context, attemptId uuid.UUID, questionId uuid.UUID) (entity.ExamEventAnswer, error)
- ListByAttempt(ctx context.Context, attemptId uuid.UUID) ([]entity.ExamEventAnswer, error)
- DeleteByAttempt(ctx context.Context, attemptId uuid.UUID) error
-
- // decomposed result (answer + question)
-}
-
-type examEventAnswerRepository struct {
- db *gorm.DB
-}
-
-func NewExamEventAnswerRepository(db *gorm.DB) ExamEventAnswerRepository {
- return &examEventAnswerRepository{db: db}
-}
-
-func (r *examEventAnswerRepository) Create(ctx context.Context, ans *entity.ExamEventAnswer) error {
- return r.db.WithContext(ctx).Create(ans).Error
-}
-
-func (r *examEventAnswerRepository) Update(ctx context.Context, ans *entity.ExamEventAnswer) error {
- return r.db.WithContext(ctx).
- Where("attempt_id = ? AND question_id = ?", ans.AttemptId, ans.QuestionId).
- Updates(ans).Error
-}
-
-func (r *examEventAnswerRepository) GetByAttemptAndQuestion(ctx context.Context, attemptId uuid.UUID, questionId uuid.UUID) (entity.ExamEventAnswer, error) {
- var ans entity.ExamEventAnswer
- err := r.db.WithContext(ctx).
- Where("attempt_id = ? AND question_id = ?", attemptId, questionId).
- First(&ans).Error
- return ans, err
-}
-
-func (r *examEventAnswerRepository) ListByAttempt(ctx context.Context, attemptId uuid.UUID) ([]entity.ExamEventAnswer, error) {
- var answers []entity.ExamEventAnswer
- err := r.db.WithContext(ctx).
- Where("attempt_id = ?", attemptId).
- Find(&answers).Error
- return answers, err
-}
-
-func (r *examEventAnswerRepository) DeleteByAttempt(ctx context.Context, attemptId uuid.UUID) error {
- return r.db.WithContext(ctx).
- Where("attempt_id = ?", attemptId).
- Delete(&entity.ExamEventAnswer{}).Error
-}
+package repositories
+
+import (
+ "context"
+
+ entity "abdanhafidz.com/go-boilerplate/models/entity"
+ "github.com/google/uuid"
+ "gorm.io/gorm"
+)
+
+type ExamEventAnswerRepository interface {
+ Create(ctx context.Context, ans *entity.ExamEventAnswer) error
+ Update(ctx context.Context, ans *entity.ExamEventAnswer) error
+ GetByAttemptAndQuestion(ctx context.Context, attemptId uuid.UUID, questionId uuid.UUID) (entity.ExamEventAnswer, error)
+ ListByAttempt(ctx context.Context, attemptId uuid.UUID) ([]entity.ExamEventAnswer, error)
+ DeleteByAttempt(ctx context.Context, attemptId uuid.UUID) error
+
+ // decomposed result (answer + question)
+}
+
+type examEventAnswerRepository struct {
+ db *gorm.DB
+}
+
+func NewExamEventAnswerRepository(db *gorm.DB) ExamEventAnswerRepository {
+ return &examEventAnswerRepository{db: db}
+}
+
+func (r *examEventAnswerRepository) Create(ctx context.Context, ans *entity.ExamEventAnswer) error {
+ return r.db.WithContext(ctx).Create(ans).Error
+}
+
+func (r *examEventAnswerRepository) Update(ctx context.Context, ans *entity.ExamEventAnswer) error {
+ return r.db.WithContext(ctx).
+ Where("attempt_id = ? AND question_id = ?", ans.AttemptId, ans.QuestionId).
+ Updates(ans).Error
+}
+
+func (r *examEventAnswerRepository) GetByAttemptAndQuestion(ctx context.Context, attemptId uuid.UUID, questionId uuid.UUID) (entity.ExamEventAnswer, error) {
+ var ans entity.ExamEventAnswer
+ err := r.db.WithContext(ctx).
+ Where("attempt_id = ? AND question_id = ?", attemptId, questionId).
+ First(&ans).Error
+ return ans, err
+}
+
+func (r *examEventAnswerRepository) ListByAttempt(ctx context.Context, attemptId uuid.UUID) ([]entity.ExamEventAnswer, error) {
+ var answers []entity.ExamEventAnswer
+ err := r.db.WithContext(ctx).
+ Where("attempt_id = ?", attemptId).
+ Find(&answers).Error
+ return answers, err
+}
+
+func (r *examEventAnswerRepository) DeleteByAttempt(ctx context.Context, attemptId uuid.UUID) error {
+ return r.db.WithContext(ctx).
+ Where("attempt_id = ?", attemptId).
+ Delete(&entity.ExamEventAnswer{}).Error
+}
diff --git a/repositories/exam_event_assign_repository.go b/repositories/exam_event_assign_repository.go
index f97b03974bb2fd746f745ef54aa2ff93e7dac380..adb003606fad99c54e0d575b3de68e416de00587 100644
--- a/repositories/exam_event_assign_repository.go
+++ b/repositories/exam_event_assign_repository.go
@@ -1,47 +1,47 @@
-package repositories
-
-import (
- "context"
-
- entity "abdanhafidz.com/go-boilerplate/models/entity"
- "github.com/google/uuid"
- "gorm.io/gorm"
-)
-
-type ExamEventAssignRepository interface {
- Create(ctx context.Context, m entity.ExamEventAssign) error
- ListByEvent(ctx context.Context, eventId uuid.UUID) ([]entity.ExamEventAssign, error)
- Delete(ctx context.Context, id uuid.UUID) error
- Check(ctx context.Context, eventId uuid.UUID, examId uuid.UUID) error
-}
-
-type examEventAssignRepository struct{ db *gorm.DB }
-
-func NewExamEventAssignRepository(db *gorm.DB) ExamEventAssignRepository {
- return &examEventAssignRepository{db}
-}
-
-func (r *examEventAssignRepository) Check(ctx context.Context, eventId uuid.UUID, examId uuid.UUID) error {
- return r.db.WithContext(ctx).
- Where("event_id = ?", eventId).
- Where("exam_id = ?", examId).
- First(&entity.ExamEventAssign{}).Error
-}
-func (r *examEventAssignRepository) Create(ctx context.Context, m entity.ExamEventAssign) error {
- return r.db.WithContext(ctx).Create(&m).Error
-}
-
-func (r *examEventAssignRepository) ListByEvent(ctx context.Context, eventId uuid.UUID) ([]entity.ExamEventAssign, error) {
- var items []entity.ExamEventAssign
- err := r.db.WithContext(ctx).
- Where("event_id = ?", eventId).
- Preload("Exam").
- Find(&items).Error
- return items, err
-}
-
-func (r *examEventAssignRepository) Delete(ctx context.Context, id uuid.UUID) error {
- return r.db.WithContext(ctx).
- Where("id = ?", id).
- Delete(&entity.ExamEventAssign{}).Error
-}
+package repositories
+
+import (
+ "context"
+
+ entity "abdanhafidz.com/go-boilerplate/models/entity"
+ "github.com/google/uuid"
+ "gorm.io/gorm"
+)
+
+type ExamEventAssignRepository interface {
+ Create(ctx context.Context, m entity.ExamEventAssign) error
+ ListByEvent(ctx context.Context, eventId uuid.UUID) ([]entity.ExamEventAssign, error)
+ Delete(ctx context.Context, id uuid.UUID) error
+ Check(ctx context.Context, eventId uuid.UUID, examId uuid.UUID) error
+}
+
+type examEventAssignRepository struct{ db *gorm.DB }
+
+func NewExamEventAssignRepository(db *gorm.DB) ExamEventAssignRepository {
+ return &examEventAssignRepository{db}
+}
+
+func (r *examEventAssignRepository) Check(ctx context.Context, eventId uuid.UUID, examId uuid.UUID) error {
+ return r.db.WithContext(ctx).
+ Where("event_id = ?", eventId).
+ Where("exam_id = ?", examId).
+ First(&entity.ExamEventAssign{}).Error
+}
+func (r *examEventAssignRepository) Create(ctx context.Context, m entity.ExamEventAssign) error {
+ return r.db.WithContext(ctx).Create(&m).Error
+}
+
+func (r *examEventAssignRepository) ListByEvent(ctx context.Context, eventId uuid.UUID) ([]entity.ExamEventAssign, error) {
+ var items []entity.ExamEventAssign
+ err := r.db.WithContext(ctx).
+ Where("event_id = ?", eventId).
+ Preload("Exam").
+ Find(&items).Error
+ return items, err
+}
+
+func (r *examEventAssignRepository) Delete(ctx context.Context, id uuid.UUID) error {
+ return r.db.WithContext(ctx).
+ Where("id = ?", id).
+ Delete(&entity.ExamEventAssign{}).Error
+}
diff --git a/repositories/exam_event_attempt_repository.go b/repositories/exam_event_attempt_repository.go
index 6ca343fe0edb251112514b5b1f6da5892d96dbe9..dc74e85afc704b211b5b5e009c20187fb9f91af2 100644
--- a/repositories/exam_event_attempt_repository.go
+++ b/repositories/exam_event_attempt_repository.go
@@ -1,55 +1,55 @@
-package repositories
-
-import (
- "context"
-
- entity "abdanhafidz.com/go-boilerplate/models/entity"
- "github.com/google/uuid"
- "gorm.io/gorm"
-)
-
-type ExamEventAttemptRepository interface {
- Create(ctx context.Context, a *entity.ExamEventAttempt) error
- GetById(ctx context.Context, attemptId uuid.UUID) (entity.ExamEventAttempt, error)
- GetByExamEvent(ctx context.Context, eventId uuid.UUID, examId uuid.UUID, accountId uuid.UUID) (entity.ExamEventAttempt, error)
- Update(ctx context.Context, a *entity.ExamEventAttempt) error
-}
-
-type examEventAttemptRepository struct{ db *gorm.DB }
-
-func NewExamEventAttemptRepository(db *gorm.DB) ExamEventAttemptRepository {
- return &examEventAttemptRepository{db}
-}
-
-func (r *examEventAttemptRepository) Create(ctx context.Context, a *entity.ExamEventAttempt) error {
- return r.db.WithContext(ctx).Create(a).Error
-}
-
-func (r *examEventAttemptRepository) GetById(ctx context.Context, attemptId uuid.UUID) (entity.ExamEventAttempt, error) {
- var a entity.ExamEventAttempt
- err := r.db.WithContext(ctx).
- Preload("Answers").
- First(&a, "id = ?", attemptId).Error
- return a, err
-}
-
-func (r *examEventAttemptRepository) GetByExamEvent(ctx context.Context, eventId uuid.UUID, examId uuid.UUID, accountId uuid.UUID) (entity.ExamEventAttempt, error) {
-
- var attempt entity.ExamEventAttempt
-
- err := r.db.WithContext(ctx).
- Preload("Answers").
- Where("event_id = ?", eventId).
- Where("exam_id = ?", examId).
- Where("account_id = ?", accountId).
- First(&attempt).Error
-
- return attempt, err
-}
-
-func (r *examEventAttemptRepository) Update(ctx context.Context, a *entity.ExamEventAttempt) error {
- return r.db.WithContext(ctx).
- Model(&entity.ExamEventAttempt{}).
- Where("id = ?", a.Id).
- Updates(a).Error
-}
+package repositories
+
+import (
+ "context"
+
+ entity "abdanhafidz.com/go-boilerplate/models/entity"
+ "github.com/google/uuid"
+ "gorm.io/gorm"
+)
+
+type ExamEventAttemptRepository interface {
+ Create(ctx context.Context, a *entity.ExamEventAttempt) error
+ GetById(ctx context.Context, attemptId uuid.UUID) (entity.ExamEventAttempt, error)
+ GetByExamEvent(ctx context.Context, eventId uuid.UUID, examId uuid.UUID, accountId uuid.UUID) (entity.ExamEventAttempt, error)
+ Update(ctx context.Context, a *entity.ExamEventAttempt) error
+}
+
+type examEventAttemptRepository struct{ db *gorm.DB }
+
+func NewExamEventAttemptRepository(db *gorm.DB) ExamEventAttemptRepository {
+ return &examEventAttemptRepository{db}
+}
+
+func (r *examEventAttemptRepository) Create(ctx context.Context, a *entity.ExamEventAttempt) error {
+ return r.db.WithContext(ctx).Create(a).Error
+}
+
+func (r *examEventAttemptRepository) GetById(ctx context.Context, attemptId uuid.UUID) (entity.ExamEventAttempt, error) {
+ var a entity.ExamEventAttempt
+ err := r.db.WithContext(ctx).
+ Preload("Answers").
+ First(&a, "id = ?", attemptId).Error
+ return a, err
+}
+
+func (r *examEventAttemptRepository) GetByExamEvent(ctx context.Context, eventId uuid.UUID, examId uuid.UUID, accountId uuid.UUID) (entity.ExamEventAttempt, error) {
+
+ var attempt entity.ExamEventAttempt
+
+ err := r.db.WithContext(ctx).
+ Preload("Answers").
+ Where("event_id = ?", eventId).
+ Where("exam_id = ?", examId).
+ Where("account_id = ?", accountId).
+ First(&attempt).Error
+
+ return attempt, err
+}
+
+func (r *examEventAttemptRepository) Update(ctx context.Context, a *entity.ExamEventAttempt) error {
+ return r.db.WithContext(ctx).
+ Model(&entity.ExamEventAttempt{}).
+ Where("id = ?", a.Id).
+ Updates(a).Error
+}
diff --git a/repositories/exam_event_repository.go b/repositories/exam_event_repository.go
index 65b01f93e8c21eaae5622cc0e6eeec5cf8b86081..564f9c78a088cf6cc95306b4a8e8551855231962 100644
--- a/repositories/exam_event_repository.go
+++ b/repositories/exam_event_repository.go
@@ -1,85 +1,85 @@
-package repositories
-
-import (
- "context"
-
- entity "abdanhafidz.com/go-boilerplate/models/entity"
- "github.com/google/uuid"
- "gorm.io/gorm"
-)
-
-type ExamRepository interface {
- Create(ctx context.Context, e entity.Exam) error
- Get(ctx context.Context, id uuid.UUID) (entity.Exam, error)
- GetBySlug(ctx context.Context, slug string) (entity.Exam, error)
- Update(ctx context.Context, e entity.Exam) error
- Delete(ctx context.Context, id uuid.UUID) error
- List(ctx context.Context) ([]entity.Exam, error)
-
- // Additional business need
- ListByEvent(ctx context.Context, eventId uuid.UUID) ([]entity.Exam, error)
-}
-
-type examRepository struct {
- db *gorm.DB
-}
-
-func NewExamRepository(db *gorm.DB) ExamRepository {
- return &examRepository{db}
-}
-
-// ================= CRUD =================
-
-func (r *examRepository) Create(ctx context.Context, e entity.Exam) error {
- return r.db.WithContext(ctx).Create(&e).Error
-}
-
-func (r *examRepository) Get(ctx context.Context, id uuid.UUID) (entity.Exam, error) {
- var e entity.Exam
- err := r.db.WithContext(ctx).
- First(&e, "exam_id = ?", id).Error
- return e, err
-}
-
-func (r *examRepository) GetBySlug(ctx context.Context, slug string) (entity.Exam, error) {
- var e entity.Exam
- err := r.db.WithContext(ctx).
- Where("slug = ?", slug).
- First(&e).Error
- return e, err
-}
-
-func (r *examRepository) Update(ctx context.Context, e entity.Exam) error {
- return r.db.WithContext(ctx).
- Model(&entity.Exam{}).
- Where("exam_id = ?", e.Id).
- Updates(e).Error
-}
-
-func (r *examRepository) Delete(ctx context.Context, id uuid.UUID) error {
- return r.db.WithContext(ctx).
- Where("exam_id = ?", id).
- Delete(&entity.Exam{}).Error
-}
-
-func (r *examRepository) List(ctx context.Context) ([]entity.Exam, error) {
- var list []entity.Exam
- err := r.db.WithContext(ctx).
- Order("created_at DESC").
- Find(&list).Error
- return list, err
-}
-
-// =========== Business Specific ============
-
-func (r *examRepository) ListByEvent(ctx context.Context, eventId uuid.UUID) ([]entity.Exam, error) {
- var exams []entity.Exam
-
- err := r.db.WithContext(ctx).
- Table("exam").
- Joins(`JOIN exam_event_assign ON exam_event_assign.exam_id = exam.id`).
- Where("exam_event_assign.event_id = ?", eventId).
- Find(&exams).Error
-
- return exams, err
-}
+package repositories
+
+import (
+ "context"
+
+ entity "abdanhafidz.com/go-boilerplate/models/entity"
+ "github.com/google/uuid"
+ "gorm.io/gorm"
+)
+
+type ExamRepository interface {
+ Create(ctx context.Context, e entity.Exam) error
+ Get(ctx context.Context, id uuid.UUID) (entity.Exam, error)
+ GetBySlug(ctx context.Context, slug string) (entity.Exam, error)
+ Update(ctx context.Context, e entity.Exam) error
+ Delete(ctx context.Context, id uuid.UUID) error
+ List(ctx context.Context) ([]entity.Exam, error)
+
+ // Additional business need
+ ListByEvent(ctx context.Context, eventId uuid.UUID) ([]entity.Exam, error)
+}
+
+type examRepository struct {
+ db *gorm.DB
+}
+
+func NewExamRepository(db *gorm.DB) ExamRepository {
+ return &examRepository{db}
+}
+
+// ================= CRUD =================
+
+func (r *examRepository) Create(ctx context.Context, e entity.Exam) error {
+ return r.db.WithContext(ctx).Create(&e).Error
+}
+
+func (r *examRepository) Get(ctx context.Context, id uuid.UUID) (entity.Exam, error) {
+ var e entity.Exam
+ err := r.db.WithContext(ctx).
+ First(&e, "exam_id = ?", id).Error
+ return e, err
+}
+
+func (r *examRepository) GetBySlug(ctx context.Context, slug string) (entity.Exam, error) {
+ var e entity.Exam
+ err := r.db.WithContext(ctx).
+ Where("slug = ?", slug).
+ First(&e).Error
+ return e, err
+}
+
+func (r *examRepository) Update(ctx context.Context, e entity.Exam) error {
+ return r.db.WithContext(ctx).
+ Model(&entity.Exam{}).
+ Where("exam_id = ?", e.Id).
+ Updates(e).Error
+}
+
+func (r *examRepository) Delete(ctx context.Context, id uuid.UUID) error {
+ return r.db.WithContext(ctx).
+ Where("exam_id = ?", id).
+ Delete(&entity.Exam{}).Error
+}
+
+func (r *examRepository) List(ctx context.Context) ([]entity.Exam, error) {
+ var list []entity.Exam
+ err := r.db.WithContext(ctx).
+ Order("created_at DESC").
+ Find(&list).Error
+ return list, err
+}
+
+// =========== Business Specific ============
+
+func (r *examRepository) ListByEvent(ctx context.Context, eventId uuid.UUID) ([]entity.Exam, error) {
+ var exams []entity.Exam
+
+ err := r.db.WithContext(ctx).
+ Table("exam").
+ Joins(`JOIN exam_event_assign ON exam_event_assign.exam_id = exam.id`).
+ Where("exam_event_assign.event_id = ?", eventId).
+ Find(&exams).Error
+
+ return exams, err
+}
diff --git a/repositories/file_repository.go b/repositories/file_repository.go
new file mode 100644
index 0000000000000000000000000000000000000000..e7ab1b77ce139f4bb81e38287cbb057bdddf2f6d
--- /dev/null
+++ b/repositories/file_repository.go
@@ -0,0 +1,43 @@
+package repositories
+
+import (
+ "context"
+ "errors"
+
+ "github.com/google/uuid"
+ "gorm.io/gorm"
+
+ entity "abdanhafidz.com/go-boilerplate/models/entity"
+ http_error "abdanhafidz.com/go-boilerplate/models/error"
+)
+
+type FileRepository interface {
+ Create(ctx context.Context, file *entity.File) error
+ FindByID(ctx context.Context, id uuid.UUID) (*entity.File, error)
+}
+
+type fileRepository struct {
+ db *gorm.DB
+}
+
+func NewFileRepository(db *gorm.DB) FileRepository {
+ return &fileRepository{db: db}
+}
+
+func (r *fileRepository) Create(ctx context.Context, file *entity.File) error {
+ return r.db.WithContext(ctx).Create(file).Error
+}
+
+func (r *fileRepository) FindByID(ctx context.Context, id uuid.UUID) (*entity.File, error) {
+ var file entity.File
+ result := r.db.WithContext(ctx).First(&file, "id = ?", id)
+
+ if result.Error != nil {
+ if errors.Is(result.Error, gorm.ErrRecordNotFound) {
+ return nil, http_error.NOT_FOUND_ERROR
+ }
+ return nil, result.Error
+ }
+
+ return &file, nil
+}
\ No newline at end of file
diff --git a/repositories/problem_set_exam_assign_repository.go b/repositories/problem_set_exam_assign_repository.go
index f930e925e62a95545ec97f7eea5bcd87f9527e7a..09bd615022a58d05af909abdadc6f446fbcfcf18 100644
--- a/repositories/problem_set_exam_assign_repository.go
+++ b/repositories/problem_set_exam_assign_repository.go
@@ -1,40 +1,40 @@
-package repositories
-
-import (
- "context"
-
- entity "abdanhafidz.com/go-boilerplate/models/entity"
- "github.com/google/uuid"
- "gorm.io/gorm"
-)
-
-type ProblemSetExamAssignRepository interface {
- Create(ctx context.Context, m entity.ProblemSetExamAssign) error
- GetByExam(ctx context.Context, examId uuid.UUID) (entity.ProblemSetExamAssign, error)
- Delete(ctx context.Context, id uuid.UUID) error
-}
-
-type problemSetExamAssignRepository struct{ db *gorm.DB }
-
-func NewProblemSetExamAssignRepository(db *gorm.DB) ProblemSetExamAssignRepository {
- return &problemSetExamAssignRepository{db}
-}
-
-func (r *problemSetExamAssignRepository) Create(ctx context.Context, m entity.ProblemSetExamAssign) error {
- return r.db.WithContext(ctx).Create(&m).Error
-}
-
-func (r *problemSetExamAssignRepository) GetByExam(ctx context.Context, examId uuid.UUID) (entity.ProblemSetExamAssign, error) {
- var items entity.ProblemSetExamAssign
- err := r.db.WithContext(ctx).
- Where("exam_id = ?", examId).
- Preload("ProblemSet").
- First(&items).Error
- return items, err
-}
-
-func (r *problemSetExamAssignRepository) Delete(ctx context.Context, id uuid.UUID) error {
- return r.db.WithContext(ctx).
- Where("id = ?", id).
- Delete(&entity.ProblemSetExamAssign{}).Error
-}
+package repositories
+
+import (
+ "context"
+
+ entity "abdanhafidz.com/go-boilerplate/models/entity"
+ "github.com/google/uuid"
+ "gorm.io/gorm"
+)
+
+type ProblemSetExamAssignRepository interface {
+ Create(ctx context.Context, m entity.ProblemSetExamAssign) error
+ GetByExam(ctx context.Context, examId uuid.UUID) (entity.ProblemSetExamAssign, error)
+ Delete(ctx context.Context, id uuid.UUID) error
+}
+
+type problemSetExamAssignRepository struct{ db *gorm.DB }
+
+func NewProblemSetExamAssignRepository(db *gorm.DB) ProblemSetExamAssignRepository {
+ return &problemSetExamAssignRepository{db}
+}
+
+func (r *problemSetExamAssignRepository) Create(ctx context.Context, m entity.ProblemSetExamAssign) error {
+ return r.db.WithContext(ctx).Create(&m).Error
+}
+
+func (r *problemSetExamAssignRepository) GetByExam(ctx context.Context, examId uuid.UUID) (entity.ProblemSetExamAssign, error) {
+ var items entity.ProblemSetExamAssign
+ err := r.db.WithContext(ctx).
+ Where("exam_id = ?", examId).
+ Preload("ProblemSet").
+ First(&items).Error
+ return items, err
+}
+
+func (r *problemSetExamAssignRepository) Delete(ctx context.Context, id uuid.UUID) error {
+ return r.db.WithContext(ctx).
+ Where("id = ?", id).
+ Delete(&entity.ProblemSetExamAssign{}).Error
+}
diff --git a/repositories/problem_set_repository.go b/repositories/problem_set_repository.go
index 6ebb2a0be12205d07c8122fc8ab2321c23482bcd..25a3b23d74a34fc59d0ba28fab5aabc94f0860b3 100644
--- a/repositories/problem_set_repository.go
+++ b/repositories/problem_set_repository.go
@@ -1,57 +1,57 @@
-package repositories
-
-import (
- "context"
-
- entity "abdanhafidz.com/go-boilerplate/models/entity"
- "github.com/google/uuid"
- "gorm.io/gorm"
-)
-
-type ProblemSetRepository interface {
- Create(ctx context.Context, ps entity.ProblemSet) error
- Get(ctx context.Context, id uuid.UUID) (entity.ProblemSet, error)
- Update(ctx context.Context, ps entity.ProblemSet) error
- Delete(ctx context.Context, id uuid.UUID) error
- List(ctx context.Context) ([]entity.ProblemSet, error)
-}
-
-type problemSetRepository struct {
- db *gorm.DB
-}
-
-func NewProblemSetRepository(db *gorm.DB) ProblemSetRepository {
- return &problemSetRepository{db: db}
-}
-
-func (r *problemSetRepository) Create(ctx context.Context, ps entity.ProblemSet) error {
- return r.db.WithContext(ctx).Create(&ps).Error
-}
-
-func (r *problemSetRepository) Get(ctx context.Context, id uuid.UUID) (entity.ProblemSet, error) {
- var ps entity.ProblemSet
- err := r.db.WithContext(ctx).
- First(&ps, "id = ?", id).Error
- return ps, err
-}
-
-func (r *problemSetRepository) List(ctx context.Context) ([]entity.ProblemSet, error) {
- var list []entity.ProblemSet
- err := r.db.WithContext(ctx).
- Order("title").
- Find(&list).Error
- return list, err
-}
-
-func (r *problemSetRepository) Update(ctx context.Context, ps entity.ProblemSet) error {
- return r.db.WithContext(ctx).
- Model(&entity.ProblemSet{}).
- Where("id = ?", ps.Id).
- Updates(ps).Error
-}
-
-func (r *problemSetRepository) Delete(ctx context.Context, id uuid.UUID) error {
- return r.db.WithContext(ctx).
- Where("id = ?", id).
- Delete(&entity.ProblemSet{}).Error
-}
+package repositories
+
+import (
+ "context"
+
+ entity "abdanhafidz.com/go-boilerplate/models/entity"
+ "github.com/google/uuid"
+ "gorm.io/gorm"
+)
+
+type ProblemSetRepository interface {
+ Create(ctx context.Context, ps entity.ProblemSet) error
+ Get(ctx context.Context, id uuid.UUID) (entity.ProblemSet, error)
+ Update(ctx context.Context, ps entity.ProblemSet) error
+ Delete(ctx context.Context, id uuid.UUID) error
+ List(ctx context.Context) ([]entity.ProblemSet, error)
+}
+
+type problemSetRepository struct {
+ db *gorm.DB
+}
+
+func NewProblemSetRepository(db *gorm.DB) ProblemSetRepository {
+ return &problemSetRepository{db: db}
+}
+
+func (r *problemSetRepository) Create(ctx context.Context, ps entity.ProblemSet) error {
+ return r.db.WithContext(ctx).Create(&ps).Error
+}
+
+func (r *problemSetRepository) Get(ctx context.Context, id uuid.UUID) (entity.ProblemSet, error) {
+ var ps entity.ProblemSet
+ err := r.db.WithContext(ctx).
+ First(&ps, "id = ?", id).Error
+ return ps, err
+}
+
+func (r *problemSetRepository) List(ctx context.Context) ([]entity.ProblemSet, error) {
+ var list []entity.ProblemSet
+ err := r.db.WithContext(ctx).
+ Order("title").
+ Find(&list).Error
+ return list, err
+}
+
+func (r *problemSetRepository) Update(ctx context.Context, ps entity.ProblemSet) error {
+ return r.db.WithContext(ctx).
+ Model(&entity.ProblemSet{}).
+ Where("id = ?", ps.Id).
+ Updates(ps).Error
+}
+
+func (r *problemSetRepository) Delete(ctx context.Context, id uuid.UUID) error {
+ return r.db.WithContext(ctx).
+ Where("id = ?", id).
+ Delete(&entity.ProblemSet{}).Error
+}
diff --git a/repositories/question_repository.go b/repositories/question_repository.go
index b4144cce471589cad0946de39b7ebfecac9b0fd2..16abf2905168e6838bf1f407cf9f6a3ee357d0fd 100644
--- a/repositories/question_repository.go
+++ b/repositories/question_repository.go
@@ -1,55 +1,55 @@
-package repositories
-
-import (
- "context"
-
- entity "abdanhafidz.com/go-boilerplate/models/entity"
- "github.com/google/uuid"
- "gorm.io/gorm"
-)
-
-type QuestionsRepository interface {
- Create(ctx context.Context, q entity.Questions) error
- Get(ctx context.Context, id uuid.UUID) (entity.Questions, error)
- Update(ctx context.Context, q entity.Questions) error
- Delete(ctx context.Context, id uuid.UUID) error
- ListByProblemSet(ctx context.Context, problemSetId uuid.UUID) ([]entity.Questions, error)
-}
-
-type questionsRepository struct{ db *gorm.DB }
-
-func NewQuestionsRepository(db *gorm.DB) QuestionsRepository {
- return &questionsRepository{db}
-}
-
-func (r *questionsRepository) Create(ctx context.Context, q entity.Questions) error {
- return r.db.WithContext(ctx).Create(&q).Error
-}
-
-func (r *questionsRepository) Get(ctx context.Context, id uuid.UUID) (entity.Questions, error) {
- var q entity.Questions
- err := r.db.WithContext(ctx).First(&q, "id = ?", id).Error
- return q, err
-}
-
-func (r *questionsRepository) Update(ctx context.Context, q entity.Questions) error {
- return r.db.WithContext(ctx).
- Model(&entity.Questions{}).
- Where("id = ?", q.Id).
- Updates(q).Error
-}
-
-func (r *questionsRepository) Delete(ctx context.Context, id uuid.UUID) error {
- return r.db.WithContext(ctx).
- Where("id = ?", id).
- Delete(&entity.Questions{}).Error
-}
-
-func (r *questionsRepository) ListByProblemSet(ctx context.Context, problemSetId uuid.UUID) ([]entity.Questions, error) {
- var q []entity.Questions
- err := r.db.WithContext(ctx).
- Where("problem_set_id = ?", problemSetId).
- Order("id").
- Find(&q).Error
- return q, err
-}
+package repositories
+
+import (
+ "context"
+
+ entity "abdanhafidz.com/go-boilerplate/models/entity"
+ "github.com/google/uuid"
+ "gorm.io/gorm"
+)
+
+type QuestionsRepository interface {
+ Create(ctx context.Context, q entity.Questions) error
+ Get(ctx context.Context, id uuid.UUID) (entity.Questions, error)
+ Update(ctx context.Context, q entity.Questions) error
+ Delete(ctx context.Context, id uuid.UUID) error
+ ListByProblemSet(ctx context.Context, problemSetId uuid.UUID) ([]entity.Questions, error)
+}
+
+type questionsRepository struct{ db *gorm.DB }
+
+func NewQuestionsRepository(db *gorm.DB) QuestionsRepository {
+ return &questionsRepository{db}
+}
+
+func (r *questionsRepository) Create(ctx context.Context, q entity.Questions) error {
+ return r.db.WithContext(ctx).Create(&q).Error
+}
+
+func (r *questionsRepository) Get(ctx context.Context, id uuid.UUID) (entity.Questions, error) {
+ var q entity.Questions
+ err := r.db.WithContext(ctx).First(&q, "id = ?", id).Error
+ return q, err
+}
+
+func (r *questionsRepository) Update(ctx context.Context, q entity.Questions) error {
+ return r.db.WithContext(ctx).
+ Model(&entity.Questions{}).
+ Where("id = ?", q.Id).
+ Updates(q).Error
+}
+
+func (r *questionsRepository) Delete(ctx context.Context, id uuid.UUID) error {
+ return r.db.WithContext(ctx).
+ Where("id = ?", id).
+ Delete(&entity.Questions{}).Error
+}
+
+func (r *questionsRepository) ListByProblemSet(ctx context.Context, problemSetId uuid.UUID) ([]entity.Questions, error) {
+ var q []entity.Questions
+ err := r.db.WithContext(ctx).
+ Where("problem_set_id = ?", problemSetId).
+ Order("id").
+ Find(&q).Error
+ return q, err
+}
diff --git a/repositories/result_repository.go b/repositories/result_repository.go
index b511c11d0302466575325b7415cc10928384b66c..83175827d82cc82efd6e6a41d2a90f3e94fa70ad 100644
--- a/repositories/result_repository.go
+++ b/repositories/result_repository.go
@@ -1,60 +1,60 @@
-package repositories
-
-import (
- "context"
-
- entity "abdanhafidz.com/go-boilerplate/models/entity"
- "github.com/google/uuid"
- "gorm.io/gorm"
-)
-
-type ResultRepository interface {
- Create(ctx context.Context, r *entity.Result) error
- GetById(ctx context.Context, id uuid.UUID) (entity.Result, error)
- Update(ctx context.Context, r *entity.Result) error
- ListByEvent(ctx context.Context, eventId uuid.UUID) ([]entity.Result, error)
- GetByAttemptId(ctx context.Context, attemptId uuid.UUID) (entity.Result, error)
-}
-
-type resultRepository struct{ db *gorm.DB }
-
-func NewResultRepository(db *gorm.DB) ResultRepository {
- return &resultRepository{db}
-}
-
-func (r *resultRepository) Create(ctx context.Context, rs *entity.Result) error {
- return r.db.WithContext(ctx).Create(rs).Error
-}
-func (r *resultRepository) GetByAttemptId(ctx context.Context, attemptId uuid.UUID) (entity.Result, error) {
- var rs entity.Result
- err := r.db.WithContext(ctx).
- Preload("ExamEventAttempt").
- First(&rs, "attempt_id = ?", attemptId).Error
- return rs, err
-}
-func (r *resultRepository) GetById(ctx context.Context, id uuid.UUID) (entity.Result, error) {
- var rs entity.Result
- err := r.db.WithContext(ctx).
- Preload("Account").
- Preload("Event").
- Preload("ProblemSet").
- Preload("ExamEventAttempt").
- First(&rs, "id = ?", id).Error
- return rs, err
-}
-
-func (r *resultRepository) Update(ctx context.Context, rs *entity.Result) error {
- return r.db.WithContext(ctx).
- Model(&entity.Result{}).
- Where("id = ?", rs.Id).
- Updates(rs).Error
-}
-
-func (r *resultRepository) ListByEvent(ctx context.Context, eventId uuid.UUID) ([]entity.Result, error) {
- var list []entity.Result
- err := r.db.WithContext(ctx).
- Where("event_id = ?", eventId).
- Preload("Account").
- Find(&list).Error
- return list, err
-}
+package repositories
+
+import (
+ "context"
+
+ entity "abdanhafidz.com/go-boilerplate/models/entity"
+ "github.com/google/uuid"
+ "gorm.io/gorm"
+)
+
+type ResultRepository interface {
+ Create(ctx context.Context, r *entity.Result) error
+ GetById(ctx context.Context, id uuid.UUID) (entity.Result, error)
+ Update(ctx context.Context, r *entity.Result) error
+ ListByEvent(ctx context.Context, eventId uuid.UUID) ([]entity.Result, error)
+ GetByAttemptId(ctx context.Context, attemptId uuid.UUID) (entity.Result, error)
+}
+
+type resultRepository struct{ db *gorm.DB }
+
+func NewResultRepository(db *gorm.DB) ResultRepository {
+ return &resultRepository{db}
+}
+
+func (r *resultRepository) Create(ctx context.Context, rs *entity.Result) error {
+ return r.db.WithContext(ctx).Create(rs).Error
+}
+func (r *resultRepository) GetByAttemptId(ctx context.Context, attemptId uuid.UUID) (entity.Result, error) {
+ var rs entity.Result
+ err := r.db.WithContext(ctx).
+ Preload("ExamEventAttempt").
+ First(&rs, "attempt_id = ?", attemptId).Error
+ return rs, err
+}
+func (r *resultRepository) GetById(ctx context.Context, id uuid.UUID) (entity.Result, error) {
+ var rs entity.Result
+ err := r.db.WithContext(ctx).
+ Preload("Account").
+ Preload("Event").
+ Preload("ProblemSet").
+ Preload("ExamEventAttempt").
+ First(&rs, "id = ?", id).Error
+ return rs, err
+}
+
+func (r *resultRepository) Update(ctx context.Context, rs *entity.Result) error {
+ return r.db.WithContext(ctx).
+ Model(&entity.Result{}).
+ Where("id = ?", rs.Id).
+ Updates(rs).Error
+}
+
+func (r *resultRepository) ListByEvent(ctx context.Context, eventId uuid.UUID) ([]entity.Result, error) {
+ var list []entity.Result
+ err := r.db.WithContext(ctx).
+ Where("event_id = ?", eventId).
+ Preload("Account").
+ Find(&list).Error
+ return list, err
+}
diff --git a/router/academy_router.go b/router/academy_router.go
index 2aa1f198ff3c0dc484390a67c518794b39676013..197dc28f5eaddef11bc48c65f47a41d49c052479 100644
--- a/router/academy_router.go
+++ b/router/academy_router.go
@@ -1,39 +1,45 @@
-package router
-
-import (
- "abdanhafidz.com/go-boilerplate/provider"
- "github.com/gin-contrib/gzip"
- "github.com/gin-gonic/gin"
-)
-
-func AcademyRouter(router *gin.Engine, middleware provider.MiddlewareProvider, controller provider.ControllerProvider) {
- academyController := controller.ProvideAcademyController()
- authenticationMiddleware := middleware.ProvideAuthenticationMiddleware()
-
- routerGroup := router.Group("/api/v1/academy")
-
- routerGroup.Use(gzip.Gzip(gzip.DefaultCompression))
-
- {
- // ADMIN
- adminGroup := routerGroup.Group("/admin", authenticationMiddleware.VerifyAccount)
- {
- // CRUD for Academies
- adminGroup.POST("/", academyController.CreateAcademy)
- adminGroup.GET("/id/:id/detail", academyController.GetAcademyDetail)
- adminGroup.PUT("/id/:id", academyController.UpdateAcademy)
- adminGroup.DELETE("/id/:id", academyController.DeleteAcademy)
-
- // Creation of Sub-resources
- adminGroup.POST("/materials", academyController.CreateMaterial)
- adminGroup.POST("/contents", academyController.CreateContent)
- }
-
- // USER
- routerGroup.GET("/", authenticationMiddleware.VerifyAccount, academyController.ListAcademies)
- routerGroup.GET("/:academy_slug", authenticationMiddleware.VerifyAccount, academyController.GetAcademy)
- routerGroup.GET("/:academy_slug/:material_slug", authenticationMiddleware.VerifyAccount, academyController.GetMaterial)
- routerGroup.GET("/:academy_slug/:material_slug/:order", authenticationMiddleware.VerifyAccount, academyController.GetContent)
- routerGroup.POST("/:academy_slug/:material_slug/:order", authenticationMiddleware.VerifyAccount, academyController.UpdateContentProgress)
- }
+package router
+
+import (
+ "abdanhafidz.com/go-boilerplate/provider"
+ "github.com/gin-contrib/gzip"
+ "github.com/gin-gonic/gin"
+)
+
+func AcademyRouter(router *gin.Engine, middleware provider.MiddlewareProvider, controller provider.ControllerProvider) {
+ academyController := controller.ProvideAcademyController()
+ authenticationMiddleware := middleware.ProvideAuthenticationMiddleware()
+ routerGroup := router.Group("/api/v1/academy")
+
+ routerGroup.Use(gzip.Gzip(gzip.DefaultCompression))
+
+ {
+ // ================= ADMIN SECTION =================
+ adminGroup := routerGroup.Group("/admin", authenticationMiddleware.VerifyAccount)
+ {
+ // CRUD for Academies
+ adminGroup.POST("/", academyController.CreateAcademy)
+ adminGroup.GET("/id/:id/detail", academyController.GetAcademyDetail)
+ adminGroup.PUT("/id/:id", academyController.UpdateAcademy)
+ adminGroup.DELETE("/id/:id", academyController.DeleteAcademy)
+
+ // Materials
+ adminGroup.POST("/materials", academyController.CreateMaterial)
+ adminGroup.DELETE("/materials/:id", academyController.DeleteMaterial)
+
+ // Contents
+ adminGroup.POST("/contents", academyController.CreateContent)
+ adminGroup.DELETE("/contents/:id", academyController.DeleteContent)
+ }
+
+ // ================= USER SECTION =================
+ // Public/Student endpoints (Authenticated)
+ routerGroup.GET("/", authenticationMiddleware.VerifyAccount, academyController.ListAcademies)
+ routerGroup.GET("/:academy_slug", authenticationMiddleware.VerifyAccount, academyController.GetAcademy)
+ routerGroup.GET("/:academy_slug/:material_slug", authenticationMiddleware.VerifyAccount, academyController.GetMaterial)
+ routerGroup.GET("/:academy_slug/:material_slug/:order", authenticationMiddleware.VerifyAccount, academyController.GetContent)
+
+ // Update Progress
+ routerGroup.POST("/:academy_slug/:material_slug/:order", authenticationMiddleware.VerifyAccount, academyController.UpdateContentProgress)
+ }
}
\ No newline at end of file
diff --git a/router/account_detail_router.go b/router/account_detail_router.go
index 3802e184a41946bdeffa9558ba3f7fe655463b07..dc040e8da7a32a305ed6871b9ad43d78e18769ba 100644
--- a/router/account_detail_router.go
+++ b/router/account_detail_router.go
@@ -1,18 +1,18 @@
-package router
-
-import (
- "abdanhafidz.com/go-boilerplate/provider"
- "github.com/gin-contrib/gzip"
- "github.com/gin-gonic/gin"
-)
-
-func AccountDetailRouter(router *gin.Engine, middleware provider.MiddlewareProvider, controller provider.ControllerProvider) {
- routerGroup := router.Group("/api/v1/account")
- accountDetailController := controller.ProvideAccountDetailController()
- authenticationMiddleware := middleware.ProvideAuthenticationMiddleware()
- routerGroup.Use(gzip.Gzip(gzip.DefaultCompression))
- {
- routerGroup.GET("/me", authenticationMiddleware.VerifyAccount, accountDetailController.GetDetail)
- routerGroup.PUT("/me", authenticationMiddleware.VerifyAccount, accountDetailController.UpdateDetail)
- }
-}
+package router
+
+import (
+ "abdanhafidz.com/go-boilerplate/provider"
+ "github.com/gin-contrib/gzip"
+ "github.com/gin-gonic/gin"
+)
+
+func AccountDetailRouter(router *gin.Engine, middleware provider.MiddlewareProvider, controller provider.ControllerProvider) {
+ routerGroup := router.Group("/api/v1/account")
+ accountDetailController := controller.ProvideAccountDetailController()
+ authenticationMiddleware := middleware.ProvideAuthenticationMiddleware()
+ routerGroup.Use(gzip.Gzip(gzip.DefaultCompression))
+ {
+ routerGroup.GET("/me", authenticationMiddleware.VerifyAccount, accountDetailController.GetDetail)
+ routerGroup.PUT("/me", authenticationMiddleware.VerifyAccount, accountDetailController.UpdateDetail)
+ }
+}
diff --git a/router/email_router.go b/router/email_router.go
index 29905f09076d143a1fce4ff62b8442f6bfde2491..ecd69e2e605fbed9ef083004450af9629198c391 100644
--- a/router/email_router.go
+++ b/router/email_router.go
@@ -1,17 +1,17 @@
-package router
-
-import (
- "abdanhafidz.com/go-boilerplate/provider"
- "github.com/gin-contrib/gzip"
- "github.com/gin-gonic/gin"
-)
-
-func EmailVerificationRouter(router *gin.Engine, controller provider.ControllerProvider) {
- emailVerificationController := controller.ProvideEmailVerificationController()
- routerGroup := router.Group("/api/v1/email")
- routerGroup.Use(gzip.Gzip(gzip.DefaultCompression))
- {
- routerGroup.POST("/verify", emailVerificationController.Validate)
- routerGroup.POST("/create-verification", emailVerificationController.Create)
- }
-}
+package router
+
+import (
+ "abdanhafidz.com/go-boilerplate/provider"
+ "github.com/gin-contrib/gzip"
+ "github.com/gin-gonic/gin"
+)
+
+func EmailVerificationRouter(router *gin.Engine, controller provider.ControllerProvider) {
+ emailVerificationController := controller.ProvideEmailVerificationController()
+ routerGroup := router.Group("/api/v1/email")
+ routerGroup.Use(gzip.Gzip(gzip.DefaultCompression))
+ {
+ routerGroup.POST("/verify", emailVerificationController.Validate)
+ routerGroup.POST("/create-verification", emailVerificationController.Create)
+ }
+}
diff --git a/router/event_router.go b/router/event_router.go
index 053bf1bd5233357b495bff9122f92c8a356bfde0..567f0fd905c7c8b1f34d4dca9efed0ad516974ef 100644
--- a/router/event_router.go
+++ b/router/event_router.go
@@ -1,19 +1,19 @@
-package router
-
-import (
- "abdanhafidz.com/go-boilerplate/provider"
- "github.com/gin-contrib/gzip"
- "github.com/gin-gonic/gin"
-)
-
-func EventRouter(router *gin.Engine, middleware provider.MiddlewareProvider, controller provider.ControllerProvider) {
- eventController := controller.ProvideEventController()
- authenticationMiddleware := middleware.ProvideAuthenticationMiddleware()
- routerGroup := router.Group("api/v1/events")
- routerGroup.Use(gzip.Gzip(gzip.DefaultCompression))
- {
- routerGroup.GET("/", authenticationMiddleware.VerifyAccount, eventController.List)
- routerGroup.GET("/:event_slug", authenticationMiddleware.VerifyAccount, eventController.DetailBySlug)
- routerGroup.POST("/register-event", authenticationMiddleware.VerifyAccount, eventController.Join)
- }
-}
+package router
+
+import (
+ "abdanhafidz.com/go-boilerplate/provider"
+ "github.com/gin-contrib/gzip"
+ "github.com/gin-gonic/gin"
+)
+
+func EventRouter(router *gin.Engine, middleware provider.MiddlewareProvider, controller provider.ControllerProvider) {
+ eventController := controller.ProvideEventController()
+ authenticationMiddleware := middleware.ProvideAuthenticationMiddleware()
+ routerGroup := router.Group("api/v1/events")
+ routerGroup.Use(gzip.Gzip(gzip.DefaultCompression))
+ {
+ routerGroup.GET("/", authenticationMiddleware.VerifyAccount, eventController.List)
+ routerGroup.GET("/:event_slug", authenticationMiddleware.VerifyAccount, eventController.DetailBySlug)
+ routerGroup.POST("/register-event", authenticationMiddleware.VerifyAccount, eventController.Join)
+ }
+}
diff --git a/router/exam_event_router.go b/router/exam_event_router.go
index 95b281d69550b3457d5881f1c19fcd3f32a99f85..9634ec19b582bd0f94d9878901d23636116fa087 100644
--- a/router/exam_event_router.go
+++ b/router/exam_event_router.go
@@ -1,20 +1,20 @@
-package router
-
-import (
- "abdanhafidz.com/go-boilerplate/provider"
- "github.com/gin-contrib/gzip"
- "github.com/gin-gonic/gin"
-)
-
-func ExamEventRouter(router *gin.Engine, middleware provider.MiddlewareProvider, controller provider.ControllerProvider) {
- examController := controller.ProvideExamController()
- auth := middleware.ProvideAuthenticationMiddleware()
- routerGroup := router.Group("api/v1/events")
- routerGroup.Use(gzip.Gzip(gzip.DefaultCompression))
- {
- routerGroup.GET("/:event_slug/exam", auth.VerifyAccount, examController.List)
- routerGroup.GET("/:event_slug/exam/:exam_slug/attempt", auth.VerifyAccount, examController.Attempt)
- routerGroup.POST("/:event_slug/exam/:attempt_id/answer_question", auth.VerifyAccount, examController.Answer)
- routerGroup.POST("/:event_slug/exam/:attempt_id/submit", auth.VerifyAccount, examController.Submit)
- }
-}
+package router
+
+import (
+ "abdanhafidz.com/go-boilerplate/provider"
+ "github.com/gin-contrib/gzip"
+ "github.com/gin-gonic/gin"
+)
+
+func ExamEventRouter(router *gin.Engine, middleware provider.MiddlewareProvider, controller provider.ControllerProvider) {
+ examController := controller.ProvideExamController()
+ auth := middleware.ProvideAuthenticationMiddleware()
+ routerGroup := router.Group("api/v1/events")
+ routerGroup.Use(gzip.Gzip(gzip.DefaultCompression))
+ {
+ routerGroup.GET("/:event_slug/exam", auth.VerifyAccount, examController.List)
+ routerGroup.GET("/:event_slug/exam/:exam_slug/attempt", auth.VerifyAccount, examController.Attempt)
+ routerGroup.POST("/:event_slug/exam/:attempt_id/answer_question", auth.VerifyAccount, examController.Answer)
+ routerGroup.POST("/:event_slug/exam/:attempt_id/submit", auth.VerifyAccount, examController.Submit)
+ }
+}
diff --git a/router/forgot_password_router.go b/router/forgot_password_router.go
index 1fe653ab05be75efa1acd76e477576ffb6c0b882..2960af2ec8b3e3839f631fdc70f01919c83f573d 100644
--- a/router/forgot_password_router.go
+++ b/router/forgot_password_router.go
@@ -1,16 +1,16 @@
-package router
-
-import (
- "abdanhafidz.com/go-boilerplate/provider"
- "github.com/gin-contrib/gzip"
- "github.com/gin-gonic/gin"
-)
-
-func ForgotPasswordRouter(router *gin.Engine, controller provider.ControllerProvider) {
- routerGroup := router.Group("/api/v1/forgot-password")
- forgotPasswordController := controller.ProvideForgotPasswordController()
- routerGroup.Use(gzip.Gzip(gzip.DefaultCompression))
- {
- routerGroup.POST("/", forgotPasswordController.Request)
- }
-}
+package router
+
+import (
+ "abdanhafidz.com/go-boilerplate/provider"
+ "github.com/gin-contrib/gzip"
+ "github.com/gin-gonic/gin"
+)
+
+func ForgotPasswordRouter(router *gin.Engine, controller provider.ControllerProvider) {
+ routerGroup := router.Group("/api/v1/forgot-password")
+ forgotPasswordController := controller.ProvideForgotPasswordController()
+ routerGroup.Use(gzip.Gzip(gzip.DefaultCompression))
+ {
+ routerGroup.POST("/", forgotPasswordController.Request)
+ }
+}
diff --git a/router/option_router.go b/router/option_router.go
index 9f11bf604aed507c4d49199b0627b06accc93c07..eaad020f4268a8609a87083376b4b2dc116954c5 100644
--- a/router/option_router.go
+++ b/router/option_router.go
@@ -1,22 +1,22 @@
-package router
-
-import (
- "abdanhafidz.com/go-boilerplate/provider"
- "github.com/gin-contrib/gzip"
- "github.com/gin-gonic/gin"
-)
-
-func OptionsRouter(router *gin.Engine, controller provider.ControllerProvider) {
- optionsController := controller.ProvideOptionController()
- regionController := controller.ProvideRegionController()
- routerGroup := router.Group("/api/v1/options")
- routerGroup.Use(gzip.Gzip(gzip.DefaultCompression))
- {
- routerGroup.POST("/create", optionsController.CreateBulk)
- routerGroup.GET("/list/:slug", optionsController.GetBySlug)
- routerGroup.GET("/region/provinces", regionController.ListProvinces)
- routerGroup.GET("/region/cities", regionController.ListCitiesByProvince)
- routerGroup.POST("/region/seed-provinces", regionController.SeedProvinces)
- routerGroup.POST("/region/seed-cities", regionController.SeedCities)
- }
-}
+package router
+
+import (
+ "abdanhafidz.com/go-boilerplate/provider"
+ "github.com/gin-contrib/gzip"
+ "github.com/gin-gonic/gin"
+)
+
+func OptionsRouter(router *gin.Engine, controller provider.ControllerProvider) {
+ optionsController := controller.ProvideOptionController()
+ regionController := controller.ProvideRegionController()
+ routerGroup := router.Group("/api/v1/options")
+ routerGroup.Use(gzip.Gzip(gzip.DefaultCompression))
+ {
+ routerGroup.POST("/create", optionsController.CreateBulk)
+ routerGroup.GET("/list/:slug", optionsController.GetBySlug)
+ routerGroup.GET("/region/provinces", regionController.ListProvinces)
+ routerGroup.GET("/region/cities", regionController.ListCitiesByProvince)
+ routerGroup.POST("/region/seed-provinces", regionController.SeedProvinces)
+ routerGroup.POST("/region/seed-cities", regionController.SeedCities)
+ }
+}
diff --git a/router/router.go b/router/router.go
index d4f247c5cf603ab8ca6e2396725b888c5acb6afd..620387c98a8dd6d70cdb7630f1173e7b0580c44f 100644
--- a/router/router.go
+++ b/router/router.go
@@ -14,5 +14,6 @@ func RunRouter(appProvider provider.AppProvider) {
OptionsRouter(router, controller)
AcademyRouter(router, middleware, controller)
ExamEventRouter(router, middleware, controller)
+ UploadRouter(router,middleware, controller)
router.Run(config.ProvideEnvConfig().GetTCPAddress())
}
diff --git a/router/upload_router.go b/router/upload_router.go
new file mode 100644
index 0000000000000000000000000000000000000000..0141f58906da86f3225fe037855d05f42a0049e5
--- /dev/null
+++ b/router/upload_router.go
@@ -0,0 +1,20 @@
+package router
+
+import (
+ "abdanhafidz.com/go-boilerplate/provider"
+ "github.com/gin-contrib/gzip"
+ "github.com/gin-gonic/gin"
+)
+
+func UploadRouter(r *gin.Engine, middleware provider.MiddlewareProvider, controller provider.ControllerProvider) {
+ uploadController := controller.ProvideUploadController()
+ authenticationMiddleware := middleware.ProvideAuthenticationMiddleware()
+
+ routerGroup := r.Group("/api/v1/files")
+ routerGroup.Use(gzip.Gzip(gzip.DefaultCompression), authenticationMiddleware.VerifyAccount)
+
+ {
+ routerGroup.POST("/", uploadController.Upload)
+ routerGroup.GET("/:id", uploadController.GetFileByID)
+ }
+}
\ No newline at end of file
diff --git a/services/academy_service.go b/services/academy_service.go
index 1b403954554dadb90d2ede3338613bf8d9a72d68..0f2e9a9442aae531b5c4f418a7df9776d57c31f4 100644
--- a/services/academy_service.go
+++ b/services/academy_service.go
@@ -1,321 +1,442 @@
-package services
-
-import (
- "context"
- "errors"
- "strings"
- "time"
-
- "abdanhafidz.com/go-boilerplate/models/dto"
- entity "abdanhafidz.com/go-boilerplate/models/entity"
- "abdanhafidz.com/go-boilerplate/repositories"
- "abdanhafidz.com/go-boilerplate/utils"
- "github.com/google/uuid"
- "github.com/gosimple/slug"
-)
-
-type AcademyService interface {
- // Academy
- GetAcademy(ctx context.Context, accountId uuid.UUID, slug string) (entity.Academy, error)
- GetAcademyDetail(ctx context.Context, id uuid.UUID) (entity.Academy, error)
-
- CreateAcademy(ctx context.Context, req dto.CreateAcademyRequest) (entity.Academy, error)
- UpdateAcademy(ctx context.Context, id uuid.UUID, req dto.UpdateAcademyRequest) (entity.Academy, error)
- DeleteAcademy(ctx context.Context, id uuid.UUID) error
-
- ListAcademies(ctx context.Context, accountId uuid.UUID) ([]entity.Academy, error)
-
- // Material
- CreateMaterial(ctx context.Context, req dto.CreateMaterialRequest) (entity.AcademyMaterial, error)
- GetMaterial(ctx context.Context, accountId uuid.UUID, academySlug string, materialSlug string) (entity.AcademyMaterial, error)
-
- // Content
- CreateContent(ctx context.Context, req dto.CreateContentRequest) (entity.AcademyContent, error)
- GetContent(ctx context.Context, accountId uuid.UUID, academySlug string, materialSlug string, order uint) (entity.AcademyContent, error)
-
- // Progress
- UpdateContentProgress(ctx context.Context, accountId uuid.UUID, academySlug string, materialSlug string, order uint) (entity.AcademyContentProgress, entity.AcademyMaterialProgress, entity.AcademyProgress, error)
- UpdateMaterialProgress(ctx context.Context, accountId uuid.UUID, academy entity.Academy, material entity.AcademyMaterial) (entity.AcademyMaterialProgress, error)
- UpdateAcademyProgress(ctx context.Context, accountId uuid.UUID, academy entity.Academy) (entity.AcademyProgress, error)
-}
-type academyService struct {
- repo repositories.AcademyRepository
-}
-
-func NewAcademyService(repo repositories.AcademyRepository) AcademyService {
- return &academyService{repo: repo}
-}
-
-//
-// ===== Academy =====
-//
-
-func (s *academyService) GetAcademy(ctx context.Context, accountId uuid.UUID, slug string) (entity.Academy, error) {
- return s.repo.GetAcademyWithProgress(ctx, accountId, slug)
-}
-
-func (s *academyService) GetAcademyDetail(ctx context.Context, id uuid.UUID) (entity.Academy, error) {
- a, _, err := s.repo.GetAcademyWithMaterials(ctx, id)
- return a, err
-}
-
-func (s *academyService) CreateAcademy(ctx context.Context, req dto.CreateAcademyRequest) (entity.Academy, error) {
- if strings.TrimSpace(req.Title) == "" {
- return entity.Academy{}, errors.New("title required")
- }
-
- slugVal := req.Slug
- if slugVal == "" {
- slugVal = slug.Make(req.Title)
- }
-
- if _, err := s.repo.GetAcademyBySlug(ctx, slugVal); err == nil {
- return entity.Academy{}, errors.New("slug already exists")
- }
-
- a := entity.Academy{
- Id: uuid.New(),
- Title: req.Title,
- Slug: slugVal,
- Description: req.Description,
- ImageUrl: req.ImageUrl,
- MaterialsCount: 0,
- }
-
- return s.repo.CreateAcademy(ctx, a)
-}
-
-func (s *academyService) UpdateAcademy(ctx context.Context, id uuid.UUID, req dto.UpdateAcademyRequest) (entity.Academy, error) {
- existing, err := s.repo.GetAcademyByID(ctx, id)
- if err != nil {
- return entity.Academy{}, errors.New("academy not found")
- }
-
- if req.Title != "" {
- existing.Title = req.Title
- }
- if req.Description != "" {
- existing.Description = req.Description
- }
-
- if req.Slug != "" {
- existing.Slug = req.Slug
- } else {
- existing.Slug = slug.Make(existing.Title)
- }
-
- return s.repo.UpdateAcademy(ctx, existing)
-}
-
-func (s *academyService) DeleteAcademy(ctx context.Context, id uuid.UUID) error {
- _, mats, err := s.repo.GetAcademyWithMaterials(ctx, id)
- if err != nil {
- return errors.New("academy not found")
- }
- if len(mats) > 0 {
- return errors.New("cannot delete academy with materials")
- }
- return s.repo.DeleteAcademy(ctx, id)
-}
-
-func (s *academyService) ListAcademies(ctx context.Context, accountId uuid.UUID) ([]entity.Academy, error) {
- return s.repo.ListAcademy(ctx, accountId)
-}
-
-//
-// ===== Material =====
-//
-
-func (s *academyService) GetMaterial(ctx context.Context, accountId uuid.UUID, academySlug string, materialSlug string) (entity.AcademyMaterial, error) {
- if strings.TrimSpace(academySlug) == "" || strings.TrimSpace(materialSlug) == "" {
- return entity.AcademyMaterial{}, errors.New("slug required")
- }
- academy, err := s.repo.GetAcademyBySlug(ctx, academySlug)
- if err != nil {
- return entity.AcademyMaterial{}, errors.New("academy not found: " + err.Error())
- }
-
- academyId := academy.Id
- return s.repo.GetMaterialWithProgress(ctx,accountId, academyId, materialSlug)
-}
-
-func (s *academyService) CreateMaterial(ctx context.Context, req dto.CreateMaterialRequest) (entity.AcademyMaterial, error) {
- if req.AcademyId == uuid.Nil {
- return entity.AcademyMaterial{}, errors.New("academy_id required")
- }
- if _, err := s.repo.GetAcademyByID(ctx, req.AcademyId); err != nil {
- return entity.AcademyMaterial{}, errors.New("academy not found")
- }
-
- slugVal := req.Slug
- if slugVal == "" {
- slugVal = slug.Make(req.Title)
- }
-
- orderCount, _ := s.repo.CountMaterialsByAcademyID(ctx, req.AcademyId)
- order := uint(orderCount + 1)
-
- m := entity.AcademyMaterial{
- Id: uuid.New(),
- AcademyId: req.AcademyId,
- Title: req.Title,
- Slug: slugVal,
- Description: req.Description,
- Order: order,
- ContentsCount: 0,
- }
-
- // Update total materials in academy
- a, _ := s.repo.GetAcademyByID(ctx, req.AcademyId)
- a.MaterialsCount = a.MaterialsCount + 1
- s.repo.UpdateAcademy(ctx, a)
-
- return s.repo.CreateMaterial(ctx, m)
-}
-
-//
-// ===== Content =====
-//
-
-func (s *academyService) CreateContent(ctx context.Context, req dto.CreateContentRequest) (entity.AcademyContent, error) {
- if req.MaterialId == uuid.Nil {
- return entity.AcademyContent{}, errors.New("academy_material_id required")
- }
-
- if _, err := s.repo.GetMaterialByID(ctx, req.MaterialId); err != nil {
- return entity.AcademyContent{}, errors.New("material not found")
- }
-
- // auto order last++
- count, _ := s.repo.CountContentsByMaterialID(ctx, req.MaterialId)
- order := uint(count + 1)
- Id := uuid.New()
-
- c := entity.AcademyContent{
- Id: Id,
- MaterialId: req.MaterialId,
- Title: req.Title,
- Contents: req.Contents,
- Order: order,
- }
-
- // Update total progress in material
- m, _ := s.repo.GetMaterialByID(ctx, req.MaterialId)
- m.ContentsCount = int64(order)
- s.repo.UpdateMaterial(ctx, m)
-
- acp := entity.AcademyContentProgress{
- Status : "NOT_STARTED",
- }
- c.AcademyContentProgress = acp
-
- return s.repo.CreateContent(ctx, c)
-}
-
-func (s *academyService) GetContent(ctx context.Context, accountId uuid.UUID, academySlug string, materialSlug string, order uint) (entity.AcademyContent, error) {
- material, err := s.GetMaterial(ctx,accountId, academySlug, materialSlug)
- if err != nil {
- return entity.AcademyContent{}, errors.New("material not found")
- }
- materialId := material.Id
- academyId := material.AcademyId
-
- return s.repo.GetContentWithProgress(ctx,accountId, academyId, materialId,order)
-}
-
-// Progress
-func (s *academyService) UpdateAcademyProgress(ctx context.Context, accountId uuid.UUID, academy entity.Academy) (entity.AcademyProgress, error) {
- //Count total completed materials for academy progress update
- totalMaterialsCompleted, _ := s.repo.CountCompletedMaterialsByAcademyAndAccount(ctx, accountId, academy.Id)
- status := "IN_PROGRESS"
- var completedAt *time.Time
- if totalMaterialsCompleted == academy.MaterialsCount {
- status = "COMPLETED"
- completedAt = utils.Ptr(time.Now())
- }
- ap := entity.AcademyProgress{
- Id: uuid.New(),
- AccountId: accountId,
- AcademyId: academy.Id,
- Progress: float64((float64(totalMaterialsCompleted) / float64(academy.MaterialsCount) * 100)),
- TotalCompletedMaterials: uint(totalMaterialsCompleted),
- Status: status,
- CompletedAt: completedAt,
- }
- _, err := s.repo.UpsertAcademyProgress(ctx, ap)
- return ap, err
-}
-
-func (s *academyService) UpdateMaterialProgress(ctx context.Context, accountId uuid.UUID, academy entity.Academy, material entity.AcademyMaterial) (entity.AcademyMaterialProgress, error) {
- // Count total completed contents for material progress update
- totalContentsCompleted, _ := s.repo.CountCompletedContentsByMaterialAndAccount(ctx, accountId, material.Id)
- m, err := s.repo.GetMaterialByID(ctx, material.Id)
- if err != nil {
- return entity.AcademyMaterialProgress{}, errors.New("material not found")
- }
- status := "IN_PROGRESS"
- var completedAt *time.Time
- if totalContentsCompleted == m.ContentsCount {
- status = "COMPLETED"
- completedAt = utils.Ptr(time.Now())
- }
-
- amp := entity.AcademyMaterialProgress{
- Id: uuid.New(),
- AccountId: accountId,
- AcademyId: academy.Id,
- MaterialId: material.Id,
- Progress: float64((float64(totalContentsCompleted) / float64(m.ContentsCount)) * 100),
- TotalCompletedContents: uint(totalContentsCompleted),
- Status: status,
- CompletedAt: completedAt,
- }
-
- _, err = s.repo.UpsertMaterialProgress(ctx, amp)
- return amp, err
-}
-
-func (s *academyService) UpdateContentProgress(ctx context.Context, accountId uuid.UUID, academySlug string, materialSlug string, order uint) (entity.AcademyContentProgress, entity.AcademyMaterialProgress, entity.AcademyProgress, error) {
- academy, err := s.repo.GetAcademyBySlug(ctx, academySlug)
-
- if err != nil {
- return entity.AcademyContentProgress{}, entity.AcademyMaterialProgress{}, entity.AcademyProgress{}, errors.New("academy not found")
- }
-
- material, err := s.repo.GetMaterialBySlug(ctx, academy.Id, materialSlug)
-
- if err != nil {
- return entity.AcademyContentProgress{}, entity.AcademyMaterialProgress{}, entity.AcademyProgress{}, errors.New("material not found")
- }
-
- content, err := s.repo.GetContentBySlug(ctx, material.Id, order)
-
- if err != nil {
- return entity.AcademyContentProgress{}, entity.AcademyMaterialProgress{}, entity.AcademyProgress{}, errors.New("content not found")
- }
-
- acp := entity.AcademyContentProgress{
- Id: uuid.New(),
- AccountId: accountId,
- AcademyId: academy.Id,
- MaterialId: material.Id,
- ContentId: content.Id,
- Status: "COMPLETED",
- CompletedAt: utils.Ptr(time.Now()),
- }
-
- _, err = s.repo.UpsertContentProgress(ctx, acp)
- if err != nil {
- return entity.AcademyContentProgress{}, entity.AcademyMaterialProgress{}, entity.AcademyProgress{}, errors.New("failed to upsert content progress: " + err.Error())
- }
- amp, err := s.UpdateMaterialProgress(ctx, accountId, academy, material)
- if err != nil {
- return entity.AcademyContentProgress{}, entity.AcademyMaterialProgress{}, entity.AcademyProgress{}, errors.New("failed to update material progress: " + err.Error())
- }
- ap, err := s.UpdateAcademyProgress(ctx, accountId, academy)
- if err != nil {
- return entity.AcademyContentProgress{}, entity.AcademyMaterialProgress{}, entity.AcademyProgress{}, errors.New("failed to update academy progress: " + err.Error())
- }
-
- return acp, amp, ap, nil
-}
+package services
+
+import (
+ "context"
+ "math"
+ "strings"
+ "time"
+
+ "github.com/google/uuid"
+ "github.com/gosimple/slug"
+
+ "abdanhafidz.com/go-boilerplate/models/dto"
+ entity "abdanhafidz.com/go-boilerplate/models/entity"
+ http_error "abdanhafidz.com/go-boilerplate/models/error"
+ "abdanhafidz.com/go-boilerplate/repositories"
+ "abdanhafidz.com/go-boilerplate/utils"
+)
+
+type AcademyRepositoryExtensions interface {
+ BatchRecalculateAcademyProgress(ctx context.Context, academyId uuid.UUID) error
+ BatchRecalculateMaterialProgress(ctx context.Context, materialId uuid.UUID) error
+}
+
+type AcademyService interface {
+ GetAcademy(ctx context.Context, accountId uuid.UUID, slug string) (entity.Academy, error)
+ GetAcademyDetail(ctx context.Context, id uuid.UUID) (entity.Academy, error)
+ CreateAcademy(ctx context.Context, req dto.CreateAcademyRequest) (entity.Academy, error)
+ UpdateAcademy(ctx context.Context, id uuid.UUID, req dto.UpdateAcademyRequest) (entity.Academy, error)
+ DeleteAcademy(ctx context.Context, id uuid.UUID) error
+ ListAcademies(ctx context.Context, accountId uuid.UUID) ([]entity.Academy, error)
+
+ CreateMaterial(ctx context.Context, req dto.CreateMaterialRequest) (entity.AcademyMaterial, error)
+ GetMaterial(ctx context.Context, accountId uuid.UUID, academySlug string, materialSlug string) (entity.AcademyMaterial, error)
+ DeleteMaterial(ctx context.Context, id uuid.UUID) error
+
+ CreateContent(ctx context.Context, req dto.CreateContentRequest) (entity.AcademyContent, error)
+ GetContent(ctx context.Context, accountId uuid.UUID, academySlug string, materialSlug string, order uint) (entity.AcademyContent, error)
+ DeleteContent(ctx context.Context, id uuid.UUID) error
+
+ UpdateContentProgress(ctx context.Context, accountId uuid.UUID, academySlug string, materialSlug string, order uint) (entity.AcademyContentProgress, entity.AcademyMaterialProgress, entity.AcademyProgress, error)
+}
+
+type academyService struct {
+ academyRepo repositories.AcademyRepository
+}
+
+func NewAcademyService(academyRepo repositories.AcademyRepository) AcademyService {
+ return &academyService{academyRepo: academyRepo}
+}
+
+// ================= ACADEMY =================
+
+func (s *academyService) GetAcademy(ctx context.Context, accountId uuid.UUID, slug string) (entity.Academy, error) {
+ return s.academyRepo.GetAcademyWithProgress(ctx, accountId, slug)
+}
+
+func (s *academyService) GetAcademyDetail(ctx context.Context, id uuid.UUID) (entity.Academy, error) {
+ a, _, err := s.academyRepo.GetAcademyWithMaterials(ctx, id)
+ return a, err
+}
+
+func (s *academyService) CreateAcademy(ctx context.Context, req dto.CreateAcademyRequest) (entity.Academy, error) {
+ if strings.TrimSpace(req.Title) == "" {
+ return entity.Academy{}, http_error.TITLE_REQUIRED
+ }
+ slugVal := req.Slug
+ if slugVal == "" {
+ slugVal = slug.Make(req.Title)
+ }
+ if _, err := s.academyRepo.GetAcademyBySlug(ctx, slugVal); err == nil {
+ return entity.Academy{}, http_error.DUPLICATE_DATA
+ }
+ a := entity.Academy{
+ Id: uuid.New(),
+ Title: req.Title,
+ Slug: slugVal,
+ Description: req.Description,
+ ImageUrl: req.ImageUrl,
+ MaterialsCount: 0,
+ }
+ return s.academyRepo.CreateAcademy(ctx, a)
+}
+
+func (s *academyService) UpdateAcademy(ctx context.Context, id uuid.UUID, req dto.UpdateAcademyRequest) (entity.Academy, error) {
+ existing, err := s.academyRepo.GetAcademyByID(ctx, id)
+ if err != nil {
+ return entity.Academy{}, http_error.ACADEMY_NOT_FOUND
+ }
+ if req.Title != "" {
+ existing.Title = req.Title
+ }
+ if req.Description != "" {
+ existing.Description = req.Description
+ }
+ if req.Slug != "" {
+ existing.Slug = req.Slug
+ }
+ return s.academyRepo.UpdateAcademy(ctx, existing)
+}
+
+func (s *academyService) DeleteAcademy(ctx context.Context, id uuid.UUID) error {
+ _, mats, err := s.academyRepo.GetAcademyWithMaterials(ctx, id)
+ if err != nil {
+ return http_error.ACADEMY_NOT_FOUND
+ }
+ if len(mats) > 0 {
+ return http_error.ACADEMY_HAS_MATERIALS
+ }
+ return s.academyRepo.DeleteAcademy(ctx, id)
+}
+
+func (s *academyService) ListAcademies(ctx context.Context, accountId uuid.UUID) ([]entity.Academy, error) {
+ // Logic list tetap sama, namun karena progress di DB sudah konsisten (akibat recalculate),
+ // kita tidak perlu sanitasi manual yang berat di sini.
+ return s.academyRepo.ListAcademy(ctx, accountId)
+}
+
+// ================= MATERIAL (CRITICAL LOGIC HERE) =================
+
+func (s *academyService) GetMaterial(ctx context.Context, accountId uuid.UUID, academySlug string, materialSlug string) (entity.AcademyMaterial, error) {
+ if strings.TrimSpace(academySlug) == "" || strings.TrimSpace(materialSlug) == "" {
+ return entity.AcademyMaterial{}, http_error.SLUG_REQUIRED
+ }
+ academy, err := s.academyRepo.GetAcademyBySlug(ctx, academySlug)
+ if err != nil {
+ return entity.AcademyMaterial{}, http_error.ACADEMY_NOT_FOUND
+ }
+
+ return s.academyRepo.GetMaterialWithProgress(ctx, accountId, academy.Id, materialSlug)
+}
+
+func (s *academyService) CreateMaterial(ctx context.Context, req dto.CreateMaterialRequest) (entity.AcademyMaterial, error) {
+ if req.AcademyId == uuid.Nil {
+ return entity.AcademyMaterial{}, http_error.ACADEMY_ID_REQUIRED
+ }
+
+ slugVal := req.Slug
+ if slugVal == "" {
+ slugVal = slug.Make(req.Title)
+ }
+
+ var createdMaterial entity.AcademyMaterial
+
+ err := s.academyRepo.Atomic(ctx, func(txRepo repositories.AcademyRepository) error {
+ orderCount, _ := txRepo.CountMaterialsByAcademyID(ctx, req.AcademyId)
+
+ m := entity.AcademyMaterial{
+ Id: uuid.New(),
+ AcademyId: req.AcademyId,
+ Title: req.Title,
+ Slug: slugVal,
+ Description: req.Description,
+ Order: uint(orderCount + 1),
+ ContentsCount: 0,
+ }
+
+ // 1. Create Material
+ res, err := txRepo.CreateMaterial(ctx, m)
+ if err != nil {
+ return err
+ }
+ createdMaterial = res
+
+ // 2. Update Parent Count (Academy)
+ realCount, err := txRepo.CountMaterialsByAcademyID(ctx, req.AcademyId)
+ if err != nil {
+ return err
+ }
+ academy, _ := txRepo.GetAcademyByID(ctx, req.AcademyId)
+ academy.MaterialsCount = int64(realCount)
+ if _, err := txRepo.UpdateAcademy(ctx, academy); err != nil {
+ return err
+ }
+
+ // 3. VALIDASI KRUSIAL: Recalculate Progress User
+ // Karena MaterialsCount bertambah (misal 3 -> 4), user yang sebelumnya 100% (3/3)
+ // sekarang menjadi 75% (3/4). Status harus berubah jadi 'InProgress'.
+ if err := txRepo.BatchRecalculateAcademyProgress(ctx, req.AcademyId); err != nil {
+ return err
+ }
+
+ return nil
+ })
+
+ if err != nil {
+ return entity.AcademyMaterial{}, err
+ }
+
+ return createdMaterial, nil
+}
+
+func (s *academyService) DeleteMaterial(ctx context.Context, id uuid.UUID) error {
+ m, err := s.academyRepo.GetMaterialByID(ctx, id)
+ if err != nil {
+ return http_error.MATERIAL_NOT_FOUND
+ }
+
+ return s.academyRepo.Atomic(ctx, func(txRepo repositories.AcademyRepository) error {
+ // 1. Cleanup Child Progress & Content
+ if err := txRepo.DeleteContentProgressByMaterialID(ctx, id); err != nil { return err }
+ if err := txRepo.DeleteMaterialProgressByMaterialID(ctx, id); err != nil { return err }
+
+ // 2. Delete Material
+ if err := txRepo.DeleteMaterial(ctx, id); err != nil { return err }
+
+ // 3. Reorder
+ if err := txRepo.DecrementMaterialOrdersGreaterThan(ctx, m.AcademyId, m.Order); err != nil { return err }
+
+ // 4. Update Parent Count
+ realCount, err := txRepo.CountMaterialsByAcademyID(ctx, m.AcademyId)
+ if err != nil { return err }
+
+ academy, _ := txRepo.GetAcademyByID(ctx, m.AcademyId)
+ academy.MaterialsCount = int64(realCount)
+ if _, err := txRepo.UpdateAcademy(ctx, academy); err != nil { return err }
+
+ // 5. VALIDASI KRUSIAL: Recalculate Progress
+ // Karena MaterialsCount berkurang (misal 4 -> 3), user yang sebelumnya 75% (3/4)
+ // sekarang menjadi 100% (3/3). Status harus berubah jadi 'Completed'.
+ if err := txRepo.BatchRecalculateAcademyProgress(ctx, m.AcademyId); err != nil { return err }
+
+ return nil
+ })
+}
+
+// ================= CONTENT (CRITICAL LOGIC HERE) =================
+
+func (s *academyService) CreateContent(ctx context.Context, req dto.CreateContentRequest) (entity.AcademyContent, error) {
+ if req.MaterialId == uuid.Nil {
+ return entity.AcademyContent{}, http_error.MATERIAL_ID_REQUIRED
+ }
+
+ var createdContent entity.AcademyContent
+
+ err := s.academyRepo.Atomic(ctx, func(txRepo repositories.AcademyRepository) error {
+ m, err := txRepo.GetMaterialByID(ctx, req.MaterialId)
+ if err != nil { return http_error.MATERIAL_NOT_FOUND }
+
+ count, _ := txRepo.CountContentsByMaterialID(ctx, req.MaterialId)
+
+ c := entity.AcademyContent{
+ Id: uuid.New(),
+ MaterialId: req.MaterialId,
+ Title: req.Title,
+ Contents: req.Contents,
+ Order: uint(count + 1),
+ // Progress status untuk content baru default-nya NotStarted
+ }
+
+ // 1. Create Content
+ res, err := txRepo.CreateContent(ctx, c)
+ if err != nil { return err }
+ createdContent = res
+
+ // 2. Update Parent Count (Material)
+ realCount, err := txRepo.CountContentsByMaterialID(ctx, req.MaterialId)
+ if err != nil { return err }
+
+ m.ContentsCount = realCount
+ if _, err := txRepo.UpdateMaterial(ctx, m); err != nil { return err }
+
+ // 3. VALIDASI KRUSIAL: Recalculate Progress Material
+ // Konten bertambah -> Material user yang 'Completed' harus jadi 'InProgress'.
+ // Ini juga akan men-trigger efek domino ke Academy Progress (accumulated value berubah).
+ if err := txRepo.BatchRecalculateMaterialProgress(ctx, req.MaterialId); err != nil { return err }
+
+ // Update Academy juga karena bobot material berubah
+ if err := txRepo.BatchRecalculateAcademyProgress(ctx, m.AcademyId); err != nil { return err }
+
+ return nil
+ })
+
+ return createdContent, err
+}
+
+func (s *academyService) GetContent(ctx context.Context, accountId uuid.UUID, academySlug string, materialSlug string, order uint) (entity.AcademyContent, error) {
+ material, err := s.GetMaterial(ctx, accountId, academySlug, materialSlug)
+ if err != nil {
+ return entity.AcademyContent{}, err
+ }
+ return s.academyRepo.GetContentWithProgress(ctx, accountId, material.AcademyId, material.Id, order)
+}
+
+func (s *academyService) DeleteContent(ctx context.Context, id uuid.UUID) error {
+ c, err := s.academyRepo.GetContentByID(ctx, id)
+ if err != nil {
+ return http_error.CONTENT_NOT_FOUND
+ }
+
+ return s.academyRepo.Atomic(ctx, func(txRepo repositories.AcademyRepository) error {
+ // 1. Delete Progress & Content
+ if err := txRepo.DeleteContentProgressByContentID(ctx, id); err != nil { return err }
+ if err := txRepo.DeleteContent(ctx, id); err != nil { return err }
+
+ // 2. Reorder
+ if err := txRepo.DecrementContentOrdersGreaterThan(ctx, c.MaterialId, c.Order); err != nil { return err }
+
+ // 3. Update Parent Count (Material)
+ realCount, err := txRepo.CountContentsByMaterialID(ctx, c.MaterialId)
+ if err != nil { return err }
+
+ material, _ := txRepo.GetMaterialByID(ctx, c.MaterialId)
+ material.ContentsCount = realCount
+ if _, err := txRepo.UpdateMaterial(ctx, material); err != nil { return err }
+
+ // 4. VALIDASI KRUSIAL: Recalculate
+ // Konten berkurang -> Material user 'InProgress' bisa jadi 'Completed'.
+ if err := txRepo.BatchRecalculateMaterialProgress(ctx, c.MaterialId); err != nil { return err }
+
+ // Update Academy juga
+ if err := txRepo.BatchRecalculateAcademyProgress(ctx, material.AcademyId); err != nil { return err }
+
+ return nil
+ })
+}
+
+// ================= UPDATE PROGRESS (USER INTERACTION) =================
+// Logic ini dijalankan saat user menyelesaikan satu konten.
+
+func (s *academyService) UpdateContentProgress(ctx context.Context, accountId uuid.UUID, academySlug string, materialSlug string, order uint) (entity.AcademyContentProgress, entity.AcademyMaterialProgress, entity.AcademyProgress, error) {
+ academy, err := s.academyRepo.GetAcademyBySlug(ctx, academySlug)
+ if err != nil {
+ return entity.AcademyContentProgress{}, entity.AcademyMaterialProgress{}, entity.AcademyProgress{}, http_error.ACADEMY_NOT_FOUND
+ }
+ material, err := s.academyRepo.GetMaterialBySlug(ctx, academy.Id, materialSlug)
+ if err != nil {
+ return entity.AcademyContentProgress{}, entity.AcademyMaterialProgress{}, entity.AcademyProgress{}, http_error.MATERIAL_NOT_FOUND
+ }
+ content, err := s.academyRepo.GetContentBySlug(ctx, material.Id, order)
+ if err != nil {
+ return entity.AcademyContentProgress{}, entity.AcademyMaterialProgress{}, entity.AcademyProgress{}, http_error.CONTENT_NOT_FOUND
+ }
+
+ var acp entity.AcademyContentProgress
+ var amp entity.AcademyMaterialProgress
+ var ap entity.AcademyProgress
+
+ err = s.academyRepo.Atomic(ctx, func(txRepo repositories.AcademyRepository) error {
+
+ // 1. Tandai Konten Selesai
+ existingACP, _ := txRepo.GetContentProgress(ctx, accountId, academy.Id, material.Id, content.Id)
+ acpID := existingACP.Id
+ if acpID == uuid.Nil { acpID = uuid.New() }
+
+ acp = entity.AcademyContentProgress{
+ Id: acpID,
+ AccountId: accountId,
+ AcademyId: academy.Id,
+ MaterialId: material.Id,
+ ContentId: content.Id,
+ Status: entity.StatusCompleted,
+ CompletedAt: utils.Ptr(time.Now()),
+ }
+ if _, err := txRepo.UpsertContentProgress(ctx, acp); err != nil { return err }
+
+ totalContentsCompleted, _ := txRepo.CountCompletedContentsByMaterialAndAccount(ctx, accountId, material.Id)
+ m, _ := txRepo.GetMaterialByID(ctx, material.Id) // Ambil count terbaru
+
+ matStatus := entity.StatusInProgress
+ var matCompletedAt *time.Time
+ progressPct := 0.0
+
+ if m.ContentsCount > 0 {
+ progressPct = (float64(totalContentsCompleted) / float64(m.ContentsCount)) * 100
+ progressPct = math.Round(progressPct*100) / 100
+ if totalContentsCompleted >= m.ContentsCount {
+ matStatus = entity.StatusCompleted
+ matCompletedAt = utils.Ptr(time.Now())
+ progressPct = 100 // Force 100
+ }
+ } else {
+ // Edge case: Material tanpa konten dianggap completed
+ matStatus = entity.StatusCompleted
+ progressPct = 100
+ }
+
+ existingAMP, _ := txRepo.GetMaterialProgress(ctx, accountId, academy.Id, material.Id)
+ ampID := existingAMP.Id
+ if ampID == uuid.Nil { ampID = uuid.New() }
+
+ amp = entity.AcademyMaterialProgress{
+ Id: ampID,
+ AccountId: accountId,
+ AcademyId: academy.Id,
+ MaterialId: material.Id,
+ Progress: progressPct,
+ TotalCompletedContents: uint(totalContentsCompleted),
+ Status: matStatus,
+ CompletedAt: matCompletedAt,
+ }
+ if _, err := txRepo.UpsertMaterialProgress(ctx, amp); err != nil { return err }
+
+ // 3. Hitung Ulang Progress Academy (Aggregation)
+ // Logic: Average dari seluruh Material Progress
+ // Atau Logic Sederhana: (Completed Material / Total Material) -> Ini kurang akurat jika material bobotnya sama.
+ // Logic Lebih Akurat: Sum(MaterialProgress) / TotalMaterial
+
+ accumulatedProgress, _ := txRepo.GetAccumulatedMaterialProgress(ctx, accountId, academy.Id)
+ a, _ := txRepo.GetAcademyByID(ctx, academy.Id) // Ambil count terbaru
+
+ acadStatus := entity.StatusNotStarted
+ var acadCompletedAt *time.Time
+ acadProgressPct := 0.0
+
+ if a.MaterialsCount > 0 {
+ // Rumus: Total Akumulasi Persen Material / Jumlah Material
+ // Contoh: Mat A (100%) + Mat B (50%) = 150%. Total Mat = 2. Academy Progress = 75%.
+ acadProgressPct = accumulatedProgress / float64(a.MaterialsCount)
+ acadProgressPct = math.Round(acadProgressPct*100) / 100
+
+ if acadProgressPct >= 100 {
+ acadStatus = entity.StatusCompleted
+ acadCompletedAt = utils.Ptr(time.Now())
+ acadProgressPct = 100
+ } else if acadProgressPct > 0 {
+ acadStatus = entity.StatusInProgress
+ }
+ }
+
+ // Hitung berapa material yang full completed (opsional, untuk display)
+ totalMaterialsCompleted, _ := txRepo.CountCompletedMaterialsByAcademyAndAccount(ctx, accountId, academy.Id)
+
+ existingAP, _ := txRepo.GetAcademyProgress(ctx, accountId, academy.Id)
+ apID := existingAP.Id
+ if apID == uuid.Nil { apID = uuid.New() }
+
+ ap = entity.AcademyProgress{
+ Id: apID,
+ AccountId: accountId,
+ AcademyId: academy.Id,
+ Progress: acadProgressPct,
+ TotalCompletedMaterials: uint(totalMaterialsCompleted),
+ Status: acadStatus,
+ CompletedAt: acadCompletedAt,
+ }
+ if _, err := txRepo.UpsertAcademyProgress(ctx, ap); err != nil { return err }
+
+ return nil
+ })
+
+ return acp, amp, ap, err
+}
\ No newline at end of file
diff --git a/services/account_service.go b/services/account_service.go
index 1024a1cadd718e5da15c576453ff0297a81c3ef7..d017aa450b4ef2aaab9b69f3030b0e9e2e3944b0 100644
--- a/services/account_service.go
+++ b/services/account_service.go
@@ -68,7 +68,7 @@ func (s *accountService) Create(ctx context.Context, name string, email string,
return entity.Account{}, err
}
- _, err = s.CreateEmptyDetail(ctx, created.Id)
+ _, _ = s.CreateEmptyDetail(ctx, created.Id)
return created, nil
diff --git a/services/exam_event_service.go b/services/exam_event_service.go
index df42c23dda6b0116b0b729ed6ef35ef3853f5edc..f6b440a976678214b18412f53987f4f83684b005 100644
--- a/services/exam_event_service.go
+++ b/services/exam_event_service.go
@@ -1,407 +1,407 @@
-package services
-
-import (
- "context"
- "errors"
- "fmt"
- "time"
-
- "abdanhafidz.com/go-boilerplate/models/dto"
- entity "abdanhafidz.com/go-boilerplate/models/entity"
- http_error "abdanhafidz.com/go-boilerplate/models/error"
- "abdanhafidz.com/go-boilerplate/repositories"
- "abdanhafidz.com/go-boilerplate/utils"
- "github.com/google/uuid"
- "gorm.io/gorm"
-)
-
-type ExamService interface {
- ListExamByEvent(ctx context.Context, eventSlug string, accountId uuid.UUID) ([]entity.Exam, error)
- GetEventExamExisting(ctx context.Context, eventSlug string, examSlug string, accountId uuid.UUID) (ev dto.EventDetailResponse, exam entity.Exam, err error)
- GetExamEventAttempt(ctx context.Context, eventSlug string, examSlug string, accountId uuid.UUID) (dto.UserExamStatus, entity.ExamEventAttempt, error)
- AttemptExamEvent(ctx context.Context, eventSlug string, examSlug string, accountId uuid.UUID) (entity.ExamEventAttempt, error)
- SetupQuestions(ctx context.Context, eventSlug string, examId uuid.UUID, accountId uuid.UUID) ([]entity.Questions, error)
- SetupAnswer(ctx context.Context, questions []entity.Questions, attemptId uuid.UUID) ([]entity.ExamEventAnswer, error)
- SetupExamTimer(ctx context.Context, exam entity.Exam) (time.Time, time.Time)
- SubmitExamEvent(ctx context.Context, attemptId uuid.UUID) (result entity.Result, err error)
- AnswerExamEvent(ctx context.Context, eventSlug string, attemptId uuid.UUID, questionId uuid.UUID, answer []string) (entity.CPQuestionVerdict, error)
-}
-type evaluator func(answer []string) (float32, entity.CPQuestionVerdict)
-
-type examService struct {
- eventService EventService
- problemSetService ProblemSetService
- problemSetExamAssignRepo repositories.ProblemSetExamAssignRepository
- examRepo repositories.ExamRepository
- examEventAttemptRepo repositories.ExamEventAttemptRepository
- examEventAnswerRepo repositories.ExamEventAnswerRepository
- examEventAssignRepo repositories.ExamEventAssignRepository
- resultRepo repositories.ResultRepository
-}
-
-func NewExamService(eventService EventService, problemSetService ProblemSetService, problemSetExamAssignRepo repositories.ProblemSetExamAssignRepository, examRepo repositories.ExamRepository, examEventAttemptRepo repositories.ExamEventAttemptRepository, examEventAssignRepo repositories.ExamEventAssignRepository, examEventAnswerRepo repositories.ExamEventAnswerRepository, resultRepo repositories.ResultRepository) ExamService {
- return &examService{
- eventService: eventService,
- problemSetService: problemSetService,
- problemSetExamAssignRepo: problemSetExamAssignRepo,
- examRepo: examRepo,
- examEventAttemptRepo: examEventAttemptRepo,
- examEventAssignRepo: examEventAssignRepo,
- examEventAnswerRepo: examEventAnswerRepo,
- resultRepo: resultRepo,
- }
-}
-
-func ProtectExamEventAttempt(attempt entity.ExamEventAttempt) entity.ExamEventAttempt {
-
- var cleanQuestions []entity.Questions
- for _, q := range attempt.Questions {
- qCopy := q
- qCopy.AnsKey = nil // hide answer key
- qCopy.CorrMark = 0
- qCopy.IncorrMark = 0
- qCopy.NullMark = 0
-
- cleanQuestions = append(cleanQuestions, qCopy)
- }
- attempt.Questions = cleanQuestions
-
- // protect answers verdict info
- var cleanAnswers []entity.ExamEventAnswer
-
- for _, a := range attempt.Answers {
- aCopy := a
- aCopy.Score = 0 // hide score
-
- cleanAnswers = append(cleanAnswers, aCopy)
- }
-
- attempt.Answers = cleanAnswers
-
- return attempt
-}
-
-func (s *examService) ListExamByEvent(ctx context.Context, eventSlug string, accountId uuid.UUID) ([]entity.Exam, error) {
- ev, err := s.eventService.DetailBySlug(ctx, eventSlug, accountId)
-
- if err != nil {
- return []entity.Exam{}, err
- }
-
- exams, err := s.examRepo.ListByEvent(ctx, ev.Data.Id)
-
- if err != nil {
- return []entity.Exam{}, err
- }
-
- return exams, err
-
-}
-
-func (s *examService) GetEventExamExisting(ctx context.Context, eventSlug string, examSlug string, accountId uuid.UUID) (ev dto.EventDetailResponse, exam entity.Exam, err error) {
-
- if ev, err = s.eventService.DetailBySlug(ctx, eventSlug, accountId); err != nil {
- return ev, exam, err
- }
-
- if exam, err = s.examRepo.GetBySlug(ctx, examSlug); err != nil {
- return ev, exam, err
- }
-
- if err := s.examEventAssignRepo.Check(ctx, ev.Data.Id, exam.Id); err != nil {
- return dto.EventDetailResponse{}, entity.Exam{}, err
- }
-
- return ev, exam, err
-}
-
-func (s *examService) GetExamEventAttempt(ctx context.Context, eventSlug string, examSlug string, accountId uuid.UUID) (dto.UserExamStatus, entity.ExamEventAttempt, error) {
-
- ev, exam, err := s.GetEventExamExisting(ctx, eventSlug, examSlug, accountId)
-
- if err != nil {
- return dto.UserExamStatus{}, entity.ExamEventAttempt{}, err
- }
-
- examEventAttempt, err := s.examEventAttemptRepo.GetByExamEvent(ctx, ev.Data.Id, exam.Id, accountId)
- fmt.Println("Error Exam Event Attempt", errors.Is(err, gorm.ErrRecordNotFound))
- if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
- return dto.UserExamStatus{}, entity.ExamEventAttempt{}, err
- }
-
- var attemptStatus dto.UserExamStatus
- attemptStatus.IsNotAttempt = errors.Is(err, gorm.ErrRecordNotFound)
- attemptStatus.IsTimeOut = (utils.CalculateRemainingTime(examEventAttempt.CreatedAt, examEventAttempt.DueAt) == 0) || false
- attemptStatus.IsSubmitted = examEventAttempt.Submitted
- attemptStatus.IsOnAttempt = !attemptStatus.IsNotAttempt && !attemptStatus.IsTimeOut && !attemptStatus.IsSubmitted
- return attemptStatus, examEventAttempt, nil
-
-}
-func (s *examService) SetupQuestions(ctx context.Context, eventSlug string, examId uuid.UUID, accountId uuid.UUID) ([]entity.Questions, error) {
- examAssign, err := s.problemSetExamAssignRepo.GetByExam(ctx, examId)
-
- if err != nil {
- return []entity.Questions{}, err
- }
-
- questions, err := s.problemSetService.ListQuestions(ctx, examAssign.ProblemSetId)
-
- if err != nil {
- return []entity.Questions{}, err
- }
-
- return questions, err
-}
-
-func (s *examService) SetupAnswer(ctx context.Context, questions []entity.Questions, attemptId uuid.UUID) ([]entity.ExamEventAnswer, error) {
- var examEventAnswers []entity.ExamEventAnswer
- for _, q := range questions {
-
- blank_ans := entity.ExamEventAnswer{
- AttemptId: attemptId,
- QuestionId: q.Id,
- }
-
- err := s.examEventAnswerRepo.Create(ctx, &blank_ans)
- if err != nil {
- return []entity.ExamEventAnswer{}, err
- }
- examEventAnswers = append(examEventAnswers, blank_ans)
- }
-
- return examEventAnswers, nil
-}
-
-func (s *examService) SetupExamTimer(ctx context.Context, exam entity.Exam) (time.Time, time.Time) {
- startTime := time.Now()
- dueTime := startTime.Add(exam.Duration * time.Minute)
- return startTime, dueTime
-}
-
-func (s *examService) SubmitExamEvent(ctx context.Context, attemptId uuid.UUID) (result entity.Result, err error) {
- attempt, err := s.examEventAttemptRepo.GetById(ctx, attemptId)
- finalScore := float32(0)
- if err != nil {
- return entity.Result{}, err
- }
-
- for _, ans := range attempt.Answers {
- finalScore += ans.Score
- }
-
- if !attempt.Submitted {
-
- attempt.Submitted = true
- result.AttemptId = attempt.Id
- result.ExamEventAttempt = &attempt
- result.FinalScore = float32(finalScore)
-
- s.examEventAttemptRepo.Update(ctx, &attempt)
- err := s.resultRepo.Create(ctx, &result)
-
- if err != nil {
- return entity.Result{}, err
- }
- } else {
- result, err = s.resultRepo.GetByAttemptId(ctx, attempt.Id)
-
- if err != nil {
- return entity.Result{}, err
- }
-
- result.FinalScore = float32(finalScore)
- err := s.resultRepo.Update(ctx, &result)
- if err != nil {
- return entity.Result{}, err
- }
- }
- return result, err
-
-}
-func (s *examService) AttemptExamEvent(ctx context.Context, eventSlug string, examSlug string, accountId uuid.UUID) (entity.ExamEventAttempt, error) {
- eventStatus, err := s.eventService.GetStatus(ctx, eventSlug, accountId)
-
- if err != nil {
- return entity.ExamEventAttempt{}, err
- }
-
- if eventStatus.IsHasNotStarted {
- return entity.ExamEventAttempt{}, http_error.EVENT_NOT_STARTED
- }
-
- ev, exam, err := s.GetEventExamExisting(ctx, eventSlug, examSlug, accountId)
-
- if err != nil {
- return entity.ExamEventAttempt{}, err
- }
- attemptStatus, examEventAttempt, err := s.GetExamEventAttempt(ctx, eventSlug, examSlug, accountId)
-
- if err != nil {
- return entity.ExamEventAttempt{}, err
- }
-
- questions, err := s.SetupQuestions(ctx, eventSlug, exam.Id, accountId)
- examEventAttempt.Questions = questions
-
- if err != nil {
- return entity.ExamEventAttempt{}, err
- }
- if attemptStatus.IsNotAttempt {
-
- if eventStatus.IsFinished {
- return entity.ExamEventAttempt{}, err
- }
-
- startTime, dueTime := s.SetupExamTimer(ctx, exam)
- remTime := utils.CalculateRemainingTime(startTime, dueTime)
-
- fmt.Println("Rem Time = ", remTime)
- examEventAttempt = entity.ExamEventAttempt{
- AccountId: accountId,
- EventId: ev.Data.Id,
- ExamId: exam.Id,
- CreatedAt: startTime,
- DueAt: dueTime,
- Submitted: false,
- RemTime: remTime,
- Questions: questions,
- }
-
- if err := s.examEventAttemptRepo.Create(ctx, &examEventAttempt); err != nil {
- return entity.ExamEventAttempt{}, err
- }
-
- answers, err := s.SetupAnswer(ctx, questions, examEventAttempt.Id)
- fmt.Println("Answer = ", answers)
- if err != nil {
- return entity.ExamEventAttempt{}, err
- }
-
- examEventAttempt.Answers = answers
- return ProtectExamEventAttempt(examEventAttempt), err
-
- } else if attemptStatus.IsOnAttempt {
-
- if eventStatus.IsFinished {
- s.SubmitExamEvent(ctx, examEventAttempt.Id)
- examEventAttempt.Submitted = true
- if err := s.examEventAttemptRepo.Update(ctx, &examEventAttempt); err != nil {
- return entity.ExamEventAttempt{}, err
- }
- return examEventAttempt, err
- }
- remTime := utils.CalculateRemainingTime(examEventAttempt.CreatedAt, examEventAttempt.DueAt)
- examEventAttempt.RemTime = remTime
-
- if err := s.examEventAttemptRepo.Update(ctx, &examEventAttempt); err != nil {
- return entity.ExamEventAttempt{}, err
- }
-
- examEventAttempt.Questions = questions
-
- return ProtectExamEventAttempt(examEventAttempt), err
-
- } else if attemptStatus.IsTimeOut {
- if examEventAttempt.RemTime != 0 {
- remTime := 0
- examEventAttempt.RemTime = remTime
- }
-
- s.SubmitExamEvent(ctx, examEventAttempt.Id)
- examEventAttempt.Submitted = true
- if err := s.examEventAttemptRepo.Update(ctx, &examEventAttempt); err != nil {
- return entity.ExamEventAttempt{}, err
- }
- return examEventAttempt, err
-
- } else if attemptStatus.IsSubmitted {
- return examEventAttempt, nil
- }
- return entity.ExamEventAttempt{}, http_error.INTERNAL_SERVER_ERROR
-}
-
-func (s *examService) EvaluateAnswer(ctx context.Context, question entity.Questions) evaluator {
-
- nonCPEvaluator := func(answer []string) (float32, entity.CPQuestionVerdict) {
- score := float32(0)
- isCorrect := true
- for i, ans := range answer {
- fmt.Println("User Answer :", ans)
- fmt.Println("Answer Key :", question.AnsKey[i])
- if ans != question.AnsKey[i] && ans != "" {
- score += float32(question.IncorrMark)
- isCorrect = false
- break
- } else if ans == "" {
- score += float32(question.NullMark)
- isCorrect = false
- break
- }
- }
-
- if isCorrect {
- score += float32(question.CorrMark)
- }
-
- return score, entity.CPQuestionVerdict{}
- }
-
- CPEvaluator := func(answer []string) (float32, entity.CPQuestionVerdict) {
- return 0, entity.CPQuestionVerdict{
- TimeExecution: 0.01,
- MemoryUsage: 256.0,
- Verdict: "AC",
- Score: 100.0,
- }
- }
-
- var examEvaluator = map[string]evaluator{
- "multiple_choices": nonCPEvaluator,
- "multiple_choices_complex": nonCPEvaluator,
- "short_answer": nonCPEvaluator,
- "true_false": nonCPEvaluator,
- "code_puzzle": nonCPEvaluator,
- "code_type": nonCPEvaluator,
- "competitive_programming": CPEvaluator,
- }
-
- return examEvaluator[question.Type]
-}
-func (s *examService) AnswerExamEvent(ctx context.Context, eventSlug string, attemptId uuid.UUID, questionId uuid.UUID, answer []string) (entity.CPQuestionVerdict, error) {
-
- attempt, err := s.examEventAttemptRepo.GetById(ctx, attemptId)
-
- if err != nil {
- return entity.CPQuestionVerdict{}, err
- }
-
- eventStatus, err := s.eventService.GetStatus(ctx, eventSlug, attempt.AccountId)
-
- if err != nil {
- return entity.CPQuestionVerdict{}, err
- }
-
- if eventStatus.IsFinished {
- return entity.CPQuestionVerdict{}, http_error.EVENT_FINISHED
- }
-
- if attempt.Submitted {
- return entity.CPQuestionVerdict{}, http_error.EXAMS_SUBMITTED
- }
-
- question, err := s.problemSetService.GetQuestionById(ctx, questionId)
- if err != nil {
- return entity.CPQuestionVerdict{}, err
- }
-
- score, CPQuestionVerdict := s.EvaluateAnswer(ctx, question)(answer)
-
- err = s.examEventAnswerRepo.Update(ctx, &entity.ExamEventAnswer{
- AttemptId: attemptId,
- QuestionId: questionId,
- Answers: answer,
- Score: score,
- })
-
- return CPQuestionVerdict, err
-}
+package services
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "time"
+
+ "abdanhafidz.com/go-boilerplate/models/dto"
+ entity "abdanhafidz.com/go-boilerplate/models/entity"
+ http_error "abdanhafidz.com/go-boilerplate/models/error"
+ "abdanhafidz.com/go-boilerplate/repositories"
+ "abdanhafidz.com/go-boilerplate/utils"
+ "github.com/google/uuid"
+ "gorm.io/gorm"
+)
+
+type ExamService interface {
+ ListExamByEvent(ctx context.Context, eventSlug string, accountId uuid.UUID) ([]entity.Exam, error)
+ GetEventExamExisting(ctx context.Context, eventSlug string, examSlug string, accountId uuid.UUID) (ev dto.EventDetailResponse, exam entity.Exam, err error)
+ GetExamEventAttempt(ctx context.Context, eventSlug string, examSlug string, accountId uuid.UUID) (dto.UserExamStatus, entity.ExamEventAttempt, error)
+ AttemptExamEvent(ctx context.Context, eventSlug string, examSlug string, accountId uuid.UUID) (entity.ExamEventAttempt, error)
+ SetupQuestions(ctx context.Context, eventSlug string, examId uuid.UUID, accountId uuid.UUID) ([]entity.Questions, error)
+ SetupAnswer(ctx context.Context, questions []entity.Questions, attemptId uuid.UUID) ([]entity.ExamEventAnswer, error)
+ SetupExamTimer(ctx context.Context, exam entity.Exam) (time.Time, time.Time)
+ SubmitExamEvent(ctx context.Context, attemptId uuid.UUID) (result entity.Result, err error)
+ AnswerExamEvent(ctx context.Context, eventSlug string, attemptId uuid.UUID, questionId uuid.UUID, answer []string) (entity.CPQuestionVerdict, error)
+}
+type evaluator func(answer []string) (float32, entity.CPQuestionVerdict)
+
+type examService struct {
+ eventService EventService
+ problemSetService ProblemSetService
+ problemSetExamAssignRepo repositories.ProblemSetExamAssignRepository
+ examRepo repositories.ExamRepository
+ examEventAttemptRepo repositories.ExamEventAttemptRepository
+ examEventAnswerRepo repositories.ExamEventAnswerRepository
+ examEventAssignRepo repositories.ExamEventAssignRepository
+ resultRepo repositories.ResultRepository
+}
+
+func NewExamService(eventService EventService, problemSetService ProblemSetService, problemSetExamAssignRepo repositories.ProblemSetExamAssignRepository, examRepo repositories.ExamRepository, examEventAttemptRepo repositories.ExamEventAttemptRepository, examEventAssignRepo repositories.ExamEventAssignRepository, examEventAnswerRepo repositories.ExamEventAnswerRepository, resultRepo repositories.ResultRepository) ExamService {
+ return &examService{
+ eventService: eventService,
+ problemSetService: problemSetService,
+ problemSetExamAssignRepo: problemSetExamAssignRepo,
+ examRepo: examRepo,
+ examEventAttemptRepo: examEventAttemptRepo,
+ examEventAssignRepo: examEventAssignRepo,
+ examEventAnswerRepo: examEventAnswerRepo,
+ resultRepo: resultRepo,
+ }
+}
+
+func ProtectExamEventAttempt(attempt entity.ExamEventAttempt) entity.ExamEventAttempt {
+
+ var cleanQuestions []entity.Questions
+ for _, q := range attempt.Questions {
+ qCopy := q
+ qCopy.AnsKey = nil // hide answer key
+ qCopy.CorrMark = 0
+ qCopy.IncorrMark = 0
+ qCopy.NullMark = 0
+
+ cleanQuestions = append(cleanQuestions, qCopy)
+ }
+ attempt.Questions = cleanQuestions
+
+ // protect answers verdict info
+ var cleanAnswers []entity.ExamEventAnswer
+
+ for _, a := range attempt.Answers {
+ aCopy := a
+ aCopy.Score = 0 // hide score
+
+ cleanAnswers = append(cleanAnswers, aCopy)
+ }
+
+ attempt.Answers = cleanAnswers
+
+ return attempt
+}
+
+func (s *examService) ListExamByEvent(ctx context.Context, eventSlug string, accountId uuid.UUID) ([]entity.Exam, error) {
+ ev, err := s.eventService.DetailBySlug(ctx, eventSlug, accountId)
+
+ if err != nil {
+ return []entity.Exam{}, err
+ }
+
+ exams, err := s.examRepo.ListByEvent(ctx, ev.Data.Id)
+
+ if err != nil {
+ return []entity.Exam{}, err
+ }
+
+ return exams, err
+
+}
+
+func (s *examService) GetEventExamExisting(ctx context.Context, eventSlug string, examSlug string, accountId uuid.UUID) (ev dto.EventDetailResponse, exam entity.Exam, err error) {
+
+ if ev, err = s.eventService.DetailBySlug(ctx, eventSlug, accountId); err != nil {
+ return ev, exam, err
+ }
+
+ if exam, err = s.examRepo.GetBySlug(ctx, examSlug); err != nil {
+ return ev, exam, err
+ }
+
+ if err := s.examEventAssignRepo.Check(ctx, ev.Data.Id, exam.Id); err != nil {
+ return dto.EventDetailResponse{}, entity.Exam{}, err
+ }
+
+ return ev, exam, err
+}
+
+func (s *examService) GetExamEventAttempt(ctx context.Context, eventSlug string, examSlug string, accountId uuid.UUID) (dto.UserExamStatus, entity.ExamEventAttempt, error) {
+
+ ev, exam, err := s.GetEventExamExisting(ctx, eventSlug, examSlug, accountId)
+
+ if err != nil {
+ return dto.UserExamStatus{}, entity.ExamEventAttempt{}, err
+ }
+
+ examEventAttempt, err := s.examEventAttemptRepo.GetByExamEvent(ctx, ev.Data.Id, exam.Id, accountId)
+ fmt.Println("Error Exam Event Attempt", errors.Is(err, gorm.ErrRecordNotFound))
+ if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
+ return dto.UserExamStatus{}, entity.ExamEventAttempt{}, err
+ }
+
+ var attemptStatus dto.UserExamStatus
+ attemptStatus.IsNotAttempt = errors.Is(err, gorm.ErrRecordNotFound)
+ attemptStatus.IsTimeOut = (utils.CalculateRemainingTime(examEventAttempt.CreatedAt, examEventAttempt.DueAt) == 0) || false
+ attemptStatus.IsSubmitted = examEventAttempt.Submitted
+ attemptStatus.IsOnAttempt = !attemptStatus.IsNotAttempt && !attemptStatus.IsTimeOut && !attemptStatus.IsSubmitted
+ return attemptStatus, examEventAttempt, nil
+
+}
+func (s *examService) SetupQuestions(ctx context.Context, eventSlug string, examId uuid.UUID, accountId uuid.UUID) ([]entity.Questions, error) {
+ examAssign, err := s.problemSetExamAssignRepo.GetByExam(ctx, examId)
+
+ if err != nil {
+ return []entity.Questions{}, err
+ }
+
+ questions, err := s.problemSetService.ListQuestions(ctx, examAssign.ProblemSetId)
+
+ if err != nil {
+ return []entity.Questions{}, err
+ }
+
+ return questions, err
+}
+
+func (s *examService) SetupAnswer(ctx context.Context, questions []entity.Questions, attemptId uuid.UUID) ([]entity.ExamEventAnswer, error) {
+ var examEventAnswers []entity.ExamEventAnswer
+ for _, q := range questions {
+
+ blank_ans := entity.ExamEventAnswer{
+ AttemptId: attemptId,
+ QuestionId: q.Id,
+ }
+
+ err := s.examEventAnswerRepo.Create(ctx, &blank_ans)
+ if err != nil {
+ return []entity.ExamEventAnswer{}, err
+ }
+ examEventAnswers = append(examEventAnswers, blank_ans)
+ }
+
+ return examEventAnswers, nil
+}
+
+func (s *examService) SetupExamTimer(ctx context.Context, exam entity.Exam) (time.Time, time.Time) {
+ startTime := time.Now()
+ dueTime := startTime.Add(exam.Duration * time.Minute)
+ return startTime, dueTime
+}
+
+func (s *examService) SubmitExamEvent(ctx context.Context, attemptId uuid.UUID) (result entity.Result, err error) {
+ attempt, err := s.examEventAttemptRepo.GetById(ctx, attemptId)
+ finalScore := float32(0)
+ if err != nil {
+ return entity.Result{}, err
+ }
+
+ for _, ans := range attempt.Answers {
+ finalScore += ans.Score
+ }
+
+ if !attempt.Submitted {
+
+ attempt.Submitted = true
+ result.AttemptId = attempt.Id
+ result.ExamEventAttempt = &attempt
+ result.FinalScore = float32(finalScore)
+
+ s.examEventAttemptRepo.Update(ctx, &attempt)
+ err := s.resultRepo.Create(ctx, &result)
+
+ if err != nil {
+ return entity.Result{}, err
+ }
+ } else {
+ result, err = s.resultRepo.GetByAttemptId(ctx, attempt.Id)
+
+ if err != nil {
+ return entity.Result{}, err
+ }
+
+ result.FinalScore = float32(finalScore)
+ err := s.resultRepo.Update(ctx, &result)
+ if err != nil {
+ return entity.Result{}, err
+ }
+ }
+ return result, err
+
+}
+func (s *examService) AttemptExamEvent(ctx context.Context, eventSlug string, examSlug string, accountId uuid.UUID) (entity.ExamEventAttempt, error) {
+ eventStatus, err := s.eventService.GetStatus(ctx, eventSlug, accountId)
+
+ if err != nil {
+ return entity.ExamEventAttempt{}, err
+ }
+
+ if eventStatus.IsHasNotStarted {
+ return entity.ExamEventAttempt{}, http_error.EVENT_NOT_STARTED
+ }
+
+ ev, exam, err := s.GetEventExamExisting(ctx, eventSlug, examSlug, accountId)
+
+ if err != nil {
+ return entity.ExamEventAttempt{}, err
+ }
+ attemptStatus, examEventAttempt, err := s.GetExamEventAttempt(ctx, eventSlug, examSlug, accountId)
+
+ if err != nil {
+ return entity.ExamEventAttempt{}, err
+ }
+
+ questions, err := s.SetupQuestions(ctx, eventSlug, exam.Id, accountId)
+ examEventAttempt.Questions = questions
+
+ if err != nil {
+ return entity.ExamEventAttempt{}, err
+ }
+ if attemptStatus.IsNotAttempt {
+
+ if eventStatus.IsFinished {
+ return entity.ExamEventAttempt{}, err
+ }
+
+ startTime, dueTime := s.SetupExamTimer(ctx, exam)
+ remTime := utils.CalculateRemainingTime(startTime, dueTime)
+
+ fmt.Println("Rem Time = ", remTime)
+ examEventAttempt = entity.ExamEventAttempt{
+ AccountId: accountId,
+ EventId: ev.Data.Id,
+ ExamId: exam.Id,
+ CreatedAt: startTime,
+ DueAt: dueTime,
+ Submitted: false,
+ RemTime: remTime,
+ Questions: questions,
+ }
+
+ if err := s.examEventAttemptRepo.Create(ctx, &examEventAttempt); err != nil {
+ return entity.ExamEventAttempt{}, err
+ }
+
+ answers, err := s.SetupAnswer(ctx, questions, examEventAttempt.Id)
+ fmt.Println("Answer = ", answers)
+ if err != nil {
+ return entity.ExamEventAttempt{}, err
+ }
+
+ examEventAttempt.Answers = answers
+ return ProtectExamEventAttempt(examEventAttempt), err
+
+ } else if attemptStatus.IsOnAttempt {
+
+ if eventStatus.IsFinished {
+ s.SubmitExamEvent(ctx, examEventAttempt.Id)
+ examEventAttempt.Submitted = true
+ if err := s.examEventAttemptRepo.Update(ctx, &examEventAttempt); err != nil {
+ return entity.ExamEventAttempt{}, err
+ }
+ return examEventAttempt, err
+ }
+ remTime := utils.CalculateRemainingTime(examEventAttempt.CreatedAt, examEventAttempt.DueAt)
+ examEventAttempt.RemTime = remTime
+
+ if err := s.examEventAttemptRepo.Update(ctx, &examEventAttempt); err != nil {
+ return entity.ExamEventAttempt{}, err
+ }
+
+ examEventAttempt.Questions = questions
+
+ return ProtectExamEventAttempt(examEventAttempt), err
+
+ } else if attemptStatus.IsTimeOut {
+ if examEventAttempt.RemTime != 0 {
+ remTime := 0
+ examEventAttempt.RemTime = remTime
+ }
+
+ s.SubmitExamEvent(ctx, examEventAttempt.Id)
+ examEventAttempt.Submitted = true
+ if err := s.examEventAttemptRepo.Update(ctx, &examEventAttempt); err != nil {
+ return entity.ExamEventAttempt{}, err
+ }
+ return examEventAttempt, err
+
+ } else if attemptStatus.IsSubmitted {
+ return examEventAttempt, nil
+ }
+ return entity.ExamEventAttempt{}, http_error.INTERNAL_SERVER_ERROR
+}
+
+func (s *examService) EvaluateAnswer(ctx context.Context, question entity.Questions) evaluator {
+
+ nonCPEvaluator := func(answer []string) (float32, entity.CPQuestionVerdict) {
+ score := float32(0)
+ isCorrect := true
+ for i, ans := range answer {
+ fmt.Println("User Answer :", ans)
+ fmt.Println("Answer Key :", question.AnsKey[i])
+ if ans != question.AnsKey[i] && ans != "" {
+ score += float32(question.IncorrMark)
+ isCorrect = false
+ break
+ } else if ans == "" {
+ score += float32(question.NullMark)
+ isCorrect = false
+ break
+ }
+ }
+
+ if isCorrect {
+ score += float32(question.CorrMark)
+ }
+
+ return score, entity.CPQuestionVerdict{}
+ }
+
+ CPEvaluator := func(answer []string) (float32, entity.CPQuestionVerdict) {
+ return 0, entity.CPQuestionVerdict{
+ TimeExecution: 0.01,
+ MemoryUsage: 256.0,
+ Verdict: "AC",
+ Score: 100.0,
+ }
+ }
+
+ var examEvaluator = map[string]evaluator{
+ "multiple_choices": nonCPEvaluator,
+ "multiple_choices_complex": nonCPEvaluator,
+ "short_answer": nonCPEvaluator,
+ "true_false": nonCPEvaluator,
+ "code_puzzle": nonCPEvaluator,
+ "code_type": nonCPEvaluator,
+ "competitive_programming": CPEvaluator,
+ }
+
+ return examEvaluator[question.Type]
+}
+func (s *examService) AnswerExamEvent(ctx context.Context, eventSlug string, attemptId uuid.UUID, questionId uuid.UUID, answer []string) (entity.CPQuestionVerdict, error) {
+
+ attempt, err := s.examEventAttemptRepo.GetById(ctx, attemptId)
+
+ if err != nil {
+ return entity.CPQuestionVerdict{}, err
+ }
+
+ eventStatus, err := s.eventService.GetStatus(ctx, eventSlug, attempt.AccountId)
+
+ if err != nil {
+ return entity.CPQuestionVerdict{}, err
+ }
+
+ if eventStatus.IsFinished {
+ return entity.CPQuestionVerdict{}, http_error.EVENT_FINISHED
+ }
+
+ if attempt.Submitted {
+ return entity.CPQuestionVerdict{}, http_error.EXAMS_SUBMITTED
+ }
+
+ question, err := s.problemSetService.GetQuestionById(ctx, questionId)
+ if err != nil {
+ return entity.CPQuestionVerdict{}, err
+ }
+
+ score, CPQuestionVerdict := s.EvaluateAnswer(ctx, question)(answer)
+
+ err = s.examEventAnswerRepo.Update(ctx, &entity.ExamEventAnswer{
+ AttemptId: attemptId,
+ QuestionId: questionId,
+ Answers: answer,
+ Score: score,
+ })
+
+ return CPQuestionVerdict, err
+}
diff --git a/services/external_auth_service.go b/services/external_auth_service.go
index 1a281a2a32ae7c92d17805c3fb12338e15a2abd4..3eedeb7db1b55a76abae5bb80a70505ab4ba3a09 100644
--- a/services/external_auth_service.go
+++ b/services/external_auth_service.go
@@ -1,93 +1,93 @@
-package services
-
-import (
- "context"
- "errors"
-
- "abdanhafidz.com/go-boilerplate/models/dto"
- entity "abdanhafidz.com/go-boilerplate/models/entity"
- "abdanhafidz.com/go-boilerplate/repositories"
- "google.golang.org/api/idtoken"
- "gorm.io/gorm"
-)
-
-type ExternalAuthService interface {
- GoogleAuth(ctx context.Context, idToken string) (dto.AuthenticatedUser, error)
-}
-
-type externalAuthService struct {
- jwtService JWTService
- accountService AccountService
- externalAuthRepo repositories.ExternalAuthRepository
-}
-
-func NewExternalAuthService(jwtService JWTService, accountService AccountService, externalAuthRepo repositories.ExternalAuthRepository) ExternalAuthService {
- return &externalAuthService{
- jwtService: jwtService,
- accountService: accountService,
- externalAuthRepo: externalAuthRepo,
- }
-}
-
-func (s *externalAuthService) GoogleAuth(ctx context.Context, idToken string) (dto.AuthenticatedUser, error) {
-
- var (
- acc entity.Account
- errAcc error
- errExtAuth error
- name string
- email string
- password string
- )
-
- payload, errTok := idtoken.Validate(context.Background(), idToken, "")
-
- if errTok != nil {
- return dto.AuthenticatedUser{}, errTok
- }
- claims := payload.Claims
-
- if v, ok := claims["email"].(string); ok {
- email = v
- }
-
- if v, ok := claims["name"].(string); ok {
- name = v
- } else {
- if v, ok := claims["given_name"].(string); ok {
- name = v
- }
- }
-
- if v, ok := claims["sub"].(string); ok {
- password = v
- }
-
- acc, err := s.accountService.GetByEmail(ctx, email)
-
- if errors.Is(err, gorm.ErrRecordNotFound) {
- acc, errAcc = s.accountService.Create(ctx, name, email, name, password)
- acc.IsEmailVerified = true
- s.accountService.Update(ctx, acc)
- _, errExtAuth = s.externalAuthRepo.Create(ctx, entity.ExternalAuth{
- OauthID: idToken,
- OauthProvider: "google",
- AccountId: acc.Id,
- })
- }
-
- if errAcc != nil {
- return dto.AuthenticatedUser{}, errAcc
- }
-
- if errExtAuth != nil {
- return dto.AuthenticatedUser{}, errExtAuth
- }
- token, _ := s.jwtService.GenerateToken(ctx, dto.JWTCustomClaims{
- AccountId: acc.Id.String(),
- })
-
- err = errors.Join(errAcc, errExtAuth)
- return dto.AuthenticatedUser{Account: acc, Token: token}, err
-
-}
+package services
+
+import (
+ "context"
+ "errors"
+
+ "abdanhafidz.com/go-boilerplate/models/dto"
+ entity "abdanhafidz.com/go-boilerplate/models/entity"
+ "abdanhafidz.com/go-boilerplate/repositories"
+ "google.golang.org/api/idtoken"
+ "gorm.io/gorm"
+)
+
+type ExternalAuthService interface {
+ GoogleAuth(ctx context.Context, idToken string) (dto.AuthenticatedUser, error)
+}
+
+type externalAuthService struct {
+ jwtService JWTService
+ accountService AccountService
+ externalAuthRepo repositories.ExternalAuthRepository
+}
+
+func NewExternalAuthService(jwtService JWTService, accountService AccountService, externalAuthRepo repositories.ExternalAuthRepository) ExternalAuthService {
+ return &externalAuthService{
+ jwtService: jwtService,
+ accountService: accountService,
+ externalAuthRepo: externalAuthRepo,
+ }
+}
+
+func (s *externalAuthService) GoogleAuth(ctx context.Context, idToken string) (dto.AuthenticatedUser, error) {
+
+ var (
+ acc entity.Account
+ errAcc error
+ errExtAuth error
+ name string
+ email string
+ password string
+ )
+
+ payload, errTok := idtoken.Validate(context.Background(), idToken, "")
+
+ if errTok != nil {
+ return dto.AuthenticatedUser{}, errTok
+ }
+ claims := payload.Claims
+
+ if v, ok := claims["email"].(string); ok {
+ email = v
+ }
+
+ if v, ok := claims["name"].(string); ok {
+ name = v
+ } else {
+ if v, ok := claims["given_name"].(string); ok {
+ name = v
+ }
+ }
+
+ if v, ok := claims["sub"].(string); ok {
+ password = v
+ }
+
+ acc, err := s.accountService.GetByEmail(ctx, email)
+
+ if errors.Is(err, gorm.ErrRecordNotFound) {
+ acc, errAcc = s.accountService.Create(ctx, name, email, name, password)
+ acc.IsEmailVerified = true
+ s.accountService.Update(ctx, acc)
+ _, errExtAuth = s.externalAuthRepo.Create(ctx, entity.ExternalAuth{
+ OauthID: idToken,
+ OauthProvider: "google",
+ AccountId: acc.Id,
+ })
+ }
+
+ if errAcc != nil {
+ return dto.AuthenticatedUser{}, errAcc
+ }
+
+ if errExtAuth != nil {
+ return dto.AuthenticatedUser{}, errExtAuth
+ }
+ token, _ := s.jwtService.GenerateToken(ctx, dto.JWTCustomClaims{
+ AccountId: acc.Id.String(),
+ })
+
+ err = errors.Join(errAcc, errExtAuth)
+ return dto.AuthenticatedUser{Account: acc, Token: token}, err
+
+}
diff --git a/services/jwt_service.go b/services/jwt_service.go
index 084d7685bd1e849e907fa229498f3deb2eaa671f..bc76ff0963095c0eae376bb418ce122f71b2dde3 100644
--- a/services/jwt_service.go
+++ b/services/jwt_service.go
@@ -1,80 +1,80 @@
-package services
-
-import (
- "context"
- "fmt"
-
- "abdanhafidz.com/go-boilerplate/models/dto"
- http_error "abdanhafidz.com/go-boilerplate/models/error"
- "github.com/golang-jwt/jwt/v4"
- "golang.org/x/crypto/bcrypt"
-)
-
-type JWTService interface {
- GenerateToken(ctx context.Context, payload dto.JWTCustomClaims) (token string, err error)
- ValidateToken(ctx context.Context, tokenStr string) (claim *dto.JWTCustomClaims, err error)
- VerifyPassword(ctx context.Context, hashedPassword string, password string) error
-}
-
-type jwtService struct {
- secretKey string
-}
-
-func NewJWTService(secretKey string) JWTService {
- return &jwtService{
- secretKey: secretKey,
- }
-}
-
-func (s *jwtService) GenerateToken(ctx context.Context, payload dto.JWTCustomClaims) (token string, err error) {
-
- claims := jwt.MapClaims{
- "account_id": payload.AccountId,
- }
-
- fmt.Println(s.secretKey)
-
- jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
- token, err_convertion := jwtToken.SignedString([]byte(s.secretKey))
-
- if err_convertion != nil {
- return "", http_error.INTERNAL_SERVER_ERROR
- }
-
- return token, nil
-}
-func (s *jwtService) VerifyPassword(ctx context.Context, hashedPassword string, password string) error {
- err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
- if err != nil {
- return http_error.WRONG_PASSWORD
- }
- return nil
-}
-
-func (s *jwtService) ValidateToken(ctx context.Context, tokenStr string) (claim *dto.JWTCustomClaims, err error) {
- token, err := jwt.Parse(tokenStr, func(token *jwt.Token) (interface{}, error) {
- if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
- return "", http_error.INTERNAL_SERVER_ERROR
- }
- return []byte(s.secretKey), nil
- })
-
- fmt.Println("Token", token)
- fmt.Println("secretKey", s.secretKey)
-
- if err != nil || !token.Valid {
- return nil, http_error.INVALID_TOKEN
- }
-
- claims, ok := token.Claims.(jwt.MapClaims)
- if !ok {
- return nil, http_error.INTERNAL_SERVER_ERROR
- }
- account_id, ok := claims["account_id"].(string)
- if !ok {
- return nil, http_error.INTERNAL_SERVER_ERROR
- }
- return &dto.JWTCustomClaims{
- AccountId: account_id,
- }, nil
-}
+package services
+
+import (
+ "context"
+ "fmt"
+
+ "abdanhafidz.com/go-boilerplate/models/dto"
+ http_error "abdanhafidz.com/go-boilerplate/models/error"
+ "github.com/golang-jwt/jwt/v4"
+ "golang.org/x/crypto/bcrypt"
+)
+
+type JWTService interface {
+ GenerateToken(ctx context.Context, payload dto.JWTCustomClaims) (token string, err error)
+ ValidateToken(ctx context.Context, tokenStr string) (claim *dto.JWTCustomClaims, err error)
+ VerifyPassword(ctx context.Context, hashedPassword string, password string) error
+}
+
+type jwtService struct {
+ secretKey string
+}
+
+func NewJWTService(secretKey string) JWTService {
+ return &jwtService{
+ secretKey: secretKey,
+ }
+}
+
+func (s *jwtService) GenerateToken(ctx context.Context, payload dto.JWTCustomClaims) (token string, err error) {
+
+ claims := jwt.MapClaims{
+ "account_id": payload.AccountId,
+ }
+
+ fmt.Println(s.secretKey)
+
+ jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
+ token, err_convertion := jwtToken.SignedString([]byte(s.secretKey))
+
+ if err_convertion != nil {
+ return "", http_error.INTERNAL_SERVER_ERROR
+ }
+
+ return token, nil
+}
+func (s *jwtService) VerifyPassword(ctx context.Context, hashedPassword string, password string) error {
+ err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
+ if err != nil {
+ return http_error.WRONG_PASSWORD
+ }
+ return nil
+}
+
+func (s *jwtService) ValidateToken(ctx context.Context, tokenStr string) (claim *dto.JWTCustomClaims, err error) {
+ token, err := jwt.Parse(tokenStr, func(token *jwt.Token) (interface{}, error) {
+ if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
+ return "", http_error.INTERNAL_SERVER_ERROR
+ }
+ return []byte(s.secretKey), nil
+ })
+
+ fmt.Println("Token", token)
+ fmt.Println("secretKey", s.secretKey)
+
+ if err != nil || !token.Valid {
+ return nil, http_error.INVALID_TOKEN
+ }
+
+ claims, ok := token.Claims.(jwt.MapClaims)
+ if !ok {
+ return nil, http_error.INTERNAL_SERVER_ERROR
+ }
+ account_id, ok := claims["account_id"].(string)
+ if !ok {
+ return nil, http_error.INTERNAL_SERVER_ERROR
+ }
+ return &dto.JWTCustomClaims{
+ AccountId: account_id,
+ }, nil
+}
diff --git a/services/problem_set_service.go b/services/problem_set_service.go
index 22fc306dc2d689f717f5e5fe232a1272a34fd100..2b4cb8ef90f0c4c51e47d1cdb02ea0380c772944 100644
--- a/services/problem_set_service.go
+++ b/services/problem_set_service.go
@@ -1,147 +1,147 @@
-package services
-
-import (
- "context"
- "errors"
-
- entity "abdanhafidz.com/go-boilerplate/models/entity"
- "abdanhafidz.com/go-boilerplate/repositories"
- "github.com/google/uuid"
-)
-
-var (
- ErrProblemSetNotFound = errors.New("problem set not found")
- ErrQuestionNotFound = errors.New("question not found")
-)
-
-type ProblemSetService interface {
- CreateProblemSet(ctx context.Context, ps entity.ProblemSet) error
- GetProblemSet(ctx context.Context, id uuid.UUID) (entity.ProblemSet, error)
- ListProblemSets(ctx context.Context) ([]entity.ProblemSet, error)
- UpdateProblemSet(ctx context.Context, ps entity.ProblemSet) error
- DeleteProblemSet(ctx context.Context, id uuid.UUID) error
-
- AddQuestion(ctx context.Context, q entity.Questions) error
- UpdateQuestion(ctx context.Context, q entity.Questions) error
- DeleteQuestion(ctx context.Context, qID uuid.UUID) error
- ListQuestions(ctx context.Context, psID uuid.UUID) ([]entity.Questions, error)
-
- AssignProblemSetToExam(ctx context.Context, examId uuid.UUID, problemSetId uuid.UUID) error
- RemoveAssignedProblemSet(ctx context.Context, assignId uuid.UUID) error
- GetQuestionById(ctx context.Context, qID uuid.UUID) (entity.Questions, error)
-}
-
-type problemSetService struct {
- problemSetRepository repositories.ProblemSetRepository
- questionsRepository repositories.QuestionsRepository
- problemSetExamAssignRepository repositories.ProblemSetExamAssignRepository
-}
-
-func NewProblemSetService(
- problemSetRepository repositories.ProblemSetRepository,
- questionsRepository repositories.QuestionsRepository,
- problemSetExamAssignRepository repositories.ProblemSetExamAssignRepository,
-) ProblemSetService {
- return &problemSetService{
- problemSetRepository: problemSetRepository,
- questionsRepository: questionsRepository,
- problemSetExamAssignRepository: problemSetExamAssignRepository,
- }
-}
-
-// ---------------- Problem Set CRUD ----------------
-
-func (s *problemSetService) CreateProblemSet(ctx context.Context, ps entity.ProblemSet) error {
- return s.problemSetRepository.Create(ctx, ps)
-}
-
-func (s *problemSetService) GetProblemSet(ctx context.Context, id uuid.UUID) (entity.ProblemSet, error) {
- ps, err := s.problemSetRepository.Get(ctx, id)
- if err != nil {
- return entity.ProblemSet{}, ErrProblemSetNotFound
- }
- return ps, nil
-}
-
-func (s *problemSetService) ListProblemSets(ctx context.Context) ([]entity.ProblemSet, error) {
- return s.problemSetRepository.List(ctx)
-}
-
-func (s *problemSetService) UpdateProblemSet(ctx context.Context, ps entity.ProblemSet) error {
- _, err := s.problemSetRepository.Get(ctx, ps.Id)
- if err != nil {
- return ErrProblemSetNotFound
- }
- return s.problemSetRepository.Update(ctx, ps)
-}
-
-func (s *problemSetService) DeleteProblemSet(ctx context.Context, id uuid.UUID) error {
- _, err := s.problemSetRepository.Get(ctx, id)
- if err != nil {
- return ErrProblemSetNotFound
- }
- return s.problemSetRepository.Delete(ctx, id)
-}
-
-// ---------------- Questions ----------------
-
-func (s *problemSetService) AddQuestion(ctx context.Context, q entity.Questions) error {
- _, err := s.problemSetRepository.Get(ctx, q.ProblemSetId)
- if err != nil {
- return ErrProblemSetNotFound
- }
- return s.questionsRepository.Create(ctx, q)
-}
-
-func (s *problemSetService) UpdateQuestion(ctx context.Context, q entity.Questions) error {
- _, err := s.questionsRepository.Get(ctx, q.Id)
- if err != nil {
- return ErrQuestionNotFound
- }
- return s.questionsRepository.Update(ctx, q)
-}
-
-func (s *problemSetService) DeleteQuestion(ctx context.Context, qID uuid.UUID) error {
- _, err := s.questionsRepository.Get(ctx, qID)
- if err != nil {
- return ErrQuestionNotFound
- }
- return s.questionsRepository.Delete(ctx, qID)
-}
-
-func (s *problemSetService) ListQuestions(ctx context.Context, psID uuid.UUID) ([]entity.Questions, error) {
- _, err := s.problemSetRepository.Get(ctx, psID)
- if err != nil {
- return nil, ErrProblemSetNotFound
- }
- return s.questionsRepository.ListByProblemSet(ctx, psID)
-}
-
-// ---------------- Exam ↔ Problem Set (Mapping Table) ----------------
-
-func (s *problemSetService) AssignProblemSetToExam(ctx context.Context, examId uuid.UUID, problemSetId uuid.UUID) error {
- _, err := s.problemSetRepository.Get(ctx, problemSetId)
- if err != nil {
- return ErrProblemSetNotFound
- }
-
- assign := entity.ProblemSetExamAssign{
- Id: uuid.New(),
- ExamId: examId,
- ProblemSetId: problemSetId,
- }
-
- return s.problemSetExamAssignRepository.Create(ctx, assign)
-}
-
-func (s *problemSetService) RemoveAssignedProblemSet(ctx context.Context, assignId uuid.UUID) error {
- return s.problemSetExamAssignRepository.Delete(ctx, assignId)
-}
-
-func (s *problemSetService) GetQuestionById(ctx context.Context, qID uuid.UUID) (entity.Questions, error) {
- question, err := s.questionsRepository.Get(ctx, qID)
- if err != nil {
- return entity.Questions{}, err
- }
- return question, err
-}
+package services
+
+import (
+ "context"
+ "errors"
+
+ entity "abdanhafidz.com/go-boilerplate/models/entity"
+ "abdanhafidz.com/go-boilerplate/repositories"
+ "github.com/google/uuid"
+)
+
+var (
+ ErrProblemSetNotFound = errors.New("problem set not found")
+ ErrQuestionNotFound = errors.New("question not found")
+)
+
+type ProblemSetService interface {
+ CreateProblemSet(ctx context.Context, ps entity.ProblemSet) error
+ GetProblemSet(ctx context.Context, id uuid.UUID) (entity.ProblemSet, error)
+ ListProblemSets(ctx context.Context) ([]entity.ProblemSet, error)
+ UpdateProblemSet(ctx context.Context, ps entity.ProblemSet) error
+ DeleteProblemSet(ctx context.Context, id uuid.UUID) error
+
+ AddQuestion(ctx context.Context, q entity.Questions) error
+ UpdateQuestion(ctx context.Context, q entity.Questions) error
+ DeleteQuestion(ctx context.Context, qID uuid.UUID) error
+ ListQuestions(ctx context.Context, psID uuid.UUID) ([]entity.Questions, error)
+
+ AssignProblemSetToExam(ctx context.Context, examId uuid.UUID, problemSetId uuid.UUID) error
+ RemoveAssignedProblemSet(ctx context.Context, assignId uuid.UUID) error
+ GetQuestionById(ctx context.Context, qID uuid.UUID) (entity.Questions, error)
+}
+
+type problemSetService struct {
+ problemSetRepository repositories.ProblemSetRepository
+ questionsRepository repositories.QuestionsRepository
+ problemSetExamAssignRepository repositories.ProblemSetExamAssignRepository
+}
+
+func NewProblemSetService(
+ problemSetRepository repositories.ProblemSetRepository,
+ questionsRepository repositories.QuestionsRepository,
+ problemSetExamAssignRepository repositories.ProblemSetExamAssignRepository,
+) ProblemSetService {
+ return &problemSetService{
+ problemSetRepository: problemSetRepository,
+ questionsRepository: questionsRepository,
+ problemSetExamAssignRepository: problemSetExamAssignRepository,
+ }
+}
+
+// ---------------- Problem Set CRUD ----------------
+
+func (s *problemSetService) CreateProblemSet(ctx context.Context, ps entity.ProblemSet) error {
+ return s.problemSetRepository.Create(ctx, ps)
+}
+
+func (s *problemSetService) GetProblemSet(ctx context.Context, id uuid.UUID) (entity.ProblemSet, error) {
+ ps, err := s.problemSetRepository.Get(ctx, id)
+ if err != nil {
+ return entity.ProblemSet{}, ErrProblemSetNotFound
+ }
+ return ps, nil
+}
+
+func (s *problemSetService) ListProblemSets(ctx context.Context) ([]entity.ProblemSet, error) {
+ return s.problemSetRepository.List(ctx)
+}
+
+func (s *problemSetService) UpdateProblemSet(ctx context.Context, ps entity.ProblemSet) error {
+ _, err := s.problemSetRepository.Get(ctx, ps.Id)
+ if err != nil {
+ return ErrProblemSetNotFound
+ }
+ return s.problemSetRepository.Update(ctx, ps)
+}
+
+func (s *problemSetService) DeleteProblemSet(ctx context.Context, id uuid.UUID) error {
+ _, err := s.problemSetRepository.Get(ctx, id)
+ if err != nil {
+ return ErrProblemSetNotFound
+ }
+ return s.problemSetRepository.Delete(ctx, id)
+}
+
+// ---------------- Questions ----------------
+
+func (s *problemSetService) AddQuestion(ctx context.Context, q entity.Questions) error {
+ _, err := s.problemSetRepository.Get(ctx, q.ProblemSetId)
+ if err != nil {
+ return ErrProblemSetNotFound
+ }
+ return s.questionsRepository.Create(ctx, q)
+}
+
+func (s *problemSetService) UpdateQuestion(ctx context.Context, q entity.Questions) error {
+ _, err := s.questionsRepository.Get(ctx, q.Id)
+ if err != nil {
+ return ErrQuestionNotFound
+ }
+ return s.questionsRepository.Update(ctx, q)
+}
+
+func (s *problemSetService) DeleteQuestion(ctx context.Context, qID uuid.UUID) error {
+ _, err := s.questionsRepository.Get(ctx, qID)
+ if err != nil {
+ return ErrQuestionNotFound
+ }
+ return s.questionsRepository.Delete(ctx, qID)
+}
+
+func (s *problemSetService) ListQuestions(ctx context.Context, psID uuid.UUID) ([]entity.Questions, error) {
+ _, err := s.problemSetRepository.Get(ctx, psID)
+ if err != nil {
+ return nil, ErrProblemSetNotFound
+ }
+ return s.questionsRepository.ListByProblemSet(ctx, psID)
+}
+
+// ---------------- Exam ↔ Problem Set (Mapping Table) ----------------
+
+func (s *problemSetService) AssignProblemSetToExam(ctx context.Context, examId uuid.UUID, problemSetId uuid.UUID) error {
+ _, err := s.problemSetRepository.Get(ctx, problemSetId)
+ if err != nil {
+ return ErrProblemSetNotFound
+ }
+
+ assign := entity.ProblemSetExamAssign{
+ Id: uuid.New(),
+ ExamId: examId,
+ ProblemSetId: problemSetId,
+ }
+
+ return s.problemSetExamAssignRepository.Create(ctx, assign)
+}
+
+func (s *problemSetService) RemoveAssignedProblemSet(ctx context.Context, assignId uuid.UUID) error {
+ return s.problemSetExamAssignRepository.Delete(ctx, assignId)
+}
+
+func (s *problemSetService) GetQuestionById(ctx context.Context, qID uuid.UUID) (entity.Questions, error) {
+ question, err := s.questionsRepository.Get(ctx, qID)
+ if err != nil {
+ return entity.Questions{}, err
+ }
+ return question, err
+}
diff --git a/services/service.go b/services/service.go
index 81e883fe0f3ace0029faf664d5c51482ef4d7f06..5e568ea8534f2e2b0e7f672e6c87520dbcfb5677 100644
--- a/services/service.go
+++ b/services/service.go
@@ -1 +1 @@
-package services
+package services
diff --git a/services/upload_service.go b/services/upload_service.go
new file mode 100644
index 0000000000000000000000000000000000000000..05615d2a10e8926ef106c83986ca2591a6996bd3
--- /dev/null
+++ b/services/upload_service.go
@@ -0,0 +1,229 @@
+package services
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "mime/multipart"
+ "path/filepath"
+ "regexp"
+ "strings"
+ "time"
+
+ "github.com/google/uuid"
+
+ entity "abdanhafidz.com/go-boilerplate/models/entity"
+ http_error "abdanhafidz.com/go-boilerplate/models/error"
+)
+
+const MB = 1024 * 1024
+
+type uploadConfig struct {
+ MaxBytes int64
+ AllowedExts map[string]bool
+ PathPrefix string
+ MaxCount int
+}
+
+type StorageProvider interface {
+ UploadFile(ctx context.Context, file io.Reader, destinationPath string, contentType string) (string, error)
+}
+
+type FileRepository interface {
+ Create(ctx context.Context, file *entity.File) error
+ FindByID(ctx context.Context, id uuid.UUID) (*entity.File, error)
+}
+
+
+type UploadService struct {
+ storageProvider StorageProvider
+ fileRepo FileRepository
+}
+
+func NewUploadService(storage StorageProvider, repo FileRepository) *UploadService {
+ return &UploadService{
+ storageProvider: storage,
+ fileRepo: repo,
+ }
+}
+
+func (s *UploadService) UploadFiles(ctx context.Context, files []*multipart.FileHeader, uploadContext string, accountID uuid.UUID) ([]entity.File, error) {
+ config, err := s.getUploadConfig(uploadContext)
+ if err != nil {
+ return nil, http_error.BAD_REQUEST_ERROR
+ }
+
+ if len(files) > config.MaxCount {
+ return nil, http_error.BAD_REQUEST_ERROR
+ }
+
+ var uploadedFiles []entity.File
+ var failedCount int
+
+ // 2. Process Files
+ for _, fileHeader := range files {
+ fileEntity, err := s.processSingleFile(ctx, fileHeader, config, uploadContext, accountID)
+ if err != nil {
+ failedCount++
+ continue
+ }
+ uploadedFiles = append(uploadedFiles, *fileEntity)
+ }
+
+ if failedCount > 0 {
+ if len(uploadedFiles) > 0 {
+ return uploadedFiles, http_error.PARTIAL_UPLOAD_FAILURE
+ }
+ return nil, http_error.INVALID_FILE_TYPE
+ }
+
+ return uploadedFiles, nil
+}
+
+func (s *UploadService) GetFileByID(ctx context.Context, fileID uuid.UUID, accountID uuid.UUID) (*entity.File, error) {
+ file, err := s.fileRepo.FindByID(ctx, fileID)
+ if err != nil {
+ if errors.Is(err, http_error.NOT_FOUND_ERROR) {
+ return nil, http_error.NOT_FOUND_ERROR
+ }
+ return nil, http_error.INTERNAL_SERVER_ERROR
+ }
+
+ if file == nil || file.AccountId != accountID {
+ return nil, http_error.NOT_FOUND_ERROR
+ }
+
+ return file, nil
+}
+
+
+func (s *UploadService) processSingleFile(ctx context.Context, fileHeader *multipart.FileHeader, config uploadConfig, uploadContext string, accountID uuid.UUID) (*entity.File, error) {
+ // Validation
+ if !s.validateFile(fileHeader, config) {
+ return nil, errors.New("validation failed")
+ }
+
+
+ ext := strings.ToLower(filepath.Ext(fileHeader.Filename))
+ storedFilename := s.generateStoredFilename(fileHeader.Filename, ext)
+ storagePath := s.generateStoragePath(config.PathPrefix, uploadContext, storedFilename, accountID)
+
+ src, err := fileHeader.Open()
+ if err != nil {
+ return nil, err
+ }
+ defer src.Close()
+
+ contentType := fileHeader.Header.Get("Content-Type")
+ publicURL, err := s.storageProvider.UploadFile(ctx, src, storagePath, contentType)
+ if err != nil {
+ return nil, err
+ }
+
+ // Create Entity
+ fileEntity := &entity.File{
+ Id: uuid.New(),
+ OriginalName: fileHeader.Filename,
+ StoredName: storedFilename,
+ MimeType: contentType,
+ Size: fileHeader.Size,
+ Path: publicURL,
+ Context: uploadContext,
+ AccountId: accountID,
+ CreatedAt: time.Now(),
+ }
+
+ // Save to DB
+ if err := s.fileRepo.Create(ctx, fileEntity); err != nil {
+ return nil, err
+ }
+
+ return fileEntity, nil
+}
+
+func (s *UploadService) validateFile(file *multipart.FileHeader, config uploadConfig) bool {
+ if file.Size == 0 || file.Size > config.MaxBytes {
+ return false
+ }
+
+ ext := strings.ToLower(filepath.Ext(file.Filename))
+ if !config.AllowedExts[ext] {
+ return false
+ }
+
+ // Block dangerous extensions hardcoded as a safety net
+ blockedExts := map[string]bool{".exe": true, ".sh": true, ".bat": true, ".php": true}
+ if blockedExts[ext] {
+ return false
+ }
+
+ return true
+}
+
+func (s *UploadService) generateStoredFilename(originalName string, ext string) string {
+ originalNameWithoutExt := strings.TrimSuffix(originalName, ext)
+ reg := regexp.MustCompile("[^a-zA-Z0-9._-]+")
+ sanitizedRawName := reg.ReplaceAllString(originalNameWithoutExt, "_")
+
+ uniqueID := uuid.New()
+ timestamp := time.Now().Unix()
+ return fmt.Sprintf("%s-%d-%s%s", uniqueID.String(), timestamp, sanitizedRawName, ext)
+}
+
+func (s *UploadService) generateStoragePath(prefix, contextType, filename string, accountID uuid.UUID) string {
+ switch contextType {
+ case "submission":
+ now := time.Now()
+ return fmt.Sprintf("%s/%d/%02d/%s", prefix, now.Year(), now.Month(), filename)
+ case "material":
+ return fmt.Sprintf("%s/%s/%s", prefix, accountID.String(), filename)
+ default:
+ // avatar, general, etc
+ return fmt.Sprintf("%s/%s", prefix, filename)
+ }
+}
+
+func (s *UploadService) getUploadConfig(contextType string) (uploadConfig, error) {
+ codeExts := map[string]bool{".cpp": true, ".c": true, ".py": true, ".java": true, ".go": true, ".js": true, ".txt": true}
+ imgExts := map[string]bool{".jpg": true, ".jpeg": true, ".png": true, ".webp": true}
+ docExts := map[string]bool{".pdf": true}
+
+ allExts := make(map[string]bool)
+ for k, v := range codeExts { allExts[k] = v }
+ for k, v := range imgExts { allExts[k] = v }
+ for k, v := range docExts { allExts[k] = v }
+
+ switch contextType {
+ case "avatar":
+ return uploadConfig{
+ MaxBytes: 5 * MB,
+ AllowedExts: imgExts,
+ PathPrefix: "avatars",
+ MaxCount: 5,
+ }, nil
+ case "material":
+ return uploadConfig{
+ MaxBytes: 10 * MB,
+ AllowedExts: docExts,
+ PathPrefix: "materials",
+ MaxCount: 1,
+ }, nil
+ case "submission":
+ return uploadConfig{
+ MaxBytes: 1 * MB,
+ AllowedExts: codeExts,
+ PathPrefix: "submissions",
+ MaxCount: 1,
+ }, nil
+ case "general":
+ return uploadConfig{
+ MaxBytes: 5 * MB,
+ AllowedExts: allExts,
+ PathPrefix: "temp",
+ MaxCount: 5,
+ }, nil
+ default:
+ return uploadConfig{}, fmt.Errorf("invalid context")
+ }
+}
\ No newline at end of file
diff --git a/space/.github/workflows/main.yml b/space/.github/workflows/main.yml
new file mode 100644
index 0000000000000000000000000000000000000000..8dd6493152ed5ed3b5e3e66359d6e888e4669459
--- /dev/null
+++ b/space/.github/workflows/main.yml
@@ -0,0 +1,49 @@
+name: Deploy to Huggingface
+on:
+ push:
+ branches:
+ - main
+jobs:
+ deploy-to-huggingface:
+ runs-on: ubuntu-latest
+ steps:
+ # Checkout repository
+ - name: Checkout Repository
+ uses: actions/checkout@v3
+ # Setup Git
+ - name: Setup Git for Huggingface
+ run: |
+ git config --global user.email "abdan.hafidz@gmail.com"
+ git config --global user.name "abdanhafidz"
+ # Clone Huggingface Space Repository
+ - name: Clone Huggingface Space
+ env:
+ HF_TOKEN: ${{ secrets.HF_TOKEN }}
+ run: |
+ git clone https://huggingface.co/spaces/lifedebugger/quzuu-api-dev-v2 space
+ # Update Git Remote URL and Pull Latest Changes
+ - name: Update Remote and Pull Changes
+ env:
+ HF_TOKEN: ${{ secrets.HF_TOKEN }}
+ run: |
+ cd space
+ git remote set-url origin https://lifedebugger:$HF_TOKEN@huggingface.co/spaces/lifedebugger/quzuu-api-dev-v2
+ git pull origin main || echo "No changes to pull"
+ # Clean Space Directory - Delete all files except .git
+ - name: Clean Space Directory
+ run: |
+ cd space
+ find . -mindepth 1 -not -path "./.git*" -delete
+ # Copy Files to Huggingface Space
+ - name: Copy Files to Space
+ run: |
+ rsync -av --exclude='.git' ./ space/
+ # Commit and Push to Huggingface Space
+ - name: Commit and Push to Huggingface
+ env:
+ HF_TOKEN: ${{ secrets.HF_TOKEN }}
+ run: |
+ cd space
+ git add .
+ git commit -m "Deploy files from GitHub repository" || echo "No changes to commit"
+ git push origin main || echo "No changes to push"
diff --git a/space/.github/workflows/tests.yml b/space/.github/workflows/tests.yml
new file mode 100644
index 0000000000000000000000000000000000000000..e3caaa689e66eac67dc6a54b91ddb3cbdb7f7fe9
--- /dev/null
+++ b/space/.github/workflows/tests.yml
@@ -0,0 +1,49 @@
+name: Deploy to Huggingface - tests
+on:
+ push:
+ branches:
+ - tests
+jobs:
+ deploy-to-huggingface:
+ runs-on: ubuntu-latest
+ steps:
+ # Checkout repository
+ - name: Checkout Repository
+ uses: actions/checkout@v3
+ # Setup Git
+ - name: Setup Git for Huggingface
+ run: |
+ git config --global user.email "abdan.hafidz@gmail.com"
+ git config --global user.name "abdanhafidz"
+ # Clone Huggingface Space Repository
+ - name: Clone Huggingface Space
+ env:
+ HF_TOKEN: ${{ secrets.HF_TOKEN }}
+ run: |
+ git clone https://huggingface.co/spaces/lifedebugger/quzuu-api-test space
+ # Update Git Remote URL and Pull Latest Changes
+ - name: Update Remote and Pull Changes
+ env:
+ HF_TOKEN: ${{ secrets.HF_TOKEN }}
+ run: |
+ cd space
+ git remote set-url origin https://lifedebugger:$HF_TOKEN@huggingface.co/spaces/lifedebugger/quzuu-api-test
+ git pull origin main || echo "No changes to pull"
+ # Clean Space Directory - Delete all files except .git
+ - name: Clean Space Directory
+ run: |
+ cd space
+ find . -mindepth 1 -not -path "./.git*" -delete
+ # Copy Files to Huggingface Space
+ - name: Copy Files to Space
+ run: |
+ rsync -av --exclude='.git' ./ space/
+ # Commit and Push to Huggingface Space
+ - name: Commit and Push to Huggingface
+ env:
+ HF_TOKEN: ${{ secrets.HF_TOKEN }}
+ run: |
+ cd space
+ git add .
+ git commit -m "Deploy files from GitHub repository" || echo "No changes to commit"
+ git push origin main || echo "No changes to push"
diff --git a/space/.gitignore b/space/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..4cb512ec1f69ca61da8f4e280399077945848a06
--- /dev/null
+++ b/space/.gitignore
@@ -0,0 +1 @@
+/.env
\ No newline at end of file
diff --git a/utils/logger_util.go b/utils/logger_util.go
deleted file mode 100644
index 2fb7092bd36b94812eb3354b06b8e0bbe4a6fa48..0000000000000000000000000000000000000000
--- a/utils/logger_util.go
+++ /dev/null
@@ -1,31 +0,0 @@
-package utils
-
-import (
- "fmt"
- "log"
- "os"
-)
-
-func InternalErrorLog(err_log error) {
- fmt.Println("There is an error!")
-
- file, err := os.OpenFile("logs/error_log.txt", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666)
-
- if err != nil {
- log.Fatal(err)
- }
- log.Println("Error Log :", err_log)
- log.SetOutput(file)
-}
-
-func SecurityLog(security_log string) {
- fmt.Println("There is an error!")
-
- file, err := os.OpenFile("logs/security_log.txt", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666)
-
- if err != nil {
- log.Fatal(err)
- }
- log.Println("Security Log :", security_log)
- log.SetOutput(file)
-}
diff --git a/utils/response_util.go b/utils/response_util.go
deleted file mode 100644
index f7f0e70c5069c832101476f78b0e2ce6b30ac480..0000000000000000000000000000000000000000
--- a/utils/response_util.go
+++ /dev/null
@@ -1,87 +0,0 @@
-package utils
-
-import (
- "errors"
-
- "abdanhafidz.com/go-boilerplate/models/dto"
- http_error "abdanhafidz.com/go-boilerplate/models/error"
- "github.com/gin-gonic/gin"
- "gorm.io/gorm"
-)
-
-func ResponseOK[Tdata any, TMetaData any](c *gin.Context, metaData TMetaData, data Tdata) {
- c.JSON(200, dto.SuccessResponse[Tdata]{
- Status: "success",
- Data: data,
- Message: "Data retrieved Successfully!",
- MetaData: metaData,
- })
-}
-
-func ResponseFAILED[TMetaData any](c *gin.Context, metaData TMetaData, err error) {
- if errors.Is(err, http_error.BAD_REQUEST_ERROR) {
- c.JSON(400, dto.ErrorResponse{
- Status: "error",
- Error: err,
- Message: "Invalid request format!",
- MetaData: metaData,
- })
- return
- } else if errors.Is(err, http_error.INTERNAL_SERVER_ERROR) {
- c.JSON(500, dto.ErrorResponse{
- Status: "error",
- Error: err,
- Message: "Internal Server Error!",
- MetaData: metaData,
- })
- return
- } else if errors.Is(err, http_error.UNAUTHORIZED) {
- c.JSON(401, dto.ErrorResponse{
- Status: "error",
- Error: err,
- Message: "Unauthorized, you don't have permission to access this service!",
- MetaData: metaData,
- })
- return
- } else if errors.Is(err, http_error.DATA_NOT_FOUND) || errors.Is(err, gorm.ErrRecordNotFound) {
- c.JSON(404, dto.ErrorResponse{
- Status: "error",
- Error: err,
- Message: "There is not data with given credential / given parameter!",
- MetaData: metaData,
- })
- return
- } else if errors.Is(err, http_error.TIMEOUT) {
- c.JSON(504, dto.ErrorResponse{
- Status: "error",
- Error: err,
- Message: "Server took to long to respond!",
- MetaData: metaData,
- })
- return
- } else {
- c.JSON(405, dto.ErrorResponse{
- Status: "error",
- Error: err,
- Message: err.Error(),
- MetaData: metaData,
- })
- return
- }
-
-}
-
-func SendResponse[Tdata any, TMetaData any](c *gin.Context, metaData TMetaData, data Tdata, err error) {
- if !c.IsAborted() {
- if err != nil {
- ResponseFAILED(c, metaData, err)
- c.Abort()
- return
- } else {
- ResponseOK(c, metaData, data)
- c.Abort()
- return
- }
- }
-
-}
diff --git a/utils/util.go b/utils/util.go
deleted file mode 100644
index 67a52b1d8552991430bcce6450b17175ad672ebc..0000000000000000000000000000000000000000
--- a/utils/util.go
+++ /dev/null
@@ -1,41 +0,0 @@
-package utils
-
-import (
- "time"
-
- http_error "abdanhafidz.com/go-boilerplate/models/error"
- "github.com/google/uuid"
-)
-
-func ToUUID(s any) (uuid.UUID, error) {
- sStr, ok := s.(string)
- if !ok {
- return uuid.UUID{}, http_error.INTERNAL_SERVER_ERROR
- }
-
- res, err := uuid.Parse(sStr)
- if err != nil {
- return uuid.UUID{}, http_error.INTERNAL_SERVER_ERROR
- }
-
- return res, nil
-}
-func CalculateRemainingTime(startTime, dueTime time.Time) int {
- now := time.Now()
-
- // kalau belum mulai (startTime > now), remaining = full duration
- if startTime.After(now) {
- return int(dueTime.Sub(startTime).Seconds())
- }
-
- remaining := int(dueTime.Sub(now).Seconds())
-
- if remaining < 0 {
- return 0
- }
- return remaining / 60
-}
-
-func Ptr[T any](v T) *T {
- return &v
-}
\ No newline at end of file