Spaces:
Running
Running
Commit ·
483bf6a
1
Parent(s): ab82ffd
Deploy files from GitHub repository
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .env.example +13 -0
- README.md +8 -8
- api_spec.txt +117 -117
- config/database_config.go +32 -27
- config/env_config.go +16 -1
- config/jwt_config.go +24 -24
- config/supabase.go +15 -0
- controllers/academy_controller.go +228 -145
- controllers/upload_controller.go +222 -0
- do_inject_config.ps1 +434 -434
- do_inject_controllers.ps1 +309 -309
- do_inject_middleware.ps1 +311 -311
- do_inject_repository.ps1 +326 -326
- do_inject_services.ps1 +382 -382
- go.mod +1 -0
- go.sum +2 -0
- middleware/authentication_middleware.go +48 -48
- middleware/authorization_middleware.go +43 -43
- middleware/middleware.go +1 -1
- models/dto/academy_dto.go +30 -30
- models/dto/account_details_dto.go +19 -19
- models/dto/event_dto.go +20 -20
- models/dto/exam_dto.go +23 -23
- models/dto/option_dto.go +12 -12
- models/dto/upload_dto.go +43 -0
- models/entity/contant.go +7 -0
- models/entity/entity.go +15 -0
- models/error/error.go +39 -15
- provider/config_provider.go +59 -38
- provider/controller_provider.go +106 -90
- provider/middleware_provider.go +30 -30
- provider/provider.go +85 -70
- provider/repositories_provider.go +178 -170
- provider/services_provider.go +121 -103
- provider/storage_interface.go +13 -0
- provider/supabase_storage.go +42 -0
- repositories/academy_repository.go +232 -157
- repositories/exam_event_answer_repository.go +59 -59
- repositories/exam_event_assign_repository.go +47 -47
- repositories/exam_event_attempt_repository.go +55 -55
- repositories/exam_event_repository.go +85 -85
- repositories/file_repository.go +43 -0
- repositories/problem_set_exam_assign_repository.go +40 -40
- repositories/problem_set_repository.go +57 -57
- repositories/question_repository.go +55 -55
- repositories/result_repository.go +60 -60
- router/academy_router.go +44 -38
- router/account_detail_router.go +18 -18
- router/email_router.go +17 -17
- router/event_router.go +19 -19
.env.example
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
DB_HOST =
|
| 2 |
+
DB_USER =
|
| 3 |
+
DB_PASSWORD =
|
| 4 |
+
DB_PORT =
|
| 5 |
+
DB_NAME =
|
| 6 |
+
HOST_ADDRESS =
|
| 7 |
+
HOST_PORT =
|
| 8 |
+
SALT =
|
| 9 |
+
LOG_PATH = logs
|
| 10 |
+
JWT_SECRET_KEY = s4b3s076
|
| 11 |
+
SUPABASE_URL=
|
| 12 |
+
SUPABASE_SERVICE_KEY=
|
| 13 |
+
SUPABASE_BUCKET_NAME=
|
README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
| 1 |
-
---
|
| 2 |
-
title: Quzuu Api Dev
|
| 3 |
-
emoji: 🐠
|
| 4 |
-
colorFrom: indigo
|
| 5 |
-
colorTo: gray
|
| 6 |
-
sdk: docker
|
| 7 |
-
pinned: false
|
| 8 |
-
---
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Quzuu Api Dev
|
| 3 |
+
emoji: 🐠
|
| 4 |
+
colorFrom: indigo
|
| 5 |
+
colorTo: gray
|
| 6 |
+
sdk: docker
|
| 7 |
+
pinned: false
|
| 8 |
+
---
|
api_spec.txt
CHANGED
|
@@ -1,118 +1,118 @@
|
|
| 1 |
-
|
| 2 |
-
attempt:{
|
| 3 |
-
id_event:
|
| 4 |
-
id_exam:
|
| 5 |
-
remaining_time:
|
| 6 |
-
answered_question:[]str
|
| 7 |
-
question
|
| 8 |
-
}
|
| 9 |
-
|
| 10 |
-
question:{
|
| 11 |
-
id:uuid,
|
| 12 |
-
question_type:string,
|
| 13 |
-
content:string,
|
| 14 |
-
options:[]string,
|
| 15 |
-
current_answer:[]string,
|
| 16 |
-
}
|
| 17 |
-
|
| 18 |
-
Answer Soal -> (1) Request ke Backend untuk attempt
|
| 19 |
-
(2) Rubah UI options
|
| 20 |
-
(3) Fetch Status -> show Status
|
| 21 |
-
(4) Set State question.current_answer update
|
| 22 |
-
(5) Update navigation button soalnya sudah keisi = "hijau", sekarang di nomor berapa = "ungu"
|
| 23 |
-
|
| 24 |
-
# Isian Singkat, Handle onChange =>
|
| 25 |
-
[kjkajsksaf]
|
| 26 |
-
|
| 27 |
-
Before:
|
| 28 |
-
def sum(a,b):
|
| 29 |
-
return {code}
|
| 30 |
-
|
| 31 |
-
print({code})
|
| 32 |
-
|
| 33 |
-
After :
|
| 34 |
-
def sum(a,b):
|
| 35 |
-
return <input type = "text" name="answer[0]" />
|
| 36 |
-
|
| 37 |
-
print(<input type = "text" name="answer[1]" />)
|
| 38 |
-
|
| 39 |
-
# Puzzle:
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
#Payload ngirim jawaban (by default) -> Endpoint Ngirim Answer
|
| 43 |
-
|
| 44 |
-
answer: {
|
| 45 |
-
id_question:uuid,
|
| 46 |
-
answers:[]str
|
| 47 |
-
}
|
| 48 |
-
|
| 49 |
-
#mc
|
| 50 |
-
answer: {
|
| 51 |
-
id_question:uuid,
|
| 52 |
-
answers:["A"]str
|
| 53 |
-
}
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
#Complexmc T/F
|
| 57 |
-
answer: {
|
| 58 |
-
id_question:uuid,
|
| 59 |
-
answers:[1,0,0,1]
|
| 60 |
-
}
|
| 61 |
-
+++
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
++++++++++++
|
| 67 |
-
#code_Puzzle
|
| 68 |
-
|
| 69 |
-
answer: {
|
| 70 |
-
id_question:uuid,
|
| 71 |
-
answers:["a+b","sum(a,b)"]
|
| 72 |
-
}
|
| 73 |
-
|
| 74 |
-
# short_answer
|
| 75 |
-
answer: {
|
| 76 |
-
id_question:uuid,
|
| 77 |
-
answers:["jawaban"]
|
| 78 |
-
}
|
| 79 |
-
|
| 80 |
-
# upload_file
|
| 81 |
-
upload_file -> Form Data
|
| 82 |
-
answer: {
|
| 83 |
-
id_question:uuid,
|
| 84 |
-
answers:["{nama_file}"]
|
| 85 |
-
}
|
| 86 |
-
|
| 87 |
-
#competitive Programming
|
| 88 |
-
question_type:mock_coding[language]
|
| 89 |
-
upload_file -> Form Data (Generate nama_file)
|
| 90 |
-
answer:{
|
| 91 |
-
id_questions:uuid,
|
| 92 |
-
answers:["{nama_file}"]
|
| 93 |
-
}
|
| 94 |
-
|
| 95 |
-
#response answer
|
| 96 |
-
response:{
|
| 97 |
-
success_response,
|
| 98 |
-
meta_data{
|
| 99 |
-
verdict: [AC/WA/TLE/RTE]
|
| 100 |
-
score:
|
| 101 |
-
time_exec:
|
| 102 |
-
memory:
|
| 103 |
-
}
|
| 104 |
-
}
|
| 105 |
-
|
| 106 |
-
#Submit
|
| 107 |
-
{
|
| 108 |
-
id_attempt:
|
| 109 |
-
}
|
| 110 |
-
|
| 111 |
-
Attempt{
|
| 112 |
-
id_user:
|
| 113 |
-
id_event:
|
| 114 |
-
id_exam:
|
| 115 |
-
}
|
| 116 |
-
|
| 117 |
-
|
| 118 |
# Result / Scoreboard
|
|
|
|
| 1 |
+
|
| 2 |
+
attempt:{
|
| 3 |
+
id_event:
|
| 4 |
+
id_exam:
|
| 5 |
+
remaining_time:
|
| 6 |
+
answered_question:[]str
|
| 7 |
+
question
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
question:{
|
| 11 |
+
id:uuid,
|
| 12 |
+
question_type:string,
|
| 13 |
+
content:string,
|
| 14 |
+
options:[]string,
|
| 15 |
+
current_answer:[]string,
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
Answer Soal -> (1) Request ke Backend untuk attempt
|
| 19 |
+
(2) Rubah UI options
|
| 20 |
+
(3) Fetch Status -> show Status
|
| 21 |
+
(4) Set State question.current_answer update
|
| 22 |
+
(5) Update navigation button soalnya sudah keisi = "hijau", sekarang di nomor berapa = "ungu"
|
| 23 |
+
|
| 24 |
+
# Isian Singkat, Handle onChange =>
|
| 25 |
+
[kjkajsksaf]
|
| 26 |
+
|
| 27 |
+
Before:
|
| 28 |
+
def sum(a,b):
|
| 29 |
+
return {code}
|
| 30 |
+
|
| 31 |
+
print({code})
|
| 32 |
+
|
| 33 |
+
After :
|
| 34 |
+
def sum(a,b):
|
| 35 |
+
return <input type = "text" name="answer[0]" />
|
| 36 |
+
|
| 37 |
+
print(<input type = "text" name="answer[1]" />)
|
| 38 |
+
|
| 39 |
+
# Puzzle:
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
#Payload ngirim jawaban (by default) -> Endpoint Ngirim Answer
|
| 43 |
+
|
| 44 |
+
answer: {
|
| 45 |
+
id_question:uuid,
|
| 46 |
+
answers:[]str
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
#mc
|
| 50 |
+
answer: {
|
| 51 |
+
id_question:uuid,
|
| 52 |
+
answers:["A"]str
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
#Complexmc T/F
|
| 57 |
+
answer: {
|
| 58 |
+
id_question:uuid,
|
| 59 |
+
answers:[1,0,0,1]
|
| 60 |
+
}
|
| 61 |
+
+++
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
++++++++++++
|
| 67 |
+
#code_Puzzle
|
| 68 |
+
|
| 69 |
+
answer: {
|
| 70 |
+
id_question:uuid,
|
| 71 |
+
answers:["a+b","sum(a,b)"]
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
# short_answer
|
| 75 |
+
answer: {
|
| 76 |
+
id_question:uuid,
|
| 77 |
+
answers:["jawaban"]
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
# upload_file
|
| 81 |
+
upload_file -> Form Data
|
| 82 |
+
answer: {
|
| 83 |
+
id_question:uuid,
|
| 84 |
+
answers:["{nama_file}"]
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
#competitive Programming
|
| 88 |
+
question_type:mock_coding[language]
|
| 89 |
+
upload_file -> Form Data (Generate nama_file)
|
| 90 |
+
answer:{
|
| 91 |
+
id_questions:uuid,
|
| 92 |
+
answers:["{nama_file}"]
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
#response answer
|
| 96 |
+
response:{
|
| 97 |
+
success_response,
|
| 98 |
+
meta_data{
|
| 99 |
+
verdict: [AC/WA/TLE/RTE]
|
| 100 |
+
score:
|
| 101 |
+
time_exec:
|
| 102 |
+
memory:
|
| 103 |
+
}
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
#Submit
|
| 107 |
+
{
|
| 108 |
+
id_attempt:
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
Attempt{
|
| 112 |
+
id_user:
|
| 113 |
+
id_event:
|
| 114 |
+
id_exam:
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
|
| 118 |
# Result / Scoreboard
|
config/database_config.go
CHANGED
|
@@ -1,51 +1,56 @@
|
|
| 1 |
package config
|
| 2 |
|
| 3 |
import (
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
)
|
| 9 |
|
| 10 |
type DatabaseConfig interface {
|
| 11 |
-
|
| 12 |
-
|
| 13 |
}
|
|
|
|
| 14 |
type databaseConfig struct {
|
| 15 |
-
|
| 16 |
}
|
| 17 |
|
| 18 |
func NewDatabaseConfig(DB_HOST, DB_USER, DB_PASSWORD, DB_NAME, DB_PORT string) DatabaseConfig {
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
|
| 36 |
-
|
| 37 |
}
|
| 38 |
|
| 39 |
func (cfg *databaseConfig) AutoMigrateAll(entities ...interface{}) error {
|
| 40 |
-
// Enable UUID extension first
|
| 41 |
if err := cfg.db.Exec("CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\"").Error; err != nil {
|
| 42 |
-
|
| 43 |
}
|
| 44 |
-
|
|
|
|
| 45 |
err := cfg.db.AutoMigrate(entities...)
|
| 46 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
}
|
| 48 |
|
| 49 |
func (cfg *databaseConfig) GetInstance() *gorm.DB {
|
| 50 |
-
|
| 51 |
-
}
|
|
|
|
| 1 |
package config
|
| 2 |
|
| 3 |
import (
|
| 4 |
+
"fmt"
|
| 5 |
+
"log"
|
| 6 |
+
"gorm.io/driver/postgres"
|
| 7 |
+
"gorm.io/gorm"
|
| 8 |
)
|
| 9 |
|
| 10 |
type DatabaseConfig interface {
|
| 11 |
+
AutoMigrateAll(entities ...interface{}) error
|
| 12 |
+
GetInstance() *gorm.DB
|
| 13 |
}
|
| 14 |
+
|
| 15 |
type databaseConfig struct {
|
| 16 |
+
db *gorm.DB
|
| 17 |
}
|
| 18 |
|
| 19 |
func NewDatabaseConfig(DB_HOST, DB_USER, DB_PASSWORD, DB_NAME, DB_PORT string) DatabaseConfig {
|
| 20 |
+
dsn := fmt.Sprintf(
|
| 21 |
+
"host=%s user=%s password=%s dbname=%s port=%s sslmode=disable TimeZone=Asia/Jakarta",
|
| 22 |
+
DB_HOST, DB_USER, DB_PASSWORD, DB_NAME, DB_PORT,
|
| 23 |
+
)
|
| 24 |
|
| 25 |
+
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
|
| 26 |
+
TranslateError: true,
|
| 27 |
+
})
|
| 28 |
|
| 29 |
+
if err != nil {
|
| 30 |
+
log.Fatal("Failed to connect to database:", err)
|
| 31 |
+
}
|
| 32 |
|
| 33 |
+
db = db.Session(&gorm.Session{
|
| 34 |
+
PrepareStmt: false,
|
| 35 |
+
})
|
| 36 |
|
| 37 |
+
return &databaseConfig{db: db}
|
| 38 |
}
|
| 39 |
|
| 40 |
func (cfg *databaseConfig) AutoMigrateAll(entities ...interface{}) error {
|
|
|
|
| 41 |
if err := cfg.db.Exec("CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\"").Error; err != nil {
|
| 42 |
+
log.Printf("Warning: Could not create uuid-ossp extension: %v", err)
|
| 43 |
}
|
| 44 |
+
|
| 45 |
+
// Run migrations
|
| 46 |
err := cfg.db.AutoMigrate(entities...)
|
| 47 |
+
if err != nil {
|
| 48 |
+
return fmt.Errorf("migration failed: %w", err)
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
return nil
|
| 52 |
}
|
| 53 |
|
| 54 |
func (cfg *databaseConfig) GetInstance() *gorm.DB {
|
| 55 |
+
return cfg.db
|
| 56 |
+
}
|
config/env_config.go
CHANGED
|
@@ -18,6 +18,9 @@ type EnvConfig interface {
|
|
| 18 |
GetDatabasePassword() string
|
| 19 |
GetDatabaseName() string
|
| 20 |
GetSalt() string
|
|
|
|
|
|
|
|
|
|
| 21 |
}
|
| 22 |
|
| 23 |
type envConfig struct {
|
|
@@ -79,7 +82,19 @@ func (e *envConfig) GetDatabaseName() string {
|
|
| 79 |
func (e *envConfig) GetSalt() string {
|
| 80 |
salt := os.Getenv("SALT")
|
| 81 |
if salt == "" {
|
| 82 |
-
return "Def4u|7"
|
| 83 |
}
|
| 84 |
return salt
|
| 85 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
GetDatabasePassword() string
|
| 19 |
GetDatabaseName() string
|
| 20 |
GetSalt() string
|
| 21 |
+
GetSupabaseURL() string
|
| 22 |
+
GetSupabaseKey() string
|
| 23 |
+
GetSupabaseBucket() string
|
| 24 |
}
|
| 25 |
|
| 26 |
type envConfig struct {
|
|
|
|
| 82 |
func (e *envConfig) GetSalt() string {
|
| 83 |
salt := os.Getenv("SALT")
|
| 84 |
if salt == "" {
|
| 85 |
+
return "Def4u|7"
|
| 86 |
}
|
| 87 |
return salt
|
| 88 |
}
|
| 89 |
+
|
| 90 |
+
func (e *envConfig) GetSupabaseURL() string {
|
| 91 |
+
return os.Getenv("SUPABASE_URL")
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
func (e *envConfig) GetSupabaseKey() string {
|
| 95 |
+
return os.Getenv("SUPABASE_SERVICE_KEY")
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
func (e *envConfig) GetSupabaseBucket() string {
|
| 99 |
+
return os.Getenv("SUPABASE_BUCKET_NAME")
|
| 100 |
+
}
|
config/jwt_config.go
CHANGED
|
@@ -1,24 +1,24 @@
|
|
| 1 |
-
package config
|
| 2 |
-
|
| 3 |
-
type JWTConfig interface {
|
| 4 |
-
SetSecretKey(key string)
|
| 5 |
-
GetSecretKey() string
|
| 6 |
-
}
|
| 7 |
-
|
| 8 |
-
type jwtConfig struct {
|
| 9 |
-
secretKey string
|
| 10 |
-
}
|
| 11 |
-
|
| 12 |
-
func NewJWTConfig(secretKey string) JWTConfig {
|
| 13 |
-
return &jwtConfig{
|
| 14 |
-
secretKey: secretKey,
|
| 15 |
-
}
|
| 16 |
-
}
|
| 17 |
-
|
| 18 |
-
func (cfg *jwtConfig) SetSecretKey(key string) {
|
| 19 |
-
cfg.secretKey = key
|
| 20 |
-
}
|
| 21 |
-
|
| 22 |
-
func (cfg *jwtConfig) GetSecretKey() string {
|
| 23 |
-
return cfg.secretKey
|
| 24 |
-
}
|
|
|
|
| 1 |
+
package config
|
| 2 |
+
|
| 3 |
+
type JWTConfig interface {
|
| 4 |
+
SetSecretKey(key string)
|
| 5 |
+
GetSecretKey() string
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
type jwtConfig struct {
|
| 9 |
+
secretKey string
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
func NewJWTConfig(secretKey string) JWTConfig {
|
| 13 |
+
return &jwtConfig{
|
| 14 |
+
secretKey: secretKey,
|
| 15 |
+
}
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
func (cfg *jwtConfig) SetSecretKey(key string) {
|
| 19 |
+
cfg.secretKey = key
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
func (cfg *jwtConfig) GetSecretKey() string {
|
| 23 |
+
return cfg.secretKey
|
| 24 |
+
}
|
config/supabase.go
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package config
|
| 2 |
+
|
| 3 |
+
type SupabaseConfig struct {
|
| 4 |
+
URL string
|
| 5 |
+
ServiceKey string
|
| 6 |
+
BucketName string
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
func NewSupabaseConfig(url, key, bucket string) SupabaseConfig {
|
| 10 |
+
return SupabaseConfig{
|
| 11 |
+
URL: url,
|
| 12 |
+
ServiceKey: key,
|
| 13 |
+
BucketName: bucket,
|
| 14 |
+
}
|
| 15 |
+
}
|
controllers/academy_controller.go
CHANGED
|
@@ -1,145 +1,228 @@
|
|
| 1 |
-
package controllers
|
| 2 |
-
|
| 3 |
-
import (
|
| 4 |
-
"fmt"
|
| 5 |
-
"net/http"
|
| 6 |
-
"strconv"
|
| 7 |
-
|
| 8 |
-
"abdanhafidz.com/go-boilerplate/models/dto"
|
| 9 |
-
"abdanhafidz.com/go-boilerplate/
|
| 10 |
-
"
|
| 11 |
-
"github.com/
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
}
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
//
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
}
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package controllers
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"fmt"
|
| 5 |
+
"net/http"
|
| 6 |
+
"strconv"
|
| 7 |
+
|
| 8 |
+
"abdanhafidz.com/go-boilerplate/models/dto"
|
| 9 |
+
http_error "abdanhafidz.com/go-boilerplate/models/error"
|
| 10 |
+
"abdanhafidz.com/go-boilerplate/services"
|
| 11 |
+
"github.com/gin-gonic/gin"
|
| 12 |
+
"github.com/google/uuid"
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
type AcademyController interface {
|
| 16 |
+
// Academy
|
| 17 |
+
CreateAcademy(ctx *gin.Context)
|
| 18 |
+
GetAcademy(ctx *gin.Context)
|
| 19 |
+
GetAcademyDetail(ctx *gin.Context)
|
| 20 |
+
ListAcademies(ctx *gin.Context)
|
| 21 |
+
UpdateAcademy(ctx *gin.Context)
|
| 22 |
+
DeleteAcademy(ctx *gin.Context)
|
| 23 |
+
|
| 24 |
+
// Material
|
| 25 |
+
GetMaterial(ctx *gin.Context)
|
| 26 |
+
CreateMaterial(ctx *gin.Context)
|
| 27 |
+
DeleteMaterial(ctx *gin.Context)
|
| 28 |
+
|
| 29 |
+
// Content
|
| 30 |
+
CreateContent(ctx *gin.Context)
|
| 31 |
+
GetContent(ctx *gin.Context)
|
| 32 |
+
DeleteContent(ctx *gin.Context)
|
| 33 |
+
|
| 34 |
+
// Progress
|
| 35 |
+
UpdateContentProgress(ctx *gin.Context)
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
type academyController struct {
|
| 39 |
+
academyService services.AcademyService
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
func NewAcademyController(academyService services.AcademyService) AcademyController {
|
| 43 |
+
return &academyController{academyService}
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
// ================= ACADEMY =================
|
| 47 |
+
|
| 48 |
+
func (c *academyController) GetAcademy(ctx *gin.Context) {
|
| 49 |
+
academySlug := ctx.Param("academy_slug")
|
| 50 |
+
accountIdStr := ctx.GetString("account_id")
|
| 51 |
+
accountId, err := uuid.Parse(accountIdStr)
|
| 52 |
+
if err != nil {
|
| 53 |
+
// [FIX] Tambahkan [any, any] agar compiler tau tipenya
|
| 54 |
+
ResponseJSON[any, any](ctx, nil, nil, http_error.UNAUTHORIZED)
|
| 55 |
+
return
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
res, err := c.academyService.GetAcademy(ctx.Request.Context(), accountId, academySlug)
|
| 59 |
+
ResponseJSON(ctx, gin.H{"academy_slug": academySlug}, res, err)
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
func (c *academyController) GetAcademyDetail(ctx *gin.Context) {
|
| 63 |
+
id, err := uuid.Parse(ctx.Param("id"))
|
| 64 |
+
if err != nil {
|
| 65 |
+
// [FIX] Tambahkan [any, any]
|
| 66 |
+
ResponseJSON[any, any](ctx, nil, nil, http_error.BAD_REQUEST_ERROR)
|
| 67 |
+
return
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
res, err := c.academyService.GetAcademyDetail(ctx.Request.Context(), id)
|
| 71 |
+
ResponseJSON(ctx, gin.H{"id": id}, res, err)
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
func (c *academyController) ListAcademies(ctx *gin.Context) {
|
| 75 |
+
accountIdStr := ctx.GetString("account_id")
|
| 76 |
+
accountId, err := uuid.Parse(accountIdStr)
|
| 77 |
+
if err != nil {
|
| 78 |
+
// [FIX] Tambahkan [any, any]
|
| 79 |
+
ResponseJSON[any, any](ctx, nil, nil, http_error.UNAUTHORIZED)
|
| 80 |
+
return
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
fmt.Println("Account ID in ListAcademies:", accountId)
|
| 84 |
+
res, err := c.academyService.ListAcademies(ctx.Request.Context(), accountId)
|
| 85 |
+
ResponseJSON(ctx, gin.H{}, res, err)
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
func (c *academyController) CreateAcademy(ctx *gin.Context) {
|
| 89 |
+
req := RequestJSON[dto.CreateAcademyRequest](ctx)
|
| 90 |
+
res, err := c.academyService.CreateAcademy(ctx.Request.Context(), req)
|
| 91 |
+
ResponseJSON(ctx, req, res, err)
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
func (c *academyController) UpdateAcademy(ctx *gin.Context) {
|
| 95 |
+
id, err := uuid.Parse(ctx.Param("id"))
|
| 96 |
+
if err != nil {
|
| 97 |
+
// [FIX] Tambahkan [any, any]
|
| 98 |
+
ResponseJSON[any, any](ctx, nil, nil, http_error.BAD_REQUEST_ERROR)
|
| 99 |
+
return
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
req := RequestJSON[dto.UpdateAcademyRequest](ctx)
|
| 103 |
+
res, err := c.academyService.UpdateAcademy(ctx.Request.Context(), id, req)
|
| 104 |
+
ResponseJSON(ctx, req, res, err)
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
func (c *academyController) DeleteAcademy(ctx *gin.Context) {
|
| 108 |
+
id, err := uuid.Parse(ctx.Param("id"))
|
| 109 |
+
if err != nil {
|
| 110 |
+
// [FIX] Tambahkan [any, any]
|
| 111 |
+
ResponseJSON[any, any](ctx, nil, nil, http_error.BAD_REQUEST_ERROR)
|
| 112 |
+
return
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
err = c.academyService.DeleteAcademy(ctx.Request.Context(), id)
|
| 116 |
+
ResponseJSON(ctx, gin.H{"id": id}, gin.H{"deleted": true}, err)
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
// ================= MATERIAL =================
|
| 120 |
+
|
| 121 |
+
func (c *academyController) GetMaterial(ctx *gin.Context) {
|
| 122 |
+
academySlug := ctx.Param("academy_slug")
|
| 123 |
+
materialSlug := ctx.Param("material_slug")
|
| 124 |
+
|
| 125 |
+
accountIdStr := ctx.GetString("account_id")
|
| 126 |
+
accountId, err := uuid.Parse(accountIdStr)
|
| 127 |
+
if err != nil {
|
| 128 |
+
// [FIX] Tambahkan [any, any]
|
| 129 |
+
ResponseJSON[any, any](ctx, nil, nil, http_error.UNAUTHORIZED)
|
| 130 |
+
return
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
res, err := c.academyService.GetMaterial(ctx.Request.Context(), accountId, academySlug, materialSlug)
|
| 134 |
+
ResponseJSON(ctx, gin.H{"academy_slug": academySlug, "material_slug": materialSlug}, res, err)
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
func (c *academyController) CreateMaterial(ctx *gin.Context) {
|
| 138 |
+
req := RequestJSON[dto.CreateMaterialRequest](ctx)
|
| 139 |
+
res, err := c.academyService.CreateMaterial(ctx.Request.Context(), req)
|
| 140 |
+
ResponseJSON(ctx, req, res, err)
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
func (c *academyController) DeleteMaterial(ctx *gin.Context) {
|
| 144 |
+
id, err := uuid.Parse(ctx.Param("id"))
|
| 145 |
+
if err != nil {
|
| 146 |
+
// [FIX] Tambahkan [any, any]
|
| 147 |
+
ResponseJSON[any, any](ctx, nil, nil, http_error.BAD_REQUEST_ERROR)
|
| 148 |
+
return
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
err = c.academyService.DeleteMaterial(ctx.Request.Context(), id)
|
| 152 |
+
ResponseJSON(ctx, gin.H{"id": id}, gin.H{"deleted": true}, err)
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
// ================= CONTENT =================
|
| 156 |
+
|
| 157 |
+
func (c *academyController) GetContent(ctx *gin.Context) {
|
| 158 |
+
accountIdStr := ctx.GetString("account_id")
|
| 159 |
+
accountId, err := uuid.Parse(accountIdStr)
|
| 160 |
+
if err != nil {
|
| 161 |
+
// [FIX] Tambahkan [any, any]
|
| 162 |
+
ResponseJSON[any, any](ctx, nil, nil, http_error.UNAUTHORIZED)
|
| 163 |
+
return
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
academySlug := ctx.Param("academy_slug")
|
| 167 |
+
materialSlug := ctx.Param("material_slug")
|
| 168 |
+
|
| 169 |
+
orderID64, err := strconv.ParseUint(ctx.Param("order"), 10, 64)
|
| 170 |
+
if err != nil {
|
| 171 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid 'order' parameter. Must be a positive integer."})
|
| 172 |
+
return
|
| 173 |
+
}
|
| 174 |
+
order := uint(orderID64)
|
| 175 |
+
|
| 176 |
+
res, err := c.academyService.GetContent(ctx.Request.Context(), accountId, academySlug, materialSlug, order)
|
| 177 |
+
ResponseJSON(ctx, gin.H{"academy_slug": academySlug, "material_slug": materialSlug, "content_order": order}, res, err)
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
func (c *academyController) CreateContent(ctx *gin.Context) {
|
| 181 |
+
req := RequestJSON[dto.CreateContentRequest](ctx)
|
| 182 |
+
res, err := c.academyService.CreateContent(ctx.Request.Context(), req)
|
| 183 |
+
ResponseJSON(ctx, req, res, err)
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
func (c *academyController) DeleteContent(ctx *gin.Context) {
|
| 187 |
+
id, err := uuid.Parse(ctx.Param("id"))
|
| 188 |
+
if err != nil {
|
| 189 |
+
// [FIX] Tambahkan [any, any]
|
| 190 |
+
ResponseJSON[any, any](ctx, nil, nil, http_error.BAD_REQUEST_ERROR)
|
| 191 |
+
return
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
err = c.academyService.DeleteContent(ctx.Request.Context(), id)
|
| 195 |
+
ResponseJSON(ctx, gin.H{"id": id}, gin.H{"deleted": true}, err)
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
// ================= PROGRESS =================
|
| 199 |
+
|
| 200 |
+
func (c *academyController) UpdateContentProgress(ctx *gin.Context) {
|
| 201 |
+
accountIdStr := ctx.GetString("account_id")
|
| 202 |
+
accountId, err := uuid.Parse(accountIdStr)
|
| 203 |
+
if err != nil {
|
| 204 |
+
// [FIX] Tambahkan [any, any]
|
| 205 |
+
ResponseJSON[any, any](ctx, nil, nil, http_error.UNAUTHORIZED)
|
| 206 |
+
return
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
academySlug := ctx.Param("academy_slug")
|
| 210 |
+
materialSlug := ctx.Param("material_slug")
|
| 211 |
+
|
| 212 |
+
orderID64, err := strconv.ParseUint(ctx.Param("order"), 10, 64)
|
| 213 |
+
if err != nil {
|
| 214 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid 'order' parameter. Must be a positive integer."})
|
| 215 |
+
return
|
| 216 |
+
}
|
| 217 |
+
order := uint(orderID64)
|
| 218 |
+
|
| 219 |
+
contentProgress, materialProgress, academyProgress, err := c.academyService.UpdateContentProgress(ctx.Request.Context(), accountId, academySlug, materialSlug, order)
|
| 220 |
+
|
| 221 |
+
res := gin.H{
|
| 222 |
+
"content_progress": contentProgress,
|
| 223 |
+
"material_progress": materialProgress,
|
| 224 |
+
"academy_progress": academyProgress,
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
ResponseJSON(ctx, gin.H{"academy_slug": academySlug, "material_slug": materialSlug, "content_order": order}, res, err)
|
| 228 |
+
}
|
controllers/upload_controller.go
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package controllers
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"errors"
|
| 5 |
+
"net/http"
|
| 6 |
+
"path/filepath"
|
| 7 |
+
"strings"
|
| 8 |
+
|
| 9 |
+
"github.com/gin-gonic/gin"
|
| 10 |
+
"github.com/google/uuid"
|
| 11 |
+
|
| 12 |
+
"abdanhafidz.com/go-boilerplate/models/dto"
|
| 13 |
+
http_error "abdanhafidz.com/go-boilerplate/models/error"
|
| 14 |
+
"abdanhafidz.com/go-boilerplate/services"
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
type UploadController struct {
|
| 18 |
+
uploadService *services.UploadService
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
func NewUploadController(s *services.UploadService) *UploadController {
|
| 22 |
+
return &UploadController{uploadService: s}
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
func (c *UploadController) Upload(ctx *gin.Context) {
|
| 26 |
+
// 1. Parse Multipart Form (Limit 32MB)
|
| 27 |
+
if err := ctx.Request.ParseMultipartForm(32 << 20); err != nil {
|
| 28 |
+
if strings.Contains(err.Error(), "http: request body too large") {
|
| 29 |
+
ctx.JSON(http.StatusBadRequest, gin.H{
|
| 30 |
+
"status": "error",
|
| 31 |
+
"message": "File size exceeds the allowed limit of 32MB",
|
| 32 |
+
})
|
| 33 |
+
return
|
| 34 |
+
}
|
| 35 |
+
ctx.JSON(http.StatusBadRequest, gin.H{
|
| 36 |
+
"status": "error",
|
| 37 |
+
"code": "INVALID_FORM",
|
| 38 |
+
"message": "Failed to parse form data",
|
| 39 |
+
})
|
| 40 |
+
return
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
form, err := ctx.MultipartForm()
|
| 44 |
+
if err != nil {
|
| 45 |
+
ctx.JSON(http.StatusBadRequest, gin.H{
|
| 46 |
+
"status": "error",
|
| 47 |
+
"code": "INVALID_DATA",
|
| 48 |
+
"message": "Invalid form data",
|
| 49 |
+
})
|
| 50 |
+
return
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
files := form.File["files"]
|
| 54 |
+
if len(files) == 0 {
|
| 55 |
+
ctx.JSON(http.StatusBadRequest, gin.H{
|
| 56 |
+
"status": "error",
|
| 57 |
+
"message": "No files uploaded",
|
| 58 |
+
})
|
| 59 |
+
return
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
// 2. Determine Upload Context
|
| 63 |
+
uploadContext := ctx.PostForm("context")
|
| 64 |
+
if uploadContext == "" {
|
| 65 |
+
// Auto-infer context based on first file extension if not provided
|
| 66 |
+
ext := strings.ToLower(filepath.Ext(files[0].Filename))
|
| 67 |
+
uploadContext = c.inferContextFromExt(ext)
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
// 3. Get Account ID from Context (Middleware)
|
| 71 |
+
accountIDStr := ctx.GetString("account_id")
|
| 72 |
+
if accountIDStr == "" {
|
| 73 |
+
ctx.JSON(http.StatusUnauthorized, gin.H{
|
| 74 |
+
"status": "error",
|
| 75 |
+
"message": "Unauthorized: Missing account ID",
|
| 76 |
+
})
|
| 77 |
+
return
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
accountID, err := uuid.Parse(accountIDStr)
|
| 81 |
+
if err != nil {
|
| 82 |
+
ctx.JSON(http.StatusUnauthorized, gin.H{
|
| 83 |
+
"status": "error",
|
| 84 |
+
"message": "Unauthorized: Invalid UUID format",
|
| 85 |
+
})
|
| 86 |
+
return
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
// 4. Call Service
|
| 90 |
+
uploadedFiles, err := c.uploadService.UploadFiles(ctx, files, uploadContext, accountID)
|
| 91 |
+
if err != nil {
|
| 92 |
+
// Map Service Errors to HTTP Status
|
| 93 |
+
if errors.Is(err, http_error.FILE_TOO_LARGE) ||
|
| 94 |
+
errors.Is(err, http_error.INVALID_FILE_TYPE) ||
|
| 95 |
+
errors.Is(err, http_error.BAD_REQUEST_ERROR) {
|
| 96 |
+
ctx.JSON(http.StatusBadRequest, gin.H{
|
| 97 |
+
"status": "error",
|
| 98 |
+
"message": err.Error(),
|
| 99 |
+
})
|
| 100 |
+
return
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
if errors.Is(err, http_error.PARTIAL_UPLOAD_FAILURE) {
|
| 104 |
+
ctx.JSON(http.StatusUnprocessableEntity, gin.H{
|
| 105 |
+
"status": "error",
|
| 106 |
+
"message": err.Error(),
|
| 107 |
+
// Opsional: Anda bisa mengembalikan file yang berhasil di sini jika perlu
|
| 108 |
+
})
|
| 109 |
+
return
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{
|
| 113 |
+
"status": "error",
|
| 114 |
+
"message": err.Error(),
|
| 115 |
+
})
|
| 116 |
+
return
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
// 5. Prepare Response DTO
|
| 120 |
+
var fileResponses []dto.FileResponse
|
| 121 |
+
for _, f := range uploadedFiles {
|
| 122 |
+
fileResponses = append(fileResponses, dto.FileResponse{
|
| 123 |
+
Id: f.Id,
|
| 124 |
+
OriginalName: f.OriginalName,
|
| 125 |
+
URL: f.Path,
|
| 126 |
+
MimeType: f.MimeType,
|
| 127 |
+
Size: f.Size,
|
| 128 |
+
CreatedAt: f.CreatedAt,
|
| 129 |
+
})
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
ctx.JSON(http.StatusCreated, dto.FileUploadResponse{
|
| 133 |
+
Status: "success",
|
| 134 |
+
Message: "Files uploaded successfully",
|
| 135 |
+
Data: fileResponses,
|
| 136 |
+
})
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
func (c *UploadController) GetFileByID(ctx *gin.Context) {
|
| 140 |
+
// 1. Validate Param ID
|
| 141 |
+
fileIDStr := ctx.Param("id")
|
| 142 |
+
fileID, err := uuid.Parse(fileIDStr)
|
| 143 |
+
if err != nil {
|
| 144 |
+
ctx.JSON(http.StatusBadRequest, gin.H{
|
| 145 |
+
"status": "error",
|
| 146 |
+
"message": "Invalid file ID format",
|
| 147 |
+
})
|
| 148 |
+
return
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
// 2. Validate Account ID
|
| 152 |
+
accountIDStr := ctx.GetString("account_id")
|
| 153 |
+
if accountIDStr == "" {
|
| 154 |
+
ctx.JSON(http.StatusUnauthorized, gin.H{
|
| 155 |
+
"status": "error",
|
| 156 |
+
"message": "Unauthorized: Missing account ID",
|
| 157 |
+
})
|
| 158 |
+
return
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
accountID, err := uuid.Parse(accountIDStr)
|
| 162 |
+
if err != nil {
|
| 163 |
+
ctx.JSON(http.StatusUnauthorized, gin.H{
|
| 164 |
+
"status": "error",
|
| 165 |
+
"message": "Unauthorized: Invalid UUID format",
|
| 166 |
+
})
|
| 167 |
+
return
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
// 3. Call Service
|
| 171 |
+
fileData, err := c.uploadService.GetFileByID(ctx, fileID, accountID)
|
| 172 |
+
if err != nil {
|
| 173 |
+
if errors.Is(err, http_error.NOT_FOUND_ERROR) {
|
| 174 |
+
ctx.JSON(http.StatusNotFound, gin.H{
|
| 175 |
+
"status": "error",
|
| 176 |
+
"message": "File not found or access denied",
|
| 177 |
+
})
|
| 178 |
+
return
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{
|
| 182 |
+
"status": "error",
|
| 183 |
+
"message": err.Error(),
|
| 184 |
+
})
|
| 185 |
+
return
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
// 4. Response
|
| 189 |
+
response := dto.FileResponse{
|
| 190 |
+
Id: fileData.Id,
|
| 191 |
+
OriginalName: fileData.OriginalName,
|
| 192 |
+
URL: fileData.Path,
|
| 193 |
+
MimeType: fileData.MimeType,
|
| 194 |
+
Size: fileData.Size,
|
| 195 |
+
CreatedAt: fileData.CreatedAt,
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
ctx.JSON(http.StatusOK, dto.FileResponseSingle{
|
| 199 |
+
Status: "success",
|
| 200 |
+
Message: "File retrieved successfully",
|
| 201 |
+
Data: response,
|
| 202 |
+
})
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
// Helper untuk logika inferensi context (clean code)
|
| 206 |
+
func (c *UploadController) inferContextFromExt(ext string) string {
|
| 207 |
+
isSourceCode := map[string]bool{
|
| 208 |
+
".cpp": true, ".c": true, ".py": true, ".java": true,
|
| 209 |
+
".go": true, ".js": true, ".txt": true,
|
| 210 |
+
}
|
| 211 |
+
isDocument := map[string]bool{
|
| 212 |
+
".pdf": true,
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
if isSourceCode[ext] {
|
| 216 |
+
return "submission"
|
| 217 |
+
}
|
| 218 |
+
if isDocument[ext] {
|
| 219 |
+
return "material"
|
| 220 |
+
}
|
| 221 |
+
return "general"
|
| 222 |
+
}
|
do_inject_config.ps1
CHANGED
|
@@ -1,435 +1,435 @@
|
|
| 1 |
-
#Requires -Version 5.1
|
| 2 |
-
<#
|
| 3 |
-
.SYNOPSIS
|
| 4 |
-
Automatic Dependency Injection Generator for Go Configuration
|
| 5 |
-
|
| 6 |
-
.DESCRIPTION
|
| 7 |
-
Scans ./config/ directory, discovers all config constructors, infers their dependencies,
|
| 8 |
-
and generates provider/config_provider.go with full DI wiring. Special handling for
|
| 9 |
-
chained dependencies where configs depend on other configs (e.g., DatabaseConfig depends on EnvConfig).
|
| 10 |
-
|
| 11 |
-
.EXAMPLE
|
| 12 |
-
.\config_injector.ps1
|
| 13 |
-
|
| 14 |
-
.NOTES
|
| 15 |
-
- Works with PowerShell 5.1+ and PowerShell 7+
|
| 16 |
-
- No external dependencies required
|
| 17 |
-
- Supports multi-line constructor signatures
|
| 18 |
-
- Handles config-to-config dependencies with topological sorting
|
| 19 |
-
- Special handling for method calls like envConfig.GetDatabaseHost()
|
| 20 |
-
#>
|
| 21 |
-
|
| 22 |
-
[CmdletBinding()]
|
| 23 |
-
param()
|
| 24 |
-
|
| 25 |
-
# Configuration
|
| 26 |
-
$ConfigDir = "./config"
|
| 27 |
-
$OutputFile = "provider/config_provider.go"
|
| 28 |
-
$ModulePath = "abdanhafidz.com/go-boilerplate/config"
|
| 29 |
-
|
| 30 |
-
# ANSI colors for better output
|
| 31 |
-
$script:UseColors = $Host.UI.SupportsVirtualTerminal
|
| 32 |
-
function Write-ColorOutput {
|
| 33 |
-
param([string]$Message, [string]$Color = "White")
|
| 34 |
-
if ($script:UseColors) {
|
| 35 |
-
$colors = @{
|
| 36 |
-
"Green" = "`e[32m"; "Yellow" = "`e[33m"; "Red" = "`e[31m"
|
| 37 |
-
"Cyan" = "`e[36m"; "Blue" = "`e[34m"; "Reset" = "`e[0m"
|
| 38 |
-
}
|
| 39 |
-
Write-Host "$($colors[$Color])$Message$($colors['Reset'])"
|
| 40 |
-
} else {
|
| 41 |
-
Write-Host $Message
|
| 42 |
-
}
|
| 43 |
-
}
|
| 44 |
-
|
| 45 |
-
# Data structures
|
| 46 |
-
class ConfigInfo {
|
| 47 |
-
[string]$ConstructorName # NewDatabaseConfig
|
| 48 |
-
[string]$Domain # DatabaseConfig
|
| 49 |
-
[string]$VarName # databaseConfig
|
| 50 |
-
[System.Collections.Generic.List[Parameter]]$Parameters
|
| 51 |
-
[System.Collections.Generic.List[string]]$ConfigDependencies
|
| 52 |
-
|
| 53 |
-
ConfigInfo() {
|
| 54 |
-
$this.Parameters = [System.Collections.Generic.List[Parameter]]::new()
|
| 55 |
-
$this.ConfigDependencies = [System.Collections.Generic.List[string]]::new()
|
| 56 |
-
}
|
| 57 |
-
}
|
| 58 |
-
|
| 59 |
-
class Parameter {
|
| 60 |
-
[string]$Name
|
| 61 |
-
[string]$RawType
|
| 62 |
-
[string]$NormalizedType
|
| 63 |
-
}
|
| 64 |
-
|
| 65 |
-
function Get-LowerCamelCase {
|
| 66 |
-
param([string]$Text)
|
| 67 |
-
if ($Text.Length -eq 0) { return $Text }
|
| 68 |
-
return $Text.Substring(0, 1).ToLower() + $Text.Substring(1)
|
| 69 |
-
}
|
| 70 |
-
|
| 71 |
-
function Normalize-TypeName {
|
| 72 |
-
param([string]$TypeStr)
|
| 73 |
-
|
| 74 |
-
# Remove leading pointer
|
| 75 |
-
$cleaned = $TypeStr -replace '^\*+', ''
|
| 76 |
-
|
| 77 |
-
# Remove package prefix (everything before last dot)
|
| 78 |
-
if ($cleaned -match '\.([^.]+)$') {
|
| 79 |
-
$cleaned = $matches[1]
|
| 80 |
-
}
|
| 81 |
-
|
| 82 |
-
return $cleaned.Trim()
|
| 83 |
-
}
|
| 84 |
-
|
| 85 |
-
function Parse-GoFiles {
|
| 86 |
-
param([string]$Directory)
|
| 87 |
-
|
| 88 |
-
Write-ColorOutput "Scanning for config constructors in $Directory..." "Cyan"
|
| 89 |
-
|
| 90 |
-
if (-not (Test-Path $Directory)) {
|
| 91 |
-
Write-ColorOutput "ERROR: Directory '$Directory' not found!" "Red"
|
| 92 |
-
exit 1
|
| 93 |
-
}
|
| 94 |
-
|
| 95 |
-
$goFiles = Get-ChildItem -Path $Directory -Filter "*.go" -Recurse -File
|
| 96 |
-
$configs = [System.Collections.Generic.List[ConfigInfo]]::new()
|
| 97 |
-
|
| 98 |
-
foreach ($file in $goFiles) {
|
| 99 |
-
$content = Get-Content $file.FullName -Raw
|
| 100 |
-
|
| 101 |
-
# Match function signatures (support multi-line)
|
| 102 |
-
# Pattern: func NewXxxConfig(...) XxxConfig
|
| 103 |
-
$pattern = '(?ms)func\s+(New[a-zA-Z0-9]+Config)\s*\(([^)]*)\)\s+([a-zA-Z0-9*_.]+Config)'
|
| 104 |
-
$matches = [regex]::Matches($content, $pattern)
|
| 105 |
-
|
| 106 |
-
foreach ($match in $matches) {
|
| 107 |
-
$constructorName = $match.Groups[1].Value
|
| 108 |
-
$paramsStr = $match.Groups[2].Value
|
| 109 |
-
$returnType = $match.Groups[3].Value
|
| 110 |
-
|
| 111 |
-
# Extract domain name (XxxConfig)
|
| 112 |
-
$domain = Normalize-TypeName $returnType
|
| 113 |
-
$varName = Get-LowerCamelCase $domain
|
| 114 |
-
|
| 115 |
-
$config = [ConfigInfo]::new()
|
| 116 |
-
$config.ConstructorName = $constructorName
|
| 117 |
-
$config.Domain = $domain
|
| 118 |
-
$config.VarName = $varName
|
| 119 |
-
|
| 120 |
-
# Parse parameters
|
| 121 |
-
if ($paramsStr.Trim() -ne "") {
|
| 122 |
-
# Split by comma, but be careful with nested types
|
| 123 |
-
$paramList = $paramsStr -split ',\s*(?![^<>]*>)'
|
| 124 |
-
|
| 125 |
-
foreach ($param in $paramList) {
|
| 126 |
-
$param = $param.Trim()
|
| 127 |
-
if ($param -eq "") { continue }
|
| 128 |
-
|
| 129 |
-
# Split into name and type
|
| 130 |
-
$parts = $param -split '\s+', 2
|
| 131 |
-
|
| 132 |
-
$p = [Parameter]::new()
|
| 133 |
-
if ($parts.Count -eq 2) {
|
| 134 |
-
$p.Name = $parts[0]
|
| 135 |
-
$p.RawType = $parts[1]
|
| 136 |
-
} elseif ($parts.Count -eq 1) {
|
| 137 |
-
# Anonymous parameter - synthesize name
|
| 138 |
-
$p.Name = "param$($config.Parameters.Count)"
|
| 139 |
-
$p.RawType = $parts[0]
|
| 140 |
-
} else {
|
| 141 |
-
continue
|
| 142 |
-
}
|
| 143 |
-
|
| 144 |
-
$p.NormalizedType = Normalize-TypeName $p.RawType
|
| 145 |
-
$config.Parameters.Add($p)
|
| 146 |
-
|
| 147 |
-
# Track config dependencies (configs that depend on other configs)
|
| 148 |
-
if ($p.NormalizedType -match 'Config$') {
|
| 149 |
-
$config.ConfigDependencies.Add($p.NormalizedType)
|
| 150 |
-
}
|
| 151 |
-
}
|
| 152 |
-
}
|
| 153 |
-
|
| 154 |
-
$configs.Add($config)
|
| 155 |
-
Write-ColorOutput " Found: $constructorName" "Green"
|
| 156 |
-
}
|
| 157 |
-
}
|
| 158 |
-
|
| 159 |
-
if ($configs.Count -eq 0) {
|
| 160 |
-
Write-ColorOutput "No config constructors found matching pattern 'NewXxxConfig'!" "Red"
|
| 161 |
-
exit 1
|
| 162 |
-
}
|
| 163 |
-
|
| 164 |
-
Write-ColorOutput "`nTotal configs discovered: $($configs.Count)" "Blue"
|
| 165 |
-
return $configs
|
| 166 |
-
}
|
| 167 |
-
|
| 168 |
-
function Get-TopologicalOrder {
|
| 169 |
-
param([System.Collections.Generic.List[ConfigInfo]]$Configs)
|
| 170 |
-
|
| 171 |
-
Write-ColorOutput "`nBuilding dependency graph..." "Cyan"
|
| 172 |
-
|
| 173 |
-
# Build adjacency list
|
| 174 |
-
$graph = @{}
|
| 175 |
-
$inDegree = @{}
|
| 176 |
-
$domainToConfig = @{}
|
| 177 |
-
|
| 178 |
-
foreach ($cfg in $Configs) {
|
| 179 |
-
$graph[$cfg.Domain] = [System.Collections.Generic.List[string]]::new()
|
| 180 |
-
$inDegree[$cfg.Domain] = 0
|
| 181 |
-
$domainToConfig[$cfg.Domain] = $cfg
|
| 182 |
-
}
|
| 183 |
-
|
| 184 |
-
# Build edges (dependencies)
|
| 185 |
-
foreach ($cfg in $Configs) {
|
| 186 |
-
foreach ($dep in $cfg.ConfigDependencies) {
|
| 187 |
-
if ($graph.ContainsKey($dep)) {
|
| 188 |
-
$graph[$dep].Add($cfg.Domain)
|
| 189 |
-
$inDegree[$cfg.Domain]++
|
| 190 |
-
}
|
| 191 |
-
}
|
| 192 |
-
}
|
| 193 |
-
|
| 194 |
-
# Kahn's algorithm for topological sort
|
| 195 |
-
$queue = [System.Collections.Generic.Queue[string]]::new()
|
| 196 |
-
foreach ($domain in $inDegree.Keys) {
|
| 197 |
-
if ($inDegree[$domain] -eq 0) {
|
| 198 |
-
$queue.Enqueue($domain)
|
| 199 |
-
}
|
| 200 |
-
}
|
| 201 |
-
|
| 202 |
-
$sorted = [System.Collections.Generic.List[string]]::new()
|
| 203 |
-
|
| 204 |
-
while ($queue.Count -gt 0) {
|
| 205 |
-
$current = $queue.Dequeue()
|
| 206 |
-
$sorted.Add($current)
|
| 207 |
-
|
| 208 |
-
foreach ($neighbor in $graph[$current]) {
|
| 209 |
-
$inDegree[$neighbor]--
|
| 210 |
-
if ($inDegree[$neighbor] -eq 0) {
|
| 211 |
-
$queue.Enqueue($neighbor)
|
| 212 |
-
}
|
| 213 |
-
}
|
| 214 |
-
}
|
| 215 |
-
|
| 216 |
-
# Check for cycles
|
| 217 |
-
if ($sorted.Count -ne $Configs.Count) {
|
| 218 |
-
$remaining = $inDegree.Keys | Where-Object { $inDegree[$_] -gt 0 }
|
| 219 |
-
Write-ColorOutput "`nERROR: Circular dependency detected in configs!" "Red"
|
| 220 |
-
Write-ColorOutput "Configs involved in cycle: $($remaining -join ', ')" "Yellow"
|
| 221 |
-
exit 1
|
| 222 |
-
}
|
| 223 |
-
|
| 224 |
-
Write-ColorOutput " Dependency graph validated (no cycles)" "Green"
|
| 225 |
-
Write-ColorOutput " Topological order: $($sorted -join ' -> ')" "Blue"
|
| 226 |
-
|
| 227 |
-
# Return configs in topological order
|
| 228 |
-
return $sorted | ForEach-Object { $domainToConfig[$_] }
|
| 229 |
-
}
|
| 230 |
-
|
| 231 |
-
function Resolve-ConfigArgument {
|
| 232 |
-
param(
|
| 233 |
-
[Parameter]$Param,
|
| 234 |
-
[string]$DependentConfigVar
|
| 235 |
-
)
|
| 236 |
-
|
| 237 |
-
$type = $Param.NormalizedType
|
| 238 |
-
$paramName = $Param.Name
|
| 239 |
-
|
| 240 |
-
# SPECIAL CASE MAPPINGS FOR CONFIG
|
| 241 |
-
# ============================================
|
| 242 |
-
|
| 243 |
-
# 1. Config pattern: XxxConfig -> already instantiated config variable
|
| 244 |
-
if ($type -match '^(.+)Config$') {
|
| 245 |
-
$configVarName = Get-LowerCamelCase $type
|
| 246 |
-
return $configVarName
|
| 247 |
-
}
|
| 248 |
-
|
| 249 |
-
# 2. String parameters - try to infer from parameter name and match with config getter methods
|
| 250 |
-
if ($type -eq "string") {
|
| 251 |
-
# Common patterns for EnvConfig getters
|
| 252 |
-
$getterMappings = @{
|
| 253 |
-
"host" = "GetDatabaseHost()"
|
| 254 |
-
"databaseHost" = "GetDatabaseHost()"
|
| 255 |
-
"user" = "GetDatabaseUser()"
|
| 256 |
-
"databaseUser" = "GetDatabaseUser()"
|
| 257 |
-
"password" = "GetDatabasePassword()"
|
| 258 |
-
"databasePassword" = "GetDatabasePassword()"
|
| 259 |
-
"name" = "GetDatabaseName()"
|
| 260 |
-
"databaseName" = "GetDatabaseName()"
|
| 261 |
-
"dbName" = "GetDatabaseName()"
|
| 262 |
-
"port" = "GetDatabasePort()"
|
| 263 |
-
"databasePort" = "GetDatabasePort()"
|
| 264 |
-
"salt" = "GetSalt()"
|
| 265 |
-
"secret" = "GetSecretKey()"
|
| 266 |
-
"secretKey" = "GetSecretKey()"
|
| 267 |
-
"jwtSecret" = "GetSecretKey()"
|
| 268 |
-
"apiKey" = "GetAPIKey()"
|
| 269 |
-
"timezone" = "GetTimezone()"
|
| 270 |
-
}
|
| 271 |
-
|
| 272 |
-
# Try to find matching getter
|
| 273 |
-
foreach ($key in $getterMappings.Keys) {
|
| 274 |
-
if ($paramName -like "*$key*") {
|
| 275 |
-
return "envConfig.$($getterMappings[$key])"
|
| 276 |
-
}
|
| 277 |
-
}
|
| 278 |
-
|
| 279 |
-
# If dependent on a config, try to construct getter name from param name
|
| 280 |
-
if ($DependentConfigVar) {
|
| 281 |
-
# Convert paramName to PascalCase for getter
|
| 282 |
-
$getterName = (Get-Culture).TextInfo.ToTitleCase($paramName)
|
| 283 |
-
$getterName = $getterName -replace '\s', ''
|
| 284 |
-
return "${DependentConfigVar}.Get${getterName}()"
|
| 285 |
-
}
|
| 286 |
-
}
|
| 287 |
-
|
| 288 |
-
# 3. Int/port parameters
|
| 289 |
-
if ($type -eq "int" -or $type -eq "int32" -or $type -eq "int64") {
|
| 290 |
-
if ($paramName -match "port") {
|
| 291 |
-
return "envConfig.GetDatabasePort()"
|
| 292 |
-
}
|
| 293 |
-
}
|
| 294 |
-
|
| 295 |
-
# ADD MORE SPECIAL CASES HERE:
|
| 296 |
-
# --------------------------------------------
|
| 297 |
-
# Example: Redis config
|
| 298 |
-
# if ($paramName -match "redis") {
|
| 299 |
-
# return "envConfig.GetRedisURL()"
|
| 300 |
-
# }
|
| 301 |
-
#
|
| 302 |
-
# Example: Mail config
|
| 303 |
-
# if ($paramName -match "smtp") {
|
| 304 |
-
# return "envConfig.GetSMTPHost()"
|
| 305 |
-
# }
|
| 306 |
-
# --------------------------------------------
|
| 307 |
-
|
| 308 |
-
# 4. Hardcoded constants (timezone example)
|
| 309 |
-
if ($type -eq "string" -and $paramName -match "timezone|location") {
|
| 310 |
-
return "`"Asia/Jakarta`""
|
| 311 |
-
}
|
| 312 |
-
|
| 313 |
-
# 5. Fallback: unresolved type
|
| 314 |
-
return "/* TODO: provide $($Param.RawType) for $paramName */"
|
| 315 |
-
}
|
| 316 |
-
|
| 317 |
-
function Generate-ProviderCode {
|
| 318 |
-
param([System.Collections.Generic.List[ConfigInfo]]$ConfigsInOrder)
|
| 319 |
-
|
| 320 |
-
Write-ColorOutput "`nGenerating config provider code..." "Cyan"
|
| 321 |
-
|
| 322 |
-
$sb = [System.Text.StringBuilder]::new()
|
| 323 |
-
[void]$sb.AppendLine("package provider")
|
| 324 |
-
[void]$sb.AppendLine()
|
| 325 |
-
[void]$sb.AppendLine("import `"$ModulePath`"")
|
| 326 |
-
[void]$sb.AppendLine()
|
| 327 |
-
|
| 328 |
-
# Interface
|
| 329 |
-
[void]$sb.AppendLine("type ConfigProvider interface {")
|
| 330 |
-
foreach ($cfg in $ConfigsInOrder) {
|
| 331 |
-
$line = "`tProvide$($cfg.Domain)() config.$($cfg.Domain)"
|
| 332 |
-
[void]$sb.AppendLine($line)
|
| 333 |
-
}
|
| 334 |
-
[void]$sb.AppendLine("}")
|
| 335 |
-
[void]$sb.AppendLine()
|
| 336 |
-
|
| 337 |
-
# Struct
|
| 338 |
-
[void]$sb.AppendLine("type configProvider struct {")
|
| 339 |
-
foreach ($cfg in $ConfigsInOrder) {
|
| 340 |
-
$line = "`t$($cfg.VarName) config.$($cfg.Domain)"
|
| 341 |
-
[void]$sb.AppendLine($line)
|
| 342 |
-
}
|
| 343 |
-
[void]$sb.AppendLine("}")
|
| 344 |
-
[void]$sb.AppendLine()
|
| 345 |
-
|
| 346 |
-
# Constructor
|
| 347 |
-
[void]$sb.AppendLine("func NewConfigProvider() ConfigProvider {")
|
| 348 |
-
|
| 349 |
-
# Initialize configs in topological order
|
| 350 |
-
foreach ($cfg in $ConfigsInOrder) {
|
| 351 |
-
# Check if this config depends on another config
|
| 352 |
-
$dependentConfigVar = $null
|
| 353 |
-
if ($cfg.ConfigDependencies.Count -gt 0) {
|
| 354 |
-
$dependentConfigVar = Get-LowerCamelCase $cfg.ConfigDependencies[0]
|
| 355 |
-
}
|
| 356 |
-
|
| 357 |
-
$args = @()
|
| 358 |
-
foreach ($param in $cfg.Parameters) {
|
| 359 |
-
$args += Resolve-ConfigArgument -Param $param -DependentConfigVar $dependentConfigVar
|
| 360 |
-
}
|
| 361 |
-
$argsStr = $args -join ", "
|
| 362 |
-
$line = "`t$($cfg.VarName) := config.$($cfg.ConstructorName)($argsStr)"
|
| 363 |
-
[void]$sb.AppendLine($line)
|
| 364 |
-
}
|
| 365 |
-
|
| 366 |
-
[void]$sb.AppendLine("`treturn &configProvider{")
|
| 367 |
-
foreach ($cfg in $ConfigsInOrder) {
|
| 368 |
-
$line = "`t`t$($cfg.VarName): $($cfg.VarName),"
|
| 369 |
-
[void]$sb.AppendLine($line)
|
| 370 |
-
}
|
| 371 |
-
[void]$sb.AppendLine("`t}")
|
| 372 |
-
[void]$sb.AppendLine("}")
|
| 373 |
-
[void]$sb.AppendLine()
|
| 374 |
-
|
| 375 |
-
# Getter methods
|
| 376 |
-
foreach ($cfg in $ConfigsInOrder) {
|
| 377 |
-
[void]$sb.AppendLine("func (c *configProvider) Provide$($cfg.Domain)() config.$($cfg.Domain) {")
|
| 378 |
-
[void]$sb.AppendLine("`treturn c.$($cfg.VarName)")
|
| 379 |
-
[void]$sb.AppendLine("}")
|
| 380 |
-
[void]$sb.AppendLine()
|
| 381 |
-
}
|
| 382 |
-
|
| 383 |
-
return $sb.ToString()
|
| 384 |
-
}
|
| 385 |
-
|
| 386 |
-
function Write-ProviderFile {
|
| 387 |
-
param([string]$Code, [string]$OutputPath)
|
| 388 |
-
|
| 389 |
-
Write-ColorOutput "Writing to $OutputPath..." "Cyan"
|
| 390 |
-
|
| 391 |
-
# Ensure directory exists
|
| 392 |
-
$dir = Split-Path $OutputPath -Parent
|
| 393 |
-
if ($dir -and -not (Test-Path $dir)) {
|
| 394 |
-
New-Item -ItemType Directory -Path $dir -Force | Out-Null
|
| 395 |
-
}
|
| 396 |
-
|
| 397 |
-
# Write file as UTF-8 without BOM
|
| 398 |
-
$utf8NoBom = [System.Text.UTF8Encoding]::new($false)
|
| 399 |
-
[System.IO.File]::WriteAllText($OutputPath, $Code, $utf8NoBom)
|
| 400 |
-
|
| 401 |
-
Write-ColorOutput " Successfully generated $OutputPath" "Green"
|
| 402 |
-
}
|
| 403 |
-
|
| 404 |
-
# ============================================
|
| 405 |
-
# MAIN EXECUTION
|
| 406 |
-
# ============================================
|
| 407 |
-
|
| 408 |
-
try {
|
| 409 |
-
Write-ColorOutput "`n=========================================" "Blue"
|
| 410 |
-
Write-ColorOutput " Go Config Provider Generator v1.0" "Blue"
|
| 411 |
-
Write-ColorOutput "=========================================`n" "Blue"
|
| 412 |
-
|
| 413 |
-
# Step 1: Parse all config constructors
|
| 414 |
-
$configs = Parse-GoFiles -Directory $ConfigDir
|
| 415 |
-
|
| 416 |
-
# Step 2: Perform topological sort (configs can depend on other configs)
|
| 417 |
-
$sortedConfigs = Get-TopologicalOrder -Configs $configs
|
| 418 |
-
|
| 419 |
-
# Step 3: Generate provider code
|
| 420 |
-
$code = Generate-ProviderCode -ConfigsInOrder $sortedConfigs
|
| 421 |
-
|
| 422 |
-
# Step 4: Write to file
|
| 423 |
-
Write-ProviderFile -Code $code -OutputPath $OutputFile
|
| 424 |
-
|
| 425 |
-
Write-ColorOutput "`nSUCCESS! Config provider generated successfully.`n" "Green"
|
| 426 |
-
Write-ColorOutput "Next steps:" "Cyan"
|
| 427 |
-
Write-ColorOutput " 1. Review $OutputFile" "White"
|
| 428 |
-
Write-ColorOutput " 2. Fill any /* TODO: provide ... */ placeholders" "White"
|
| 429 |
-
Write-ColorOutput " 3. Run: go build ./provider" "White"
|
| 430 |
-
|
| 431 |
-
} catch {
|
| 432 |
-
Write-ColorOutput "`nERROR: $($_.Exception.Message)" "Red"
|
| 433 |
-
Write-ColorOutput "Stack trace: $($_.ScriptStackTrace)" "Yellow"
|
| 434 |
-
exit 1
|
| 435 |
}
|
|
|
|
| 1 |
+
#Requires -Version 5.1
|
| 2 |
+
<#
|
| 3 |
+
.SYNOPSIS
|
| 4 |
+
Automatic Dependency Injection Generator for Go Configuration
|
| 5 |
+
|
| 6 |
+
.DESCRIPTION
|
| 7 |
+
Scans ./config/ directory, discovers all config constructors, infers their dependencies,
|
| 8 |
+
and generates provider/config_provider.go with full DI wiring. Special handling for
|
| 9 |
+
chained dependencies where configs depend on other configs (e.g., DatabaseConfig depends on EnvConfig).
|
| 10 |
+
|
| 11 |
+
.EXAMPLE
|
| 12 |
+
.\config_injector.ps1
|
| 13 |
+
|
| 14 |
+
.NOTES
|
| 15 |
+
- Works with PowerShell 5.1+ and PowerShell 7+
|
| 16 |
+
- No external dependencies required
|
| 17 |
+
- Supports multi-line constructor signatures
|
| 18 |
+
- Handles config-to-config dependencies with topological sorting
|
| 19 |
+
- Special handling for method calls like envConfig.GetDatabaseHost()
|
| 20 |
+
#>
|
| 21 |
+
|
| 22 |
+
[CmdletBinding()]
|
| 23 |
+
param()
|
| 24 |
+
|
| 25 |
+
# Configuration
|
| 26 |
+
$ConfigDir = "./config"
|
| 27 |
+
$OutputFile = "provider/config_provider.go"
|
| 28 |
+
$ModulePath = "abdanhafidz.com/go-boilerplate/config"
|
| 29 |
+
|
| 30 |
+
# ANSI colors for better output
|
| 31 |
+
$script:UseColors = $Host.UI.SupportsVirtualTerminal
|
| 32 |
+
function Write-ColorOutput {
|
| 33 |
+
param([string]$Message, [string]$Color = "White")
|
| 34 |
+
if ($script:UseColors) {
|
| 35 |
+
$colors = @{
|
| 36 |
+
"Green" = "`e[32m"; "Yellow" = "`e[33m"; "Red" = "`e[31m"
|
| 37 |
+
"Cyan" = "`e[36m"; "Blue" = "`e[34m"; "Reset" = "`e[0m"
|
| 38 |
+
}
|
| 39 |
+
Write-Host "$($colors[$Color])$Message$($colors['Reset'])"
|
| 40 |
+
} else {
|
| 41 |
+
Write-Host $Message
|
| 42 |
+
}
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
# Data structures
|
| 46 |
+
class ConfigInfo {
|
| 47 |
+
[string]$ConstructorName # NewDatabaseConfig
|
| 48 |
+
[string]$Domain # DatabaseConfig
|
| 49 |
+
[string]$VarName # databaseConfig
|
| 50 |
+
[System.Collections.Generic.List[Parameter]]$Parameters
|
| 51 |
+
[System.Collections.Generic.List[string]]$ConfigDependencies
|
| 52 |
+
|
| 53 |
+
ConfigInfo() {
|
| 54 |
+
$this.Parameters = [System.Collections.Generic.List[Parameter]]::new()
|
| 55 |
+
$this.ConfigDependencies = [System.Collections.Generic.List[string]]::new()
|
| 56 |
+
}
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
class Parameter {
|
| 60 |
+
[string]$Name
|
| 61 |
+
[string]$RawType
|
| 62 |
+
[string]$NormalizedType
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
function Get-LowerCamelCase {
|
| 66 |
+
param([string]$Text)
|
| 67 |
+
if ($Text.Length -eq 0) { return $Text }
|
| 68 |
+
return $Text.Substring(0, 1).ToLower() + $Text.Substring(1)
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
function Normalize-TypeName {
|
| 72 |
+
param([string]$TypeStr)
|
| 73 |
+
|
| 74 |
+
# Remove leading pointer
|
| 75 |
+
$cleaned = $TypeStr -replace '^\*+', ''
|
| 76 |
+
|
| 77 |
+
# Remove package prefix (everything before last dot)
|
| 78 |
+
if ($cleaned -match '\.([^.]+)$') {
|
| 79 |
+
$cleaned = $matches[1]
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
return $cleaned.Trim()
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
function Parse-GoFiles {
|
| 86 |
+
param([string]$Directory)
|
| 87 |
+
|
| 88 |
+
Write-ColorOutput "Scanning for config constructors in $Directory..." "Cyan"
|
| 89 |
+
|
| 90 |
+
if (-not (Test-Path $Directory)) {
|
| 91 |
+
Write-ColorOutput "ERROR: Directory '$Directory' not found!" "Red"
|
| 92 |
+
exit 1
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
$goFiles = Get-ChildItem -Path $Directory -Filter "*.go" -Recurse -File
|
| 96 |
+
$configs = [System.Collections.Generic.List[ConfigInfo]]::new()
|
| 97 |
+
|
| 98 |
+
foreach ($file in $goFiles) {
|
| 99 |
+
$content = Get-Content $file.FullName -Raw
|
| 100 |
+
|
| 101 |
+
# Match function signatures (support multi-line)
|
| 102 |
+
# Pattern: func NewXxxConfig(...) XxxConfig
|
| 103 |
+
$pattern = '(?ms)func\s+(New[a-zA-Z0-9]+Config)\s*\(([^)]*)\)\s+([a-zA-Z0-9*_.]+Config)'
|
| 104 |
+
$matches = [regex]::Matches($content, $pattern)
|
| 105 |
+
|
| 106 |
+
foreach ($match in $matches) {
|
| 107 |
+
$constructorName = $match.Groups[1].Value
|
| 108 |
+
$paramsStr = $match.Groups[2].Value
|
| 109 |
+
$returnType = $match.Groups[3].Value
|
| 110 |
+
|
| 111 |
+
# Extract domain name (XxxConfig)
|
| 112 |
+
$domain = Normalize-TypeName $returnType
|
| 113 |
+
$varName = Get-LowerCamelCase $domain
|
| 114 |
+
|
| 115 |
+
$config = [ConfigInfo]::new()
|
| 116 |
+
$config.ConstructorName = $constructorName
|
| 117 |
+
$config.Domain = $domain
|
| 118 |
+
$config.VarName = $varName
|
| 119 |
+
|
| 120 |
+
# Parse parameters
|
| 121 |
+
if ($paramsStr.Trim() -ne "") {
|
| 122 |
+
# Split by comma, but be careful with nested types
|
| 123 |
+
$paramList = $paramsStr -split ',\s*(?![^<>]*>)'
|
| 124 |
+
|
| 125 |
+
foreach ($param in $paramList) {
|
| 126 |
+
$param = $param.Trim()
|
| 127 |
+
if ($param -eq "") { continue }
|
| 128 |
+
|
| 129 |
+
# Split into name and type
|
| 130 |
+
$parts = $param -split '\s+', 2
|
| 131 |
+
|
| 132 |
+
$p = [Parameter]::new()
|
| 133 |
+
if ($parts.Count -eq 2) {
|
| 134 |
+
$p.Name = $parts[0]
|
| 135 |
+
$p.RawType = $parts[1]
|
| 136 |
+
} elseif ($parts.Count -eq 1) {
|
| 137 |
+
# Anonymous parameter - synthesize name
|
| 138 |
+
$p.Name = "param$($config.Parameters.Count)"
|
| 139 |
+
$p.RawType = $parts[0]
|
| 140 |
+
} else {
|
| 141 |
+
continue
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
$p.NormalizedType = Normalize-TypeName $p.RawType
|
| 145 |
+
$config.Parameters.Add($p)
|
| 146 |
+
|
| 147 |
+
# Track config dependencies (configs that depend on other configs)
|
| 148 |
+
if ($p.NormalizedType -match 'Config$') {
|
| 149 |
+
$config.ConfigDependencies.Add($p.NormalizedType)
|
| 150 |
+
}
|
| 151 |
+
}
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
$configs.Add($config)
|
| 155 |
+
Write-ColorOutput " Found: $constructorName" "Green"
|
| 156 |
+
}
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
if ($configs.Count -eq 0) {
|
| 160 |
+
Write-ColorOutput "No config constructors found matching pattern 'NewXxxConfig'!" "Red"
|
| 161 |
+
exit 1
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
Write-ColorOutput "`nTotal configs discovered: $($configs.Count)" "Blue"
|
| 165 |
+
return $configs
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
function Get-TopologicalOrder {
|
| 169 |
+
param([System.Collections.Generic.List[ConfigInfo]]$Configs)
|
| 170 |
+
|
| 171 |
+
Write-ColorOutput "`nBuilding dependency graph..." "Cyan"
|
| 172 |
+
|
| 173 |
+
# Build adjacency list
|
| 174 |
+
$graph = @{}
|
| 175 |
+
$inDegree = @{}
|
| 176 |
+
$domainToConfig = @{}
|
| 177 |
+
|
| 178 |
+
foreach ($cfg in $Configs) {
|
| 179 |
+
$graph[$cfg.Domain] = [System.Collections.Generic.List[string]]::new()
|
| 180 |
+
$inDegree[$cfg.Domain] = 0
|
| 181 |
+
$domainToConfig[$cfg.Domain] = $cfg
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
# Build edges (dependencies)
|
| 185 |
+
foreach ($cfg in $Configs) {
|
| 186 |
+
foreach ($dep in $cfg.ConfigDependencies) {
|
| 187 |
+
if ($graph.ContainsKey($dep)) {
|
| 188 |
+
$graph[$dep].Add($cfg.Domain)
|
| 189 |
+
$inDegree[$cfg.Domain]++
|
| 190 |
+
}
|
| 191 |
+
}
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
# Kahn's algorithm for topological sort
|
| 195 |
+
$queue = [System.Collections.Generic.Queue[string]]::new()
|
| 196 |
+
foreach ($domain in $inDegree.Keys) {
|
| 197 |
+
if ($inDegree[$domain] -eq 0) {
|
| 198 |
+
$queue.Enqueue($domain)
|
| 199 |
+
}
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
$sorted = [System.Collections.Generic.List[string]]::new()
|
| 203 |
+
|
| 204 |
+
while ($queue.Count -gt 0) {
|
| 205 |
+
$current = $queue.Dequeue()
|
| 206 |
+
$sorted.Add($current)
|
| 207 |
+
|
| 208 |
+
foreach ($neighbor in $graph[$current]) {
|
| 209 |
+
$inDegree[$neighbor]--
|
| 210 |
+
if ($inDegree[$neighbor] -eq 0) {
|
| 211 |
+
$queue.Enqueue($neighbor)
|
| 212 |
+
}
|
| 213 |
+
}
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
# Check for cycles
|
| 217 |
+
if ($sorted.Count -ne $Configs.Count) {
|
| 218 |
+
$remaining = $inDegree.Keys | Where-Object { $inDegree[$_] -gt 0 }
|
| 219 |
+
Write-ColorOutput "`nERROR: Circular dependency detected in configs!" "Red"
|
| 220 |
+
Write-ColorOutput "Configs involved in cycle: $($remaining -join ', ')" "Yellow"
|
| 221 |
+
exit 1
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
Write-ColorOutput " Dependency graph validated (no cycles)" "Green"
|
| 225 |
+
Write-ColorOutput " Topological order: $($sorted -join ' -> ')" "Blue"
|
| 226 |
+
|
| 227 |
+
# Return configs in topological order
|
| 228 |
+
return $sorted | ForEach-Object { $domainToConfig[$_] }
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
function Resolve-ConfigArgument {
|
| 232 |
+
param(
|
| 233 |
+
[Parameter]$Param,
|
| 234 |
+
[string]$DependentConfigVar
|
| 235 |
+
)
|
| 236 |
+
|
| 237 |
+
$type = $Param.NormalizedType
|
| 238 |
+
$paramName = $Param.Name
|
| 239 |
+
|
| 240 |
+
# SPECIAL CASE MAPPINGS FOR CONFIG
|
| 241 |
+
# ============================================
|
| 242 |
+
|
| 243 |
+
# 1. Config pattern: XxxConfig -> already instantiated config variable
|
| 244 |
+
if ($type -match '^(.+)Config$') {
|
| 245 |
+
$configVarName = Get-LowerCamelCase $type
|
| 246 |
+
return $configVarName
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
# 2. String parameters - try to infer from parameter name and match with config getter methods
|
| 250 |
+
if ($type -eq "string") {
|
| 251 |
+
# Common patterns for EnvConfig getters
|
| 252 |
+
$getterMappings = @{
|
| 253 |
+
"host" = "GetDatabaseHost()"
|
| 254 |
+
"databaseHost" = "GetDatabaseHost()"
|
| 255 |
+
"user" = "GetDatabaseUser()"
|
| 256 |
+
"databaseUser" = "GetDatabaseUser()"
|
| 257 |
+
"password" = "GetDatabasePassword()"
|
| 258 |
+
"databasePassword" = "GetDatabasePassword()"
|
| 259 |
+
"name" = "GetDatabaseName()"
|
| 260 |
+
"databaseName" = "GetDatabaseName()"
|
| 261 |
+
"dbName" = "GetDatabaseName()"
|
| 262 |
+
"port" = "GetDatabasePort()"
|
| 263 |
+
"databasePort" = "GetDatabasePort()"
|
| 264 |
+
"salt" = "GetSalt()"
|
| 265 |
+
"secret" = "GetSecretKey()"
|
| 266 |
+
"secretKey" = "GetSecretKey()"
|
| 267 |
+
"jwtSecret" = "GetSecretKey()"
|
| 268 |
+
"apiKey" = "GetAPIKey()"
|
| 269 |
+
"timezone" = "GetTimezone()"
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
# Try to find matching getter
|
| 273 |
+
foreach ($key in $getterMappings.Keys) {
|
| 274 |
+
if ($paramName -like "*$key*") {
|
| 275 |
+
return "envConfig.$($getterMappings[$key])"
|
| 276 |
+
}
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
# If dependent on a config, try to construct getter name from param name
|
| 280 |
+
if ($DependentConfigVar) {
|
| 281 |
+
# Convert paramName to PascalCase for getter
|
| 282 |
+
$getterName = (Get-Culture).TextInfo.ToTitleCase($paramName)
|
| 283 |
+
$getterName = $getterName -replace '\s', ''
|
| 284 |
+
return "${DependentConfigVar}.Get${getterName}()"
|
| 285 |
+
}
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
# 3. Int/port parameters
|
| 289 |
+
if ($type -eq "int" -or $type -eq "int32" -or $type -eq "int64") {
|
| 290 |
+
if ($paramName -match "port") {
|
| 291 |
+
return "envConfig.GetDatabasePort()"
|
| 292 |
+
}
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
# ADD MORE SPECIAL CASES HERE:
|
| 296 |
+
# --------------------------------------------
|
| 297 |
+
# Example: Redis config
|
| 298 |
+
# if ($paramName -match "redis") {
|
| 299 |
+
# return "envConfig.GetRedisURL()"
|
| 300 |
+
# }
|
| 301 |
+
#
|
| 302 |
+
# Example: Mail config
|
| 303 |
+
# if ($paramName -match "smtp") {
|
| 304 |
+
# return "envConfig.GetSMTPHost()"
|
| 305 |
+
# }
|
| 306 |
+
# --------------------------------------------
|
| 307 |
+
|
| 308 |
+
# 4. Hardcoded constants (timezone example)
|
| 309 |
+
if ($type -eq "string" -and $paramName -match "timezone|location") {
|
| 310 |
+
return "`"Asia/Jakarta`""
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
# 5. Fallback: unresolved type
|
| 314 |
+
return "/* TODO: provide $($Param.RawType) for $paramName */"
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
function Generate-ProviderCode {
|
| 318 |
+
param([System.Collections.Generic.List[ConfigInfo]]$ConfigsInOrder)
|
| 319 |
+
|
| 320 |
+
Write-ColorOutput "`nGenerating config provider code..." "Cyan"
|
| 321 |
+
|
| 322 |
+
$sb = [System.Text.StringBuilder]::new()
|
| 323 |
+
[void]$sb.AppendLine("package provider")
|
| 324 |
+
[void]$sb.AppendLine()
|
| 325 |
+
[void]$sb.AppendLine("import `"$ModulePath`"")
|
| 326 |
+
[void]$sb.AppendLine()
|
| 327 |
+
|
| 328 |
+
# Interface
|
| 329 |
+
[void]$sb.AppendLine("type ConfigProvider interface {")
|
| 330 |
+
foreach ($cfg in $ConfigsInOrder) {
|
| 331 |
+
$line = "`tProvide$($cfg.Domain)() config.$($cfg.Domain)"
|
| 332 |
+
[void]$sb.AppendLine($line)
|
| 333 |
+
}
|
| 334 |
+
[void]$sb.AppendLine("}")
|
| 335 |
+
[void]$sb.AppendLine()
|
| 336 |
+
|
| 337 |
+
# Struct
|
| 338 |
+
[void]$sb.AppendLine("type configProvider struct {")
|
| 339 |
+
foreach ($cfg in $ConfigsInOrder) {
|
| 340 |
+
$line = "`t$($cfg.VarName) config.$($cfg.Domain)"
|
| 341 |
+
[void]$sb.AppendLine($line)
|
| 342 |
+
}
|
| 343 |
+
[void]$sb.AppendLine("}")
|
| 344 |
+
[void]$sb.AppendLine()
|
| 345 |
+
|
| 346 |
+
# Constructor
|
| 347 |
+
[void]$sb.AppendLine("func NewConfigProvider() ConfigProvider {")
|
| 348 |
+
|
| 349 |
+
# Initialize configs in topological order
|
| 350 |
+
foreach ($cfg in $ConfigsInOrder) {
|
| 351 |
+
# Check if this config depends on another config
|
| 352 |
+
$dependentConfigVar = $null
|
| 353 |
+
if ($cfg.ConfigDependencies.Count -gt 0) {
|
| 354 |
+
$dependentConfigVar = Get-LowerCamelCase $cfg.ConfigDependencies[0]
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
$args = @()
|
| 358 |
+
foreach ($param in $cfg.Parameters) {
|
| 359 |
+
$args += Resolve-ConfigArgument -Param $param -DependentConfigVar $dependentConfigVar
|
| 360 |
+
}
|
| 361 |
+
$argsStr = $args -join ", "
|
| 362 |
+
$line = "`t$($cfg.VarName) := config.$($cfg.ConstructorName)($argsStr)"
|
| 363 |
+
[void]$sb.AppendLine($line)
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
[void]$sb.AppendLine("`treturn &configProvider{")
|
| 367 |
+
foreach ($cfg in $ConfigsInOrder) {
|
| 368 |
+
$line = "`t`t$($cfg.VarName): $($cfg.VarName),"
|
| 369 |
+
[void]$sb.AppendLine($line)
|
| 370 |
+
}
|
| 371 |
+
[void]$sb.AppendLine("`t}")
|
| 372 |
+
[void]$sb.AppendLine("}")
|
| 373 |
+
[void]$sb.AppendLine()
|
| 374 |
+
|
| 375 |
+
# Getter methods
|
| 376 |
+
foreach ($cfg in $ConfigsInOrder) {
|
| 377 |
+
[void]$sb.AppendLine("func (c *configProvider) Provide$($cfg.Domain)() config.$($cfg.Domain) {")
|
| 378 |
+
[void]$sb.AppendLine("`treturn c.$($cfg.VarName)")
|
| 379 |
+
[void]$sb.AppendLine("}")
|
| 380 |
+
[void]$sb.AppendLine()
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
return $sb.ToString()
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
function Write-ProviderFile {
|
| 387 |
+
param([string]$Code, [string]$OutputPath)
|
| 388 |
+
|
| 389 |
+
Write-ColorOutput "Writing to $OutputPath..." "Cyan"
|
| 390 |
+
|
| 391 |
+
# Ensure directory exists
|
| 392 |
+
$dir = Split-Path $OutputPath -Parent
|
| 393 |
+
if ($dir -and -not (Test-Path $dir)) {
|
| 394 |
+
New-Item -ItemType Directory -Path $dir -Force | Out-Null
|
| 395 |
+
}
|
| 396 |
+
|
| 397 |
+
# Write file as UTF-8 without BOM
|
| 398 |
+
$utf8NoBom = [System.Text.UTF8Encoding]::new($false)
|
| 399 |
+
[System.IO.File]::WriteAllText($OutputPath, $Code, $utf8NoBom)
|
| 400 |
+
|
| 401 |
+
Write-ColorOutput " Successfully generated $OutputPath" "Green"
|
| 402 |
+
}
|
| 403 |
+
|
| 404 |
+
# ============================================
|
| 405 |
+
# MAIN EXECUTION
|
| 406 |
+
# ============================================
|
| 407 |
+
|
| 408 |
+
try {
|
| 409 |
+
Write-ColorOutput "`n=========================================" "Blue"
|
| 410 |
+
Write-ColorOutput " Go Config Provider Generator v1.0" "Blue"
|
| 411 |
+
Write-ColorOutput "=========================================`n" "Blue"
|
| 412 |
+
|
| 413 |
+
# Step 1: Parse all config constructors
|
| 414 |
+
$configs = Parse-GoFiles -Directory $ConfigDir
|
| 415 |
+
|
| 416 |
+
# Step 2: Perform topological sort (configs can depend on other configs)
|
| 417 |
+
$sortedConfigs = Get-TopologicalOrder -Configs $configs
|
| 418 |
+
|
| 419 |
+
# Step 3: Generate provider code
|
| 420 |
+
$code = Generate-ProviderCode -ConfigsInOrder $sortedConfigs
|
| 421 |
+
|
| 422 |
+
# Step 4: Write to file
|
| 423 |
+
Write-ProviderFile -Code $code -OutputPath $OutputFile
|
| 424 |
+
|
| 425 |
+
Write-ColorOutput "`nSUCCESS! Config provider generated successfully.`n" "Green"
|
| 426 |
+
Write-ColorOutput "Next steps:" "Cyan"
|
| 427 |
+
Write-ColorOutput " 1. Review $OutputFile" "White"
|
| 428 |
+
Write-ColorOutput " 2. Fill any /* TODO: provide ... */ placeholders" "White"
|
| 429 |
+
Write-ColorOutput " 3. Run: go build ./provider" "White"
|
| 430 |
+
|
| 431 |
+
} catch {
|
| 432 |
+
Write-ColorOutput "`nERROR: $($_.Exception.Message)" "Red"
|
| 433 |
+
Write-ColorOutput "Stack trace: $($_.ScriptStackTrace)" "Yellow"
|
| 434 |
+
exit 1
|
| 435 |
}
|
do_inject_controllers.ps1
CHANGED
|
@@ -1,310 +1,310 @@
|
|
| 1 |
-
#Requires -Version 5.1
|
| 2 |
-
<#
|
| 3 |
-
.SYNOPSIS
|
| 4 |
-
Automatic Dependency Injection Generator for Go Controllers
|
| 5 |
-
|
| 6 |
-
.DESCRIPTION
|
| 7 |
-
Scans ./controllers/ directory, discovers all controller constructors, infers their dependencies,
|
| 8 |
-
and generates provider/controller_provider.go with full DI wiring.
|
| 9 |
-
|
| 10 |
-
.EXAMPLE
|
| 11 |
-
.\controller_injector.ps1
|
| 12 |
-
|
| 13 |
-
.NOTES
|
| 14 |
-
- Works with PowerShell 5.1+ and PowerShell 7+
|
| 15 |
-
- No external dependencies required
|
| 16 |
-
- Supports multi-line constructor signatures
|
| 17 |
-
- Controllers depend on services from ServicesProvider
|
| 18 |
-
#>
|
| 19 |
-
|
| 20 |
-
[CmdletBinding()]
|
| 21 |
-
param()
|
| 22 |
-
|
| 23 |
-
# Configuration
|
| 24 |
-
$ControllersDir = "./controllers"
|
| 25 |
-
$OutputFile = "provider/controller_provider.go"
|
| 26 |
-
$ModulePath = "abdanhafidz.com/go-boilerplate/controllers"
|
| 27 |
-
|
| 28 |
-
# ANSI colors for better output
|
| 29 |
-
$script:UseColors = $Host.UI.SupportsVirtualTerminal
|
| 30 |
-
function Write-ColorOutput {
|
| 31 |
-
param([string]$Message, [string]$Color = "White")
|
| 32 |
-
if ($script:UseColors) {
|
| 33 |
-
$colors = @{
|
| 34 |
-
"Green" = "`e[32m"; "Yellow" = "`e[33m"; "Red" = "`e[31m"
|
| 35 |
-
"Cyan" = "`e[36m"; "Blue" = "`e[34m"; "Reset" = "`e[0m"
|
| 36 |
-
}
|
| 37 |
-
Write-Host "$($colors[$Color])$Message$($colors['Reset'])"
|
| 38 |
-
} else {
|
| 39 |
-
Write-Host $Message
|
| 40 |
-
}
|
| 41 |
-
}
|
| 42 |
-
|
| 43 |
-
# Data structures
|
| 44 |
-
class ControllerInfo {
|
| 45 |
-
[string]$ConstructorName # NewAccountController
|
| 46 |
-
[string]$Domain # AccountController
|
| 47 |
-
[string]$VarName # accountController
|
| 48 |
-
[System.Collections.Generic.List[Parameter]]$Parameters
|
| 49 |
-
|
| 50 |
-
ControllerInfo() {
|
| 51 |
-
$this.Parameters = [System.Collections.Generic.List[Parameter]]::new()
|
| 52 |
-
}
|
| 53 |
-
}
|
| 54 |
-
|
| 55 |
-
class Parameter {
|
| 56 |
-
[string]$Name
|
| 57 |
-
[string]$RawType
|
| 58 |
-
[string]$NormalizedType
|
| 59 |
-
}
|
| 60 |
-
|
| 61 |
-
function Get-LowerCamelCase {
|
| 62 |
-
param([string]$Text)
|
| 63 |
-
if ($Text.Length -eq 0) { return $Text }
|
| 64 |
-
return $Text.Substring(0, 1).ToLower() + $Text.Substring(1)
|
| 65 |
-
}
|
| 66 |
-
|
| 67 |
-
function Normalize-TypeName {
|
| 68 |
-
param([string]$TypeStr)
|
| 69 |
-
|
| 70 |
-
# Remove leading pointer
|
| 71 |
-
$cleaned = $TypeStr -replace '^\*+', ''
|
| 72 |
-
|
| 73 |
-
# Remove package prefix (everything before last dot)
|
| 74 |
-
if ($cleaned -match '\.([^.]+)$') {
|
| 75 |
-
$cleaned = $matches[1]
|
| 76 |
-
}
|
| 77 |
-
|
| 78 |
-
return $cleaned.Trim()
|
| 79 |
-
}
|
| 80 |
-
|
| 81 |
-
function Parse-GoFiles {
|
| 82 |
-
param([string]$Directory)
|
| 83 |
-
|
| 84 |
-
Write-ColorOutput "Scanning for controller constructors in $Directory..." "Cyan"
|
| 85 |
-
|
| 86 |
-
if (-not (Test-Path $Directory)) {
|
| 87 |
-
Write-ColorOutput "ERROR: Directory '$Directory' not found!" "Red"
|
| 88 |
-
exit 1
|
| 89 |
-
}
|
| 90 |
-
|
| 91 |
-
$goFiles = Get-ChildItem -Path $Directory -Filter "*.go" -Recurse -File
|
| 92 |
-
$controllers = [System.Collections.Generic.List[ControllerInfo]]::new()
|
| 93 |
-
|
| 94 |
-
foreach ($file in $goFiles) {
|
| 95 |
-
$content = Get-Content $file.FullName -Raw
|
| 96 |
-
|
| 97 |
-
# Match function signatures (support multi-line)
|
| 98 |
-
# Pattern: func NewXxxController(...) XxxController
|
| 99 |
-
$pattern = '(?ms)func\s+(New[a-zA-Z0-9]+Controller)\s*\(([^)]*)\)\s+([a-zA-Z0-9*_.]+Controller)'
|
| 100 |
-
$matches = [regex]::Matches($content, $pattern)
|
| 101 |
-
|
| 102 |
-
foreach ($match in $matches) {
|
| 103 |
-
$constructorName = $match.Groups[1].Value
|
| 104 |
-
$paramsStr = $match.Groups[2].Value
|
| 105 |
-
$returnType = $match.Groups[3].Value
|
| 106 |
-
|
| 107 |
-
# Extract domain name (XxxController)
|
| 108 |
-
$domain = Normalize-TypeName $returnType
|
| 109 |
-
$varName = Get-LowerCamelCase $domain
|
| 110 |
-
|
| 111 |
-
$controller = [ControllerInfo]::new()
|
| 112 |
-
$controller.ConstructorName = $constructorName
|
| 113 |
-
$controller.Domain = $domain
|
| 114 |
-
$controller.VarName = $varName
|
| 115 |
-
|
| 116 |
-
# Parse parameters
|
| 117 |
-
if ($paramsStr.Trim() -ne "") {
|
| 118 |
-
# Split by comma, but be careful with nested types
|
| 119 |
-
$paramList = $paramsStr -split ',\s*(?![^<>]*>)'
|
| 120 |
-
|
| 121 |
-
foreach ($param in $paramList) {
|
| 122 |
-
$param = $param.Trim()
|
| 123 |
-
if ($param -eq "") { continue }
|
| 124 |
-
|
| 125 |
-
# Split into name and type
|
| 126 |
-
$parts = $param -split '\s+', 2
|
| 127 |
-
|
| 128 |
-
$p = [Parameter]::new()
|
| 129 |
-
if ($parts.Count -eq 2) {
|
| 130 |
-
$p.Name = $parts[0]
|
| 131 |
-
$p.RawType = $parts[1]
|
| 132 |
-
} elseif ($parts.Count -eq 1) {
|
| 133 |
-
# Anonymous parameter - synthesize name
|
| 134 |
-
$p.Name = "param$($controller.Parameters.Count)"
|
| 135 |
-
$p.RawType = $parts[0]
|
| 136 |
-
} else {
|
| 137 |
-
continue
|
| 138 |
-
}
|
| 139 |
-
|
| 140 |
-
$p.NormalizedType = Normalize-TypeName $p.RawType
|
| 141 |
-
$controller.Parameters.Add($p)
|
| 142 |
-
}
|
| 143 |
-
}
|
| 144 |
-
|
| 145 |
-
$controllers.Add($controller)
|
| 146 |
-
Write-ColorOutput " Found: $constructorName" "Green"
|
| 147 |
-
}
|
| 148 |
-
}
|
| 149 |
-
|
| 150 |
-
if ($controllers.Count -eq 0) {
|
| 151 |
-
Write-ColorOutput "No controller constructors found matching pattern 'NewXxxController'!" "Red"
|
| 152 |
-
exit 1
|
| 153 |
-
}
|
| 154 |
-
|
| 155 |
-
Write-ColorOutput "`nTotal controllers discovered: $($controllers.Count)" "Blue"
|
| 156 |
-
return $controllers
|
| 157 |
-
}
|
| 158 |
-
|
| 159 |
-
function Resolve-ControllerArgument {
|
| 160 |
-
param([Parameter]$Param)
|
| 161 |
-
|
| 162 |
-
$type = $Param.NormalizedType
|
| 163 |
-
|
| 164 |
-
# DEPENDENCY RESOLUTION RULES
|
| 165 |
-
# ============================================
|
| 166 |
-
|
| 167 |
-
# 1. Service pattern: XxxxService -> servicesProvider.ProvideXxxxService()
|
| 168 |
-
if ($type -match '^(.+)Service$') {
|
| 169 |
-
$serviceName = $type
|
| 170 |
-
return "servicesProvider.Provide${serviceName}()"
|
| 171 |
-
}
|
| 172 |
-
|
| 173 |
-
# ADD MORE SPECIAL CASES HERE:
|
| 174 |
-
# --------------------------------------------
|
| 175 |
-
# Example: Config dependency
|
| 176 |
-
# if ($type -eq "Config") {
|
| 177 |
-
# return "configProvider.ProvideConfig()"
|
| 178 |
-
# }
|
| 179 |
-
#
|
| 180 |
-
# Example: Logger
|
| 181 |
-
# if ($type -eq "Logger") {
|
| 182 |
-
# return "loggerProvider.ProvideLogger()"
|
| 183 |
-
# }
|
| 184 |
-
#
|
| 185 |
-
# Example: Validator
|
| 186 |
-
# if ($type -eq "Validator") {
|
| 187 |
-
# return "validatorProvider.ProvideValidator()"
|
| 188 |
-
# }
|
| 189 |
-
# --------------------------------------------
|
| 190 |
-
|
| 191 |
-
# 2. Fallback: unresolved type
|
| 192 |
-
return "/* TODO: provide $($Param.RawType) */"
|
| 193 |
-
}
|
| 194 |
-
|
| 195 |
-
function Generate-ProviderCode {
|
| 196 |
-
param([System.Collections.Generic.List[ControllerInfo]]$Controllers)
|
| 197 |
-
|
| 198 |
-
Write-ColorOutput "`nGenerating controller provider code..." "Cyan"
|
| 199 |
-
|
| 200 |
-
# Sort controllers alphabetically for consistent output
|
| 201 |
-
$sortedControllers = $Controllers | Sort-Object -Property Domain
|
| 202 |
-
|
| 203 |
-
$sb = [System.Text.StringBuilder]::new()
|
| 204 |
-
[void]$sb.AppendLine("package provider")
|
| 205 |
-
[void]$sb.AppendLine()
|
| 206 |
-
[void]$sb.AppendLine("import `"$ModulePath`"")
|
| 207 |
-
[void]$sb.AppendLine()
|
| 208 |
-
|
| 209 |
-
# Interface
|
| 210 |
-
[void]$sb.AppendLine("type ControllerProvider interface {")
|
| 211 |
-
foreach ($ctrl in $sortedControllers) {
|
| 212 |
-
$line = "`tProvide$($ctrl.Domain)() controllers.$($ctrl.Domain)"
|
| 213 |
-
[void]$sb.AppendLine($line)
|
| 214 |
-
}
|
| 215 |
-
[void]$sb.AppendLine("}")
|
| 216 |
-
[void]$sb.AppendLine()
|
| 217 |
-
|
| 218 |
-
# Struct
|
| 219 |
-
[void]$sb.AppendLine("type controllerProvider struct {")
|
| 220 |
-
foreach ($ctrl in $sortedControllers) {
|
| 221 |
-
$line = "`t$($ctrl.VarName) controllers.$($ctrl.Domain)"
|
| 222 |
-
[void]$sb.AppendLine($line)
|
| 223 |
-
}
|
| 224 |
-
[void]$sb.AppendLine("}")
|
| 225 |
-
[void]$sb.AppendLine()
|
| 226 |
-
|
| 227 |
-
# Constructor
|
| 228 |
-
[void]$sb.AppendLine("func NewControllerProvider(servicesProvider ServicesProvider) ControllerProvider {")
|
| 229 |
-
[void]$sb.AppendLine()
|
| 230 |
-
|
| 231 |
-
# Initialize controllers
|
| 232 |
-
foreach ($ctrl in $sortedControllers) {
|
| 233 |
-
$args = @()
|
| 234 |
-
foreach ($param in $ctrl.Parameters) {
|
| 235 |
-
$args += Resolve-ControllerArgument $param
|
| 236 |
-
}
|
| 237 |
-
$argsStr = $args -join ", "
|
| 238 |
-
$line = "`t$($ctrl.VarName) := controllers.$($ctrl.ConstructorName)($argsStr)"
|
| 239 |
-
[void]$sb.AppendLine($line)
|
| 240 |
-
}
|
| 241 |
-
|
| 242 |
-
[void]$sb.AppendLine("`treturn &controllerProvider{")
|
| 243 |
-
foreach ($ctrl in $sortedControllers) {
|
| 244 |
-
$line = "`t`t$($ctrl.VarName): $($ctrl.VarName),"
|
| 245 |
-
[void]$sb.AppendLine($line)
|
| 246 |
-
}
|
| 247 |
-
[void]$sb.AppendLine("`t}")
|
| 248 |
-
[void]$sb.AppendLine("}")
|
| 249 |
-
[void]$sb.AppendLine()
|
| 250 |
-
|
| 251 |
-
# Getter methods
|
| 252 |
-
[void]$sb.AppendLine("// --- Getter Methods ---")
|
| 253 |
-
[void]$sb.AppendLine()
|
| 254 |
-
foreach ($ctrl in $sortedControllers) {
|
| 255 |
-
[void]$sb.AppendLine("func (c *controllerProvider) Provide$($ctrl.Domain)() controllers.$($ctrl.Domain) {")
|
| 256 |
-
[void]$sb.AppendLine("`treturn c.$($ctrl.VarName)")
|
| 257 |
-
[void]$sb.AppendLine("}")
|
| 258 |
-
[void]$sb.AppendLine()
|
| 259 |
-
}
|
| 260 |
-
|
| 261 |
-
return $sb.ToString()
|
| 262 |
-
}
|
| 263 |
-
|
| 264 |
-
function Write-ProviderFile {
|
| 265 |
-
param([string]$Code, [string]$OutputPath)
|
| 266 |
-
|
| 267 |
-
Write-ColorOutput "Writing to $OutputPath..." "Cyan"
|
| 268 |
-
|
| 269 |
-
# Ensure directory exists
|
| 270 |
-
$dir = Split-Path $OutputPath -Parent
|
| 271 |
-
if ($dir -and -not (Test-Path $dir)) {
|
| 272 |
-
New-Item -ItemType Directory -Path $dir -Force | Out-Null
|
| 273 |
-
}
|
| 274 |
-
|
| 275 |
-
# Write file as UTF-8 without BOM
|
| 276 |
-
$utf8NoBom = [System.Text.UTF8Encoding]::new($false)
|
| 277 |
-
[System.IO.File]::WriteAllText($OutputPath, $Code, $utf8NoBom)
|
| 278 |
-
|
| 279 |
-
Write-ColorOutput " Successfully generated $OutputPath" "Green"
|
| 280 |
-
}
|
| 281 |
-
|
| 282 |
-
# ============================================
|
| 283 |
-
# MAIN EXECUTION
|
| 284 |
-
# ============================================
|
| 285 |
-
|
| 286 |
-
try {
|
| 287 |
-
Write-ColorOutput "`n=========================================" "Blue"
|
| 288 |
-
Write-ColorOutput " Go Controller Provider Generator v1.0" "Blue"
|
| 289 |
-
Write-ColorOutput "=========================================`n" "Blue"
|
| 290 |
-
|
| 291 |
-
# Step 1: Parse all controller constructors
|
| 292 |
-
$controllers = Parse-GoFiles -Directory $ControllersDir
|
| 293 |
-
|
| 294 |
-
# Step 2: Generate provider code
|
| 295 |
-
$code = Generate-ProviderCode -Controllers $controllers
|
| 296 |
-
|
| 297 |
-
# Step 3: Write to file
|
| 298 |
-
Write-ProviderFile -Code $code -OutputPath $OutputFile
|
| 299 |
-
|
| 300 |
-
Write-ColorOutput "`nSUCCESS! Controller provider generated successfully.`n" "Green"
|
| 301 |
-
Write-ColorOutput "Next steps:" "Cyan"
|
| 302 |
-
Write-ColorOutput " 1. Review $OutputFile" "White"
|
| 303 |
-
Write-ColorOutput " 2. Fill any /* TODO: provide ... */ placeholders" "White"
|
| 304 |
-
Write-ColorOutput " 3. Run: go build ./provider" "White"
|
| 305 |
-
|
| 306 |
-
} catch {
|
| 307 |
-
Write-ColorOutput "`nERROR: $($_.Exception.Message)" "Red"
|
| 308 |
-
Write-ColorOutput "Stack trace: $($_.ScriptStackTrace)" "Yellow"
|
| 309 |
-
exit 1
|
| 310 |
}
|
|
|
|
| 1 |
+
#Requires -Version 5.1
|
| 2 |
+
<#
|
| 3 |
+
.SYNOPSIS
|
| 4 |
+
Automatic Dependency Injection Generator for Go Controllers
|
| 5 |
+
|
| 6 |
+
.DESCRIPTION
|
| 7 |
+
Scans ./controllers/ directory, discovers all controller constructors, infers their dependencies,
|
| 8 |
+
and generates provider/controller_provider.go with full DI wiring.
|
| 9 |
+
|
| 10 |
+
.EXAMPLE
|
| 11 |
+
.\controller_injector.ps1
|
| 12 |
+
|
| 13 |
+
.NOTES
|
| 14 |
+
- Works with PowerShell 5.1+ and PowerShell 7+
|
| 15 |
+
- No external dependencies required
|
| 16 |
+
- Supports multi-line constructor signatures
|
| 17 |
+
- Controllers depend on services from ServicesProvider
|
| 18 |
+
#>
|
| 19 |
+
|
| 20 |
+
[CmdletBinding()]
|
| 21 |
+
param()
|
| 22 |
+
|
| 23 |
+
# Configuration
|
| 24 |
+
$ControllersDir = "./controllers"
|
| 25 |
+
$OutputFile = "provider/controller_provider.go"
|
| 26 |
+
$ModulePath = "abdanhafidz.com/go-boilerplate/controllers"
|
| 27 |
+
|
| 28 |
+
# ANSI colors for better output
|
| 29 |
+
$script:UseColors = $Host.UI.SupportsVirtualTerminal
|
| 30 |
+
function Write-ColorOutput {
|
| 31 |
+
param([string]$Message, [string]$Color = "White")
|
| 32 |
+
if ($script:UseColors) {
|
| 33 |
+
$colors = @{
|
| 34 |
+
"Green" = "`e[32m"; "Yellow" = "`e[33m"; "Red" = "`e[31m"
|
| 35 |
+
"Cyan" = "`e[36m"; "Blue" = "`e[34m"; "Reset" = "`e[0m"
|
| 36 |
+
}
|
| 37 |
+
Write-Host "$($colors[$Color])$Message$($colors['Reset'])"
|
| 38 |
+
} else {
|
| 39 |
+
Write-Host $Message
|
| 40 |
+
}
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
# Data structures
|
| 44 |
+
class ControllerInfo {
|
| 45 |
+
[string]$ConstructorName # NewAccountController
|
| 46 |
+
[string]$Domain # AccountController
|
| 47 |
+
[string]$VarName # accountController
|
| 48 |
+
[System.Collections.Generic.List[Parameter]]$Parameters
|
| 49 |
+
|
| 50 |
+
ControllerInfo() {
|
| 51 |
+
$this.Parameters = [System.Collections.Generic.List[Parameter]]::new()
|
| 52 |
+
}
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
class Parameter {
|
| 56 |
+
[string]$Name
|
| 57 |
+
[string]$RawType
|
| 58 |
+
[string]$NormalizedType
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
function Get-LowerCamelCase {
|
| 62 |
+
param([string]$Text)
|
| 63 |
+
if ($Text.Length -eq 0) { return $Text }
|
| 64 |
+
return $Text.Substring(0, 1).ToLower() + $Text.Substring(1)
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
function Normalize-TypeName {
|
| 68 |
+
param([string]$TypeStr)
|
| 69 |
+
|
| 70 |
+
# Remove leading pointer
|
| 71 |
+
$cleaned = $TypeStr -replace '^\*+', ''
|
| 72 |
+
|
| 73 |
+
# Remove package prefix (everything before last dot)
|
| 74 |
+
if ($cleaned -match '\.([^.]+)$') {
|
| 75 |
+
$cleaned = $matches[1]
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
return $cleaned.Trim()
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
function Parse-GoFiles {
|
| 82 |
+
param([string]$Directory)
|
| 83 |
+
|
| 84 |
+
Write-ColorOutput "Scanning for controller constructors in $Directory..." "Cyan"
|
| 85 |
+
|
| 86 |
+
if (-not (Test-Path $Directory)) {
|
| 87 |
+
Write-ColorOutput "ERROR: Directory '$Directory' not found!" "Red"
|
| 88 |
+
exit 1
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
$goFiles = Get-ChildItem -Path $Directory -Filter "*.go" -Recurse -File
|
| 92 |
+
$controllers = [System.Collections.Generic.List[ControllerInfo]]::new()
|
| 93 |
+
|
| 94 |
+
foreach ($file in $goFiles) {
|
| 95 |
+
$content = Get-Content $file.FullName -Raw
|
| 96 |
+
|
| 97 |
+
# Match function signatures (support multi-line)
|
| 98 |
+
# Pattern: func NewXxxController(...) XxxController
|
| 99 |
+
$pattern = '(?ms)func\s+(New[a-zA-Z0-9]+Controller)\s*\(([^)]*)\)\s+([a-zA-Z0-9*_.]+Controller)'
|
| 100 |
+
$matches = [regex]::Matches($content, $pattern)
|
| 101 |
+
|
| 102 |
+
foreach ($match in $matches) {
|
| 103 |
+
$constructorName = $match.Groups[1].Value
|
| 104 |
+
$paramsStr = $match.Groups[2].Value
|
| 105 |
+
$returnType = $match.Groups[3].Value
|
| 106 |
+
|
| 107 |
+
# Extract domain name (XxxController)
|
| 108 |
+
$domain = Normalize-TypeName $returnType
|
| 109 |
+
$varName = Get-LowerCamelCase $domain
|
| 110 |
+
|
| 111 |
+
$controller = [ControllerInfo]::new()
|
| 112 |
+
$controller.ConstructorName = $constructorName
|
| 113 |
+
$controller.Domain = $domain
|
| 114 |
+
$controller.VarName = $varName
|
| 115 |
+
|
| 116 |
+
# Parse parameters
|
| 117 |
+
if ($paramsStr.Trim() -ne "") {
|
| 118 |
+
# Split by comma, but be careful with nested types
|
| 119 |
+
$paramList = $paramsStr -split ',\s*(?![^<>]*>)'
|
| 120 |
+
|
| 121 |
+
foreach ($param in $paramList) {
|
| 122 |
+
$param = $param.Trim()
|
| 123 |
+
if ($param -eq "") { continue }
|
| 124 |
+
|
| 125 |
+
# Split into name and type
|
| 126 |
+
$parts = $param -split '\s+', 2
|
| 127 |
+
|
| 128 |
+
$p = [Parameter]::new()
|
| 129 |
+
if ($parts.Count -eq 2) {
|
| 130 |
+
$p.Name = $parts[0]
|
| 131 |
+
$p.RawType = $parts[1]
|
| 132 |
+
} elseif ($parts.Count -eq 1) {
|
| 133 |
+
# Anonymous parameter - synthesize name
|
| 134 |
+
$p.Name = "param$($controller.Parameters.Count)"
|
| 135 |
+
$p.RawType = $parts[0]
|
| 136 |
+
} else {
|
| 137 |
+
continue
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
$p.NormalizedType = Normalize-TypeName $p.RawType
|
| 141 |
+
$controller.Parameters.Add($p)
|
| 142 |
+
}
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
$controllers.Add($controller)
|
| 146 |
+
Write-ColorOutput " Found: $constructorName" "Green"
|
| 147 |
+
}
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
if ($controllers.Count -eq 0) {
|
| 151 |
+
Write-ColorOutput "No controller constructors found matching pattern 'NewXxxController'!" "Red"
|
| 152 |
+
exit 1
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
Write-ColorOutput "`nTotal controllers discovered: $($controllers.Count)" "Blue"
|
| 156 |
+
return $controllers
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
function Resolve-ControllerArgument {
|
| 160 |
+
param([Parameter]$Param)
|
| 161 |
+
|
| 162 |
+
$type = $Param.NormalizedType
|
| 163 |
+
|
| 164 |
+
# DEPENDENCY RESOLUTION RULES
|
| 165 |
+
# ============================================
|
| 166 |
+
|
| 167 |
+
# 1. Service pattern: XxxxService -> servicesProvider.ProvideXxxxService()
|
| 168 |
+
if ($type -match '^(.+)Service$') {
|
| 169 |
+
$serviceName = $type
|
| 170 |
+
return "servicesProvider.Provide${serviceName}()"
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
# ADD MORE SPECIAL CASES HERE:
|
| 174 |
+
# --------------------------------------------
|
| 175 |
+
# Example: Config dependency
|
| 176 |
+
# if ($type -eq "Config") {
|
| 177 |
+
# return "configProvider.ProvideConfig()"
|
| 178 |
+
# }
|
| 179 |
+
#
|
| 180 |
+
# Example: Logger
|
| 181 |
+
# if ($type -eq "Logger") {
|
| 182 |
+
# return "loggerProvider.ProvideLogger()"
|
| 183 |
+
# }
|
| 184 |
+
#
|
| 185 |
+
# Example: Validator
|
| 186 |
+
# if ($type -eq "Validator") {
|
| 187 |
+
# return "validatorProvider.ProvideValidator()"
|
| 188 |
+
# }
|
| 189 |
+
# --------------------------------------------
|
| 190 |
+
|
| 191 |
+
# 2. Fallback: unresolved type
|
| 192 |
+
return "/* TODO: provide $($Param.RawType) */"
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
function Generate-ProviderCode {
|
| 196 |
+
param([System.Collections.Generic.List[ControllerInfo]]$Controllers)
|
| 197 |
+
|
| 198 |
+
Write-ColorOutput "`nGenerating controller provider code..." "Cyan"
|
| 199 |
+
|
| 200 |
+
# Sort controllers alphabetically for consistent output
|
| 201 |
+
$sortedControllers = $Controllers | Sort-Object -Property Domain
|
| 202 |
+
|
| 203 |
+
$sb = [System.Text.StringBuilder]::new()
|
| 204 |
+
[void]$sb.AppendLine("package provider")
|
| 205 |
+
[void]$sb.AppendLine()
|
| 206 |
+
[void]$sb.AppendLine("import `"$ModulePath`"")
|
| 207 |
+
[void]$sb.AppendLine()
|
| 208 |
+
|
| 209 |
+
# Interface
|
| 210 |
+
[void]$sb.AppendLine("type ControllerProvider interface {")
|
| 211 |
+
foreach ($ctrl in $sortedControllers) {
|
| 212 |
+
$line = "`tProvide$($ctrl.Domain)() controllers.$($ctrl.Domain)"
|
| 213 |
+
[void]$sb.AppendLine($line)
|
| 214 |
+
}
|
| 215 |
+
[void]$sb.AppendLine("}")
|
| 216 |
+
[void]$sb.AppendLine()
|
| 217 |
+
|
| 218 |
+
# Struct
|
| 219 |
+
[void]$sb.AppendLine("type controllerProvider struct {")
|
| 220 |
+
foreach ($ctrl in $sortedControllers) {
|
| 221 |
+
$line = "`t$($ctrl.VarName) controllers.$($ctrl.Domain)"
|
| 222 |
+
[void]$sb.AppendLine($line)
|
| 223 |
+
}
|
| 224 |
+
[void]$sb.AppendLine("}")
|
| 225 |
+
[void]$sb.AppendLine()
|
| 226 |
+
|
| 227 |
+
# Constructor
|
| 228 |
+
[void]$sb.AppendLine("func NewControllerProvider(servicesProvider ServicesProvider) ControllerProvider {")
|
| 229 |
+
[void]$sb.AppendLine()
|
| 230 |
+
|
| 231 |
+
# Initialize controllers
|
| 232 |
+
foreach ($ctrl in $sortedControllers) {
|
| 233 |
+
$args = @()
|
| 234 |
+
foreach ($param in $ctrl.Parameters) {
|
| 235 |
+
$args += Resolve-ControllerArgument $param
|
| 236 |
+
}
|
| 237 |
+
$argsStr = $args -join ", "
|
| 238 |
+
$line = "`t$($ctrl.VarName) := controllers.$($ctrl.ConstructorName)($argsStr)"
|
| 239 |
+
[void]$sb.AppendLine($line)
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
[void]$sb.AppendLine("`treturn &controllerProvider{")
|
| 243 |
+
foreach ($ctrl in $sortedControllers) {
|
| 244 |
+
$line = "`t`t$($ctrl.VarName): $($ctrl.VarName),"
|
| 245 |
+
[void]$sb.AppendLine($line)
|
| 246 |
+
}
|
| 247 |
+
[void]$sb.AppendLine("`t}")
|
| 248 |
+
[void]$sb.AppendLine("}")
|
| 249 |
+
[void]$sb.AppendLine()
|
| 250 |
+
|
| 251 |
+
# Getter methods
|
| 252 |
+
[void]$sb.AppendLine("// --- Getter Methods ---")
|
| 253 |
+
[void]$sb.AppendLine()
|
| 254 |
+
foreach ($ctrl in $sortedControllers) {
|
| 255 |
+
[void]$sb.AppendLine("func (c *controllerProvider) Provide$($ctrl.Domain)() controllers.$($ctrl.Domain) {")
|
| 256 |
+
[void]$sb.AppendLine("`treturn c.$($ctrl.VarName)")
|
| 257 |
+
[void]$sb.AppendLine("}")
|
| 258 |
+
[void]$sb.AppendLine()
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
return $sb.ToString()
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
function Write-ProviderFile {
|
| 265 |
+
param([string]$Code, [string]$OutputPath)
|
| 266 |
+
|
| 267 |
+
Write-ColorOutput "Writing to $OutputPath..." "Cyan"
|
| 268 |
+
|
| 269 |
+
# Ensure directory exists
|
| 270 |
+
$dir = Split-Path $OutputPath -Parent
|
| 271 |
+
if ($dir -and -not (Test-Path $dir)) {
|
| 272 |
+
New-Item -ItemType Directory -Path $dir -Force | Out-Null
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
# Write file as UTF-8 without BOM
|
| 276 |
+
$utf8NoBom = [System.Text.UTF8Encoding]::new($false)
|
| 277 |
+
[System.IO.File]::WriteAllText($OutputPath, $Code, $utf8NoBom)
|
| 278 |
+
|
| 279 |
+
Write-ColorOutput " Successfully generated $OutputPath" "Green"
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
# ============================================
|
| 283 |
+
# MAIN EXECUTION
|
| 284 |
+
# ============================================
|
| 285 |
+
|
| 286 |
+
try {
|
| 287 |
+
Write-ColorOutput "`n=========================================" "Blue"
|
| 288 |
+
Write-ColorOutput " Go Controller Provider Generator v1.0" "Blue"
|
| 289 |
+
Write-ColorOutput "=========================================`n" "Blue"
|
| 290 |
+
|
| 291 |
+
# Step 1: Parse all controller constructors
|
| 292 |
+
$controllers = Parse-GoFiles -Directory $ControllersDir
|
| 293 |
+
|
| 294 |
+
# Step 2: Generate provider code
|
| 295 |
+
$code = Generate-ProviderCode -Controllers $controllers
|
| 296 |
+
|
| 297 |
+
# Step 3: Write to file
|
| 298 |
+
Write-ProviderFile -Code $code -OutputPath $OutputFile
|
| 299 |
+
|
| 300 |
+
Write-ColorOutput "`nSUCCESS! Controller provider generated successfully.`n" "Green"
|
| 301 |
+
Write-ColorOutput "Next steps:" "Cyan"
|
| 302 |
+
Write-ColorOutput " 1. Review $OutputFile" "White"
|
| 303 |
+
Write-ColorOutput " 2. Fill any /* TODO: provide ... */ placeholders" "White"
|
| 304 |
+
Write-ColorOutput " 3. Run: go build ./provider" "White"
|
| 305 |
+
|
| 306 |
+
} catch {
|
| 307 |
+
Write-ColorOutput "`nERROR: $($_.Exception.Message)" "Red"
|
| 308 |
+
Write-ColorOutput "Stack trace: $($_.ScriptStackTrace)" "Yellow"
|
| 309 |
+
exit 1
|
| 310 |
}
|
do_inject_middleware.ps1
CHANGED
|
@@ -1,312 +1,312 @@
|
|
| 1 |
-
#Requires -Version 5.1
|
| 2 |
-
<#
|
| 3 |
-
.SYNOPSIS
|
| 4 |
-
Automatic Dependency Injection Generator for Go Middleware
|
| 5 |
-
|
| 6 |
-
.DESCRIPTION
|
| 7 |
-
Scans ./middleware/ directory, discovers all middleware constructors, infers their dependencies,
|
| 8 |
-
and generates provider/middleware_provider.go with full DI wiring.
|
| 9 |
-
|
| 10 |
-
.EXAMPLE
|
| 11 |
-
.\middleware_injector.ps1
|
| 12 |
-
|
| 13 |
-
.NOTES
|
| 14 |
-
- Works with PowerShell 5.1+ and PowerShell 7+
|
| 15 |
-
- No external dependencies required
|
| 16 |
-
- Supports multi-line constructor signatures
|
| 17 |
-
- Middleware depend on services from ServicesProvider
|
| 18 |
-
#>
|
| 19 |
-
|
| 20 |
-
[CmdletBinding()]
|
| 21 |
-
param()
|
| 22 |
-
|
| 23 |
-
# Configuration
|
| 24 |
-
$MiddlewareDir = "./middleware"
|
| 25 |
-
$OutputFile = "provider/middleware_provider.go"
|
| 26 |
-
$ModulePath = "abdanhafidz.com/go-boilerplate/middleware"
|
| 27 |
-
|
| 28 |
-
# ANSI colors for better output
|
| 29 |
-
$script:UseColors = $Host.UI.SupportsVirtualTerminal
|
| 30 |
-
function Write-ColorOutput {
|
| 31 |
-
param([string]$Message, [string]$Color = "White")
|
| 32 |
-
if ($script:UseColors) {
|
| 33 |
-
$colors = @{
|
| 34 |
-
"Green" = "`e[32m"; "Yellow" = "`e[33m"; "Red" = "`e[31m"
|
| 35 |
-
"Cyan" = "`e[36m"; "Blue" = "`e[34m"; "Reset" = "`e[0m"
|
| 36 |
-
}
|
| 37 |
-
Write-Host "$($colors[$Color])$Message$($colors['Reset'])"
|
| 38 |
-
} else {
|
| 39 |
-
Write-Host $Message
|
| 40 |
-
}
|
| 41 |
-
}
|
| 42 |
-
|
| 43 |
-
# Data structures
|
| 44 |
-
class MiddlewareInfo {
|
| 45 |
-
[string]$ConstructorName # NewAuthenticationMiddleware
|
| 46 |
-
[string]$Domain # AuthenticationMiddleware
|
| 47 |
-
[string]$VarName # authenticationMiddleware
|
| 48 |
-
[System.Collections.Generic.List[Parameter]]$Parameters
|
| 49 |
-
|
| 50 |
-
MiddlewareInfo() {
|
| 51 |
-
$this.Parameters = [System.Collections.Generic.List[Parameter]]::new()
|
| 52 |
-
}
|
| 53 |
-
}
|
| 54 |
-
|
| 55 |
-
class Parameter {
|
| 56 |
-
[string]$Name
|
| 57 |
-
[string]$RawType
|
| 58 |
-
[string]$NormalizedType
|
| 59 |
-
}
|
| 60 |
-
|
| 61 |
-
function Get-LowerCamelCase {
|
| 62 |
-
param([string]$Text)
|
| 63 |
-
if ($Text.Length -eq 0) { return $Text }
|
| 64 |
-
return $Text.Substring(0, 1).ToLower() + $Text.Substring(1)
|
| 65 |
-
}
|
| 66 |
-
|
| 67 |
-
function Normalize-TypeName {
|
| 68 |
-
param([string]$TypeStr)
|
| 69 |
-
|
| 70 |
-
# Remove leading pointer
|
| 71 |
-
$cleaned = $TypeStr -replace '^\*+', ''
|
| 72 |
-
|
| 73 |
-
# Remove package prefix (everything before last dot)
|
| 74 |
-
if ($cleaned -match '\.([^.]+)$') {
|
| 75 |
-
$cleaned = $matches[1]
|
| 76 |
-
}
|
| 77 |
-
|
| 78 |
-
return $cleaned.Trim()
|
| 79 |
-
}
|
| 80 |
-
|
| 81 |
-
function Parse-GoFiles {
|
| 82 |
-
param([string]$Directory)
|
| 83 |
-
|
| 84 |
-
Write-ColorOutput "Scanning for middleware constructors in $Directory..." "Cyan"
|
| 85 |
-
|
| 86 |
-
if (-not (Test-Path $Directory)) {
|
| 87 |
-
Write-ColorOutput "ERROR: Directory '$Directory' not found!" "Red"
|
| 88 |
-
exit 1
|
| 89 |
-
}
|
| 90 |
-
|
| 91 |
-
$goFiles = Get-ChildItem -Path $Directory -Filter "*.go" -Recurse -File
|
| 92 |
-
$middlewares = [System.Collections.Generic.List[MiddlewareInfo]]::new()
|
| 93 |
-
|
| 94 |
-
foreach ($file in $goFiles) {
|
| 95 |
-
$content = Get-Content $file.FullName -Raw
|
| 96 |
-
|
| 97 |
-
# Match function signatures (support multi-line)
|
| 98 |
-
# Pattern: func NewXxxMiddleware(...) XxxMiddleware
|
| 99 |
-
$pattern = '(?ms)func\s+(New[a-zA-Z0-9]+Middleware)\s*\(([^)]*)\)\s+([a-zA-Z0-9*_.]+Middleware)'
|
| 100 |
-
$matches = [regex]::Matches($content, $pattern)
|
| 101 |
-
|
| 102 |
-
foreach ($match in $matches) {
|
| 103 |
-
$constructorName = $match.Groups[1].Value
|
| 104 |
-
$paramsStr = $match.Groups[2].Value
|
| 105 |
-
$returnType = $match.Groups[3].Value
|
| 106 |
-
|
| 107 |
-
# Extract domain name (XxxMiddleware)
|
| 108 |
-
$domain = Normalize-TypeName $returnType
|
| 109 |
-
$varName = Get-LowerCamelCase $domain
|
| 110 |
-
|
| 111 |
-
$middleware = [MiddlewareInfo]::new()
|
| 112 |
-
$middleware.ConstructorName = $constructorName
|
| 113 |
-
$middleware.Domain = $domain
|
| 114 |
-
$middleware.VarName = $varName
|
| 115 |
-
|
| 116 |
-
# Parse parameters
|
| 117 |
-
if ($paramsStr.Trim() -ne "") {
|
| 118 |
-
# Split by comma, but be careful with nested types
|
| 119 |
-
$paramList = $paramsStr -split ',\s*(?![^<>]*>)'
|
| 120 |
-
|
| 121 |
-
foreach ($param in $paramList) {
|
| 122 |
-
$param = $param.Trim()
|
| 123 |
-
if ($param -eq "") { continue }
|
| 124 |
-
|
| 125 |
-
# Split into name and type
|
| 126 |
-
$parts = $param -split '\s+', 2
|
| 127 |
-
|
| 128 |
-
$p = [Parameter]::new()
|
| 129 |
-
if ($parts.Count -eq 2) {
|
| 130 |
-
$p.Name = $parts[0]
|
| 131 |
-
$p.RawType = $parts[1]
|
| 132 |
-
} elseif ($parts.Count -eq 1) {
|
| 133 |
-
# Anonymous parameter - synthesize name
|
| 134 |
-
$p.Name = "param$($middleware.Parameters.Count)"
|
| 135 |
-
$p.RawType = $parts[0]
|
| 136 |
-
} else {
|
| 137 |
-
continue
|
| 138 |
-
}
|
| 139 |
-
|
| 140 |
-
$p.NormalizedType = Normalize-TypeName $p.RawType
|
| 141 |
-
$middleware.Parameters.Add($p)
|
| 142 |
-
}
|
| 143 |
-
}
|
| 144 |
-
|
| 145 |
-
$middlewares.Add($middleware)
|
| 146 |
-
Write-ColorOutput " Found: $constructorName" "Green"
|
| 147 |
-
}
|
| 148 |
-
}
|
| 149 |
-
|
| 150 |
-
if ($middlewares.Count -eq 0) {
|
| 151 |
-
Write-ColorOutput "No middleware constructors found matching pattern 'NewXxxMiddleware'!" "Red"
|
| 152 |
-
exit 1
|
| 153 |
-
}
|
| 154 |
-
|
| 155 |
-
Write-ColorOutput "`nTotal middleware discovered: $($middlewares.Count)" "Blue"
|
| 156 |
-
return $middlewares
|
| 157 |
-
}
|
| 158 |
-
|
| 159 |
-
function Resolve-MiddlewareArgument {
|
| 160 |
-
param([Parameter]$Param)
|
| 161 |
-
|
| 162 |
-
$type = $Param.NormalizedType
|
| 163 |
-
|
| 164 |
-
# DEPENDENCY RESOLUTION RULES
|
| 165 |
-
# ============================================
|
| 166 |
-
|
| 167 |
-
# 1. Service pattern: XxxxService -> servicesProvider.ProvideXxxxService()
|
| 168 |
-
if ($type -match '^(.+)Service$') {
|
| 169 |
-
$serviceName = $type
|
| 170 |
-
return "servicesProvider.Provide${serviceName}()"
|
| 171 |
-
}
|
| 172 |
-
|
| 173 |
-
# ADD MORE SPECIAL CASES HERE:
|
| 174 |
-
# --------------------------------------------
|
| 175 |
-
# Example: Config dependency
|
| 176 |
-
# if ($type -eq "Config") {
|
| 177 |
-
# return "configProvider.ProvideConfig()"
|
| 178 |
-
# }
|
| 179 |
-
#
|
| 180 |
-
# Example: Logger
|
| 181 |
-
# if ($type -eq "Logger") {
|
| 182 |
-
# return "loggerProvider.ProvideLogger()"
|
| 183 |
-
# }
|
| 184 |
-
#
|
| 185 |
-
# Example: JWT Config
|
| 186 |
-
# if ($type -eq "JWTConfig") {
|
| 187 |
-
# return "configProvider.ProvideJWTConfig()"
|
| 188 |
-
# }
|
| 189 |
-
#
|
| 190 |
-
# Example: Database
|
| 191 |
-
# if ($type -eq "DB" -or $type -eq "Database") {
|
| 192 |
-
# return "dbProvider.ProvideDatabase()"
|
| 193 |
-
# }
|
| 194 |
-
# --------------------------------------------
|
| 195 |
-
|
| 196 |
-
# 2. Fallback: unresolved type
|
| 197 |
-
return "/* TODO: provide $($Param.RawType) */"
|
| 198 |
-
}
|
| 199 |
-
|
| 200 |
-
function Generate-ProviderCode {
|
| 201 |
-
param([System.Collections.Generic.List[MiddlewareInfo]]$Middlewares)
|
| 202 |
-
|
| 203 |
-
Write-ColorOutput "`nGenerating middleware provider code..." "Cyan"
|
| 204 |
-
|
| 205 |
-
# Sort middleware alphabetically for consistent output
|
| 206 |
-
$sortedMiddlewares = $Middlewares | Sort-Object -Property Domain
|
| 207 |
-
|
| 208 |
-
$sb = [System.Text.StringBuilder]::new()
|
| 209 |
-
[void]$sb.AppendLine("package provider")
|
| 210 |
-
[void]$sb.AppendLine()
|
| 211 |
-
[void]$sb.AppendLine("import `"$ModulePath`"")
|
| 212 |
-
[void]$sb.AppendLine()
|
| 213 |
-
|
| 214 |
-
# Interface
|
| 215 |
-
[void]$sb.AppendLine("type MiddlewareProvider interface {")
|
| 216 |
-
foreach ($mw in $sortedMiddlewares) {
|
| 217 |
-
$line = "`tProvide$($mw.Domain)() middleware.$($mw.Domain)"
|
| 218 |
-
[void]$sb.AppendLine($line)
|
| 219 |
-
}
|
| 220 |
-
[void]$sb.AppendLine("}")
|
| 221 |
-
[void]$sb.AppendLine()
|
| 222 |
-
|
| 223 |
-
# Struct
|
| 224 |
-
[void]$sb.AppendLine("type middlewareProvider struct {")
|
| 225 |
-
foreach ($mw in $sortedMiddlewares) {
|
| 226 |
-
$line = "`t$($mw.VarName) middleware.$($mw.Domain)"
|
| 227 |
-
[void]$sb.AppendLine($line)
|
| 228 |
-
}
|
| 229 |
-
[void]$sb.AppendLine("}")
|
| 230 |
-
[void]$sb.AppendLine()
|
| 231 |
-
|
| 232 |
-
# Constructor
|
| 233 |
-
[void]$sb.AppendLine("func NewMiddlewareProvider(servicesProvider ServicesProvider) MiddlewareProvider {")
|
| 234 |
-
|
| 235 |
-
# Initialize middleware
|
| 236 |
-
foreach ($mw in $sortedMiddlewares) {
|
| 237 |
-
$args = @()
|
| 238 |
-
foreach ($param in $mw.Parameters) {
|
| 239 |
-
$args += Resolve-MiddlewareArgument $param
|
| 240 |
-
}
|
| 241 |
-
$argsStr = $args -join ", "
|
| 242 |
-
$line = "`t$($mw.VarName) := middleware.$($mw.ConstructorName)($argsStr)"
|
| 243 |
-
[void]$sb.AppendLine($line)
|
| 244 |
-
}
|
| 245 |
-
|
| 246 |
-
[void]$sb.AppendLine("`treturn &middlewareProvider{")
|
| 247 |
-
foreach ($mw in $sortedMiddlewares) {
|
| 248 |
-
$line = "`t`t$($mw.VarName): $($mw.VarName),"
|
| 249 |
-
[void]$sb.AppendLine($line)
|
| 250 |
-
}
|
| 251 |
-
[void]$sb.AppendLine("`t}")
|
| 252 |
-
[void]$sb.AppendLine("}")
|
| 253 |
-
[void]$sb.AppendLine()
|
| 254 |
-
|
| 255 |
-
# Getter methods
|
| 256 |
-
foreach ($mw in $sortedMiddlewares) {
|
| 257 |
-
[void]$sb.AppendLine("func (p *middlewareProvider) Provide$($mw.Domain)() middleware.$($mw.Domain) {")
|
| 258 |
-
[void]$sb.AppendLine("`treturn p.$($mw.VarName)")
|
| 259 |
-
[void]$sb.AppendLine("}")
|
| 260 |
-
[void]$sb.AppendLine()
|
| 261 |
-
}
|
| 262 |
-
|
| 263 |
-
return $sb.ToString()
|
| 264 |
-
}
|
| 265 |
-
|
| 266 |
-
function Write-ProviderFile {
|
| 267 |
-
param([string]$Code, [string]$OutputPath)
|
| 268 |
-
|
| 269 |
-
Write-ColorOutput "Writing to $OutputPath..." "Cyan"
|
| 270 |
-
|
| 271 |
-
# Ensure directory exists
|
| 272 |
-
$dir = Split-Path $OutputPath -Parent
|
| 273 |
-
if ($dir -and -not (Test-Path $dir)) {
|
| 274 |
-
New-Item -ItemType Directory -Path $dir -Force | Out-Null
|
| 275 |
-
}
|
| 276 |
-
|
| 277 |
-
# Write file as UTF-8 without BOM
|
| 278 |
-
$utf8NoBom = [System.Text.UTF8Encoding]::new($false)
|
| 279 |
-
[System.IO.File]::WriteAllText($OutputPath, $Code, $utf8NoBom)
|
| 280 |
-
|
| 281 |
-
Write-ColorOutput " Successfully generated $OutputPath" "Green"
|
| 282 |
-
}
|
| 283 |
-
|
| 284 |
-
# ============================================
|
| 285 |
-
# MAIN EXECUTION
|
| 286 |
-
# ============================================
|
| 287 |
-
|
| 288 |
-
try {
|
| 289 |
-
Write-ColorOutput "`n=========================================" "Blue"
|
| 290 |
-
Write-ColorOutput " Go Middleware Provider Generator v1.0" "Blue"
|
| 291 |
-
Write-ColorOutput "=========================================`n" "Blue"
|
| 292 |
-
|
| 293 |
-
# Step 1: Parse all middleware constructors
|
| 294 |
-
$middlewares = Parse-GoFiles -Directory $MiddlewareDir
|
| 295 |
-
|
| 296 |
-
# Step 2: Generate provider code
|
| 297 |
-
$code = Generate-ProviderCode -Middlewares $middlewares
|
| 298 |
-
|
| 299 |
-
# Step 3: Write to file
|
| 300 |
-
Write-ProviderFile -Code $code -OutputPath $OutputFile
|
| 301 |
-
|
| 302 |
-
Write-ColorOutput "`nSUCCESS! Middleware provider generated successfully.`n" "Green"
|
| 303 |
-
Write-ColorOutput "Next steps:" "Cyan"
|
| 304 |
-
Write-ColorOutput " 1. Review $OutputFile" "White"
|
| 305 |
-
Write-ColorOutput " 2. Fill any /* TODO: provide ... */ placeholders" "White"
|
| 306 |
-
Write-ColorOutput " 3. Run: go build ./provider" "White"
|
| 307 |
-
|
| 308 |
-
} catch {
|
| 309 |
-
Write-ColorOutput "`nERROR: $($_.Exception.Message)" "Red"
|
| 310 |
-
Write-ColorOutput "Stack trace: $($_.ScriptStackTrace)" "Yellow"
|
| 311 |
-
exit 1
|
| 312 |
}
|
|
|
|
| 1 |
+
#Requires -Version 5.1
|
| 2 |
+
<#
|
| 3 |
+
.SYNOPSIS
|
| 4 |
+
Automatic Dependency Injection Generator for Go Middleware
|
| 5 |
+
|
| 6 |
+
.DESCRIPTION
|
| 7 |
+
Scans ./middleware/ directory, discovers all middleware constructors, infers their dependencies,
|
| 8 |
+
and generates provider/middleware_provider.go with full DI wiring.
|
| 9 |
+
|
| 10 |
+
.EXAMPLE
|
| 11 |
+
.\middleware_injector.ps1
|
| 12 |
+
|
| 13 |
+
.NOTES
|
| 14 |
+
- Works with PowerShell 5.1+ and PowerShell 7+
|
| 15 |
+
- No external dependencies required
|
| 16 |
+
- Supports multi-line constructor signatures
|
| 17 |
+
- Middleware depend on services from ServicesProvider
|
| 18 |
+
#>
|
| 19 |
+
|
| 20 |
+
[CmdletBinding()]
|
| 21 |
+
param()
|
| 22 |
+
|
| 23 |
+
# Configuration
|
| 24 |
+
$MiddlewareDir = "./middleware"
|
| 25 |
+
$OutputFile = "provider/middleware_provider.go"
|
| 26 |
+
$ModulePath = "abdanhafidz.com/go-boilerplate/middleware"
|
| 27 |
+
|
| 28 |
+
# ANSI colors for better output
|
| 29 |
+
$script:UseColors = $Host.UI.SupportsVirtualTerminal
|
| 30 |
+
function Write-ColorOutput {
|
| 31 |
+
param([string]$Message, [string]$Color = "White")
|
| 32 |
+
if ($script:UseColors) {
|
| 33 |
+
$colors = @{
|
| 34 |
+
"Green" = "`e[32m"; "Yellow" = "`e[33m"; "Red" = "`e[31m"
|
| 35 |
+
"Cyan" = "`e[36m"; "Blue" = "`e[34m"; "Reset" = "`e[0m"
|
| 36 |
+
}
|
| 37 |
+
Write-Host "$($colors[$Color])$Message$($colors['Reset'])"
|
| 38 |
+
} else {
|
| 39 |
+
Write-Host $Message
|
| 40 |
+
}
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
# Data structures
|
| 44 |
+
class MiddlewareInfo {
|
| 45 |
+
[string]$ConstructorName # NewAuthenticationMiddleware
|
| 46 |
+
[string]$Domain # AuthenticationMiddleware
|
| 47 |
+
[string]$VarName # authenticationMiddleware
|
| 48 |
+
[System.Collections.Generic.List[Parameter]]$Parameters
|
| 49 |
+
|
| 50 |
+
MiddlewareInfo() {
|
| 51 |
+
$this.Parameters = [System.Collections.Generic.List[Parameter]]::new()
|
| 52 |
+
}
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
class Parameter {
|
| 56 |
+
[string]$Name
|
| 57 |
+
[string]$RawType
|
| 58 |
+
[string]$NormalizedType
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
function Get-LowerCamelCase {
|
| 62 |
+
param([string]$Text)
|
| 63 |
+
if ($Text.Length -eq 0) { return $Text }
|
| 64 |
+
return $Text.Substring(0, 1).ToLower() + $Text.Substring(1)
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
function Normalize-TypeName {
|
| 68 |
+
param([string]$TypeStr)
|
| 69 |
+
|
| 70 |
+
# Remove leading pointer
|
| 71 |
+
$cleaned = $TypeStr -replace '^\*+', ''
|
| 72 |
+
|
| 73 |
+
# Remove package prefix (everything before last dot)
|
| 74 |
+
if ($cleaned -match '\.([^.]+)$') {
|
| 75 |
+
$cleaned = $matches[1]
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
return $cleaned.Trim()
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
function Parse-GoFiles {
|
| 82 |
+
param([string]$Directory)
|
| 83 |
+
|
| 84 |
+
Write-ColorOutput "Scanning for middleware constructors in $Directory..." "Cyan"
|
| 85 |
+
|
| 86 |
+
if (-not (Test-Path $Directory)) {
|
| 87 |
+
Write-ColorOutput "ERROR: Directory '$Directory' not found!" "Red"
|
| 88 |
+
exit 1
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
$goFiles = Get-ChildItem -Path $Directory -Filter "*.go" -Recurse -File
|
| 92 |
+
$middlewares = [System.Collections.Generic.List[MiddlewareInfo]]::new()
|
| 93 |
+
|
| 94 |
+
foreach ($file in $goFiles) {
|
| 95 |
+
$content = Get-Content $file.FullName -Raw
|
| 96 |
+
|
| 97 |
+
# Match function signatures (support multi-line)
|
| 98 |
+
# Pattern: func NewXxxMiddleware(...) XxxMiddleware
|
| 99 |
+
$pattern = '(?ms)func\s+(New[a-zA-Z0-9]+Middleware)\s*\(([^)]*)\)\s+([a-zA-Z0-9*_.]+Middleware)'
|
| 100 |
+
$matches = [regex]::Matches($content, $pattern)
|
| 101 |
+
|
| 102 |
+
foreach ($match in $matches) {
|
| 103 |
+
$constructorName = $match.Groups[1].Value
|
| 104 |
+
$paramsStr = $match.Groups[2].Value
|
| 105 |
+
$returnType = $match.Groups[3].Value
|
| 106 |
+
|
| 107 |
+
# Extract domain name (XxxMiddleware)
|
| 108 |
+
$domain = Normalize-TypeName $returnType
|
| 109 |
+
$varName = Get-LowerCamelCase $domain
|
| 110 |
+
|
| 111 |
+
$middleware = [MiddlewareInfo]::new()
|
| 112 |
+
$middleware.ConstructorName = $constructorName
|
| 113 |
+
$middleware.Domain = $domain
|
| 114 |
+
$middleware.VarName = $varName
|
| 115 |
+
|
| 116 |
+
# Parse parameters
|
| 117 |
+
if ($paramsStr.Trim() -ne "") {
|
| 118 |
+
# Split by comma, but be careful with nested types
|
| 119 |
+
$paramList = $paramsStr -split ',\s*(?![^<>]*>)'
|
| 120 |
+
|
| 121 |
+
foreach ($param in $paramList) {
|
| 122 |
+
$param = $param.Trim()
|
| 123 |
+
if ($param -eq "") { continue }
|
| 124 |
+
|
| 125 |
+
# Split into name and type
|
| 126 |
+
$parts = $param -split '\s+', 2
|
| 127 |
+
|
| 128 |
+
$p = [Parameter]::new()
|
| 129 |
+
if ($parts.Count -eq 2) {
|
| 130 |
+
$p.Name = $parts[0]
|
| 131 |
+
$p.RawType = $parts[1]
|
| 132 |
+
} elseif ($parts.Count -eq 1) {
|
| 133 |
+
# Anonymous parameter - synthesize name
|
| 134 |
+
$p.Name = "param$($middleware.Parameters.Count)"
|
| 135 |
+
$p.RawType = $parts[0]
|
| 136 |
+
} else {
|
| 137 |
+
continue
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
$p.NormalizedType = Normalize-TypeName $p.RawType
|
| 141 |
+
$middleware.Parameters.Add($p)
|
| 142 |
+
}
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
$middlewares.Add($middleware)
|
| 146 |
+
Write-ColorOutput " Found: $constructorName" "Green"
|
| 147 |
+
}
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
if ($middlewares.Count -eq 0) {
|
| 151 |
+
Write-ColorOutput "No middleware constructors found matching pattern 'NewXxxMiddleware'!" "Red"
|
| 152 |
+
exit 1
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
Write-ColorOutput "`nTotal middleware discovered: $($middlewares.Count)" "Blue"
|
| 156 |
+
return $middlewares
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
function Resolve-MiddlewareArgument {
|
| 160 |
+
param([Parameter]$Param)
|
| 161 |
+
|
| 162 |
+
$type = $Param.NormalizedType
|
| 163 |
+
|
| 164 |
+
# DEPENDENCY RESOLUTION RULES
|
| 165 |
+
# ============================================
|
| 166 |
+
|
| 167 |
+
# 1. Service pattern: XxxxService -> servicesProvider.ProvideXxxxService()
|
| 168 |
+
if ($type -match '^(.+)Service$') {
|
| 169 |
+
$serviceName = $type
|
| 170 |
+
return "servicesProvider.Provide${serviceName}()"
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
# ADD MORE SPECIAL CASES HERE:
|
| 174 |
+
# --------------------------------------------
|
| 175 |
+
# Example: Config dependency
|
| 176 |
+
# if ($type -eq "Config") {
|
| 177 |
+
# return "configProvider.ProvideConfig()"
|
| 178 |
+
# }
|
| 179 |
+
#
|
| 180 |
+
# Example: Logger
|
| 181 |
+
# if ($type -eq "Logger") {
|
| 182 |
+
# return "loggerProvider.ProvideLogger()"
|
| 183 |
+
# }
|
| 184 |
+
#
|
| 185 |
+
# Example: JWT Config
|
| 186 |
+
# if ($type -eq "JWTConfig") {
|
| 187 |
+
# return "configProvider.ProvideJWTConfig()"
|
| 188 |
+
# }
|
| 189 |
+
#
|
| 190 |
+
# Example: Database
|
| 191 |
+
# if ($type -eq "DB" -or $type -eq "Database") {
|
| 192 |
+
# return "dbProvider.ProvideDatabase()"
|
| 193 |
+
# }
|
| 194 |
+
# --------------------------------------------
|
| 195 |
+
|
| 196 |
+
# 2. Fallback: unresolved type
|
| 197 |
+
return "/* TODO: provide $($Param.RawType) */"
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
function Generate-ProviderCode {
|
| 201 |
+
param([System.Collections.Generic.List[MiddlewareInfo]]$Middlewares)
|
| 202 |
+
|
| 203 |
+
Write-ColorOutput "`nGenerating middleware provider code..." "Cyan"
|
| 204 |
+
|
| 205 |
+
# Sort middleware alphabetically for consistent output
|
| 206 |
+
$sortedMiddlewares = $Middlewares | Sort-Object -Property Domain
|
| 207 |
+
|
| 208 |
+
$sb = [System.Text.StringBuilder]::new()
|
| 209 |
+
[void]$sb.AppendLine("package provider")
|
| 210 |
+
[void]$sb.AppendLine()
|
| 211 |
+
[void]$sb.AppendLine("import `"$ModulePath`"")
|
| 212 |
+
[void]$sb.AppendLine()
|
| 213 |
+
|
| 214 |
+
# Interface
|
| 215 |
+
[void]$sb.AppendLine("type MiddlewareProvider interface {")
|
| 216 |
+
foreach ($mw in $sortedMiddlewares) {
|
| 217 |
+
$line = "`tProvide$($mw.Domain)() middleware.$($mw.Domain)"
|
| 218 |
+
[void]$sb.AppendLine($line)
|
| 219 |
+
}
|
| 220 |
+
[void]$sb.AppendLine("}")
|
| 221 |
+
[void]$sb.AppendLine()
|
| 222 |
+
|
| 223 |
+
# Struct
|
| 224 |
+
[void]$sb.AppendLine("type middlewareProvider struct {")
|
| 225 |
+
foreach ($mw in $sortedMiddlewares) {
|
| 226 |
+
$line = "`t$($mw.VarName) middleware.$($mw.Domain)"
|
| 227 |
+
[void]$sb.AppendLine($line)
|
| 228 |
+
}
|
| 229 |
+
[void]$sb.AppendLine("}")
|
| 230 |
+
[void]$sb.AppendLine()
|
| 231 |
+
|
| 232 |
+
# Constructor
|
| 233 |
+
[void]$sb.AppendLine("func NewMiddlewareProvider(servicesProvider ServicesProvider) MiddlewareProvider {")
|
| 234 |
+
|
| 235 |
+
# Initialize middleware
|
| 236 |
+
foreach ($mw in $sortedMiddlewares) {
|
| 237 |
+
$args = @()
|
| 238 |
+
foreach ($param in $mw.Parameters) {
|
| 239 |
+
$args += Resolve-MiddlewareArgument $param
|
| 240 |
+
}
|
| 241 |
+
$argsStr = $args -join ", "
|
| 242 |
+
$line = "`t$($mw.VarName) := middleware.$($mw.ConstructorName)($argsStr)"
|
| 243 |
+
[void]$sb.AppendLine($line)
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
[void]$sb.AppendLine("`treturn &middlewareProvider{")
|
| 247 |
+
foreach ($mw in $sortedMiddlewares) {
|
| 248 |
+
$line = "`t`t$($mw.VarName): $($mw.VarName),"
|
| 249 |
+
[void]$sb.AppendLine($line)
|
| 250 |
+
}
|
| 251 |
+
[void]$sb.AppendLine("`t}")
|
| 252 |
+
[void]$sb.AppendLine("}")
|
| 253 |
+
[void]$sb.AppendLine()
|
| 254 |
+
|
| 255 |
+
# Getter methods
|
| 256 |
+
foreach ($mw in $sortedMiddlewares) {
|
| 257 |
+
[void]$sb.AppendLine("func (p *middlewareProvider) Provide$($mw.Domain)() middleware.$($mw.Domain) {")
|
| 258 |
+
[void]$sb.AppendLine("`treturn p.$($mw.VarName)")
|
| 259 |
+
[void]$sb.AppendLine("}")
|
| 260 |
+
[void]$sb.AppendLine()
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
return $sb.ToString()
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
function Write-ProviderFile {
|
| 267 |
+
param([string]$Code, [string]$OutputPath)
|
| 268 |
+
|
| 269 |
+
Write-ColorOutput "Writing to $OutputPath..." "Cyan"
|
| 270 |
+
|
| 271 |
+
# Ensure directory exists
|
| 272 |
+
$dir = Split-Path $OutputPath -Parent
|
| 273 |
+
if ($dir -and -not (Test-Path $dir)) {
|
| 274 |
+
New-Item -ItemType Directory -Path $dir -Force | Out-Null
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
# Write file as UTF-8 without BOM
|
| 278 |
+
$utf8NoBom = [System.Text.UTF8Encoding]::new($false)
|
| 279 |
+
[System.IO.File]::WriteAllText($OutputPath, $Code, $utf8NoBom)
|
| 280 |
+
|
| 281 |
+
Write-ColorOutput " Successfully generated $OutputPath" "Green"
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
# ============================================
|
| 285 |
+
# MAIN EXECUTION
|
| 286 |
+
# ============================================
|
| 287 |
+
|
| 288 |
+
try {
|
| 289 |
+
Write-ColorOutput "`n=========================================" "Blue"
|
| 290 |
+
Write-ColorOutput " Go Middleware Provider Generator v1.0" "Blue"
|
| 291 |
+
Write-ColorOutput "=========================================`n" "Blue"
|
| 292 |
+
|
| 293 |
+
# Step 1: Parse all middleware constructors
|
| 294 |
+
$middlewares = Parse-GoFiles -Directory $MiddlewareDir
|
| 295 |
+
|
| 296 |
+
# Step 2: Generate provider code
|
| 297 |
+
$code = Generate-ProviderCode -Middlewares $middlewares
|
| 298 |
+
|
| 299 |
+
# Step 3: Write to file
|
| 300 |
+
Write-ProviderFile -Code $code -OutputPath $OutputFile
|
| 301 |
+
|
| 302 |
+
Write-ColorOutput "`nSUCCESS! Middleware provider generated successfully.`n" "Green"
|
| 303 |
+
Write-ColorOutput "Next steps:" "Cyan"
|
| 304 |
+
Write-ColorOutput " 1. Review $OutputFile" "White"
|
| 305 |
+
Write-ColorOutput " 2. Fill any /* TODO: provide ... */ placeholders" "White"
|
| 306 |
+
Write-ColorOutput " 3. Run: go build ./provider" "White"
|
| 307 |
+
|
| 308 |
+
} catch {
|
| 309 |
+
Write-ColorOutput "`nERROR: $($_.Exception.Message)" "Red"
|
| 310 |
+
Write-ColorOutput "Stack trace: $($_.ScriptStackTrace)" "Yellow"
|
| 311 |
+
exit 1
|
| 312 |
}
|
do_inject_repository.ps1
CHANGED
|
@@ -1,327 +1,327 @@
|
|
| 1 |
-
#Requires -Version 5.1
|
| 2 |
-
<#
|
| 3 |
-
.SYNOPSIS
|
| 4 |
-
Automatic Dependency Injection Generator for Go Repositories
|
| 5 |
-
|
| 6 |
-
.DESCRIPTION
|
| 7 |
-
Scans ./repositories/ directory, discovers all repository constructors,
|
| 8 |
-
and generates provider/repositories_provider.go with full DI wiring.
|
| 9 |
-
Repositories typically depend on database connection from ConfigProvider.
|
| 10 |
-
|
| 11 |
-
.EXAMPLE
|
| 12 |
-
.\repository_injector.ps1
|
| 13 |
-
|
| 14 |
-
.NOTES
|
| 15 |
-
- Works with PowerShell 5.1+ and PowerShell 7+
|
| 16 |
-
- No external dependencies required
|
| 17 |
-
- Supports multi-line constructor signatures
|
| 18 |
-
- Repositories depend on database instance from ConfigProvider
|
| 19 |
-
#>
|
| 20 |
-
|
| 21 |
-
[CmdletBinding()]
|
| 22 |
-
param()
|
| 23 |
-
|
| 24 |
-
# Configuration
|
| 25 |
-
$RepositoriesDir = "./repositories"
|
| 26 |
-
$OutputFile = "provider/repositories_provider.go"
|
| 27 |
-
$ModulePath = "abdanhafidz.com/go-boilerplate/repositories"
|
| 28 |
-
|
| 29 |
-
# ANSI colors for better output
|
| 30 |
-
$script:UseColors = $Host.UI.SupportsVirtualTerminal
|
| 31 |
-
function Write-ColorOutput {
|
| 32 |
-
param([string]$Message, [string]$Color = "White")
|
| 33 |
-
if ($script:UseColors) {
|
| 34 |
-
$colors = @{
|
| 35 |
-
"Green" = "`e[32m"; "Yellow" = "`e[33m"; "Red" = "`e[31m"
|
| 36 |
-
"Cyan" = "`e[36m"; "Blue" = "`e[34m"; "Reset" = "`e[0m"
|
| 37 |
-
}
|
| 38 |
-
Write-Host "$($colors[$Color])$Message$($colors['Reset'])"
|
| 39 |
-
} else {
|
| 40 |
-
Write-Host $Message
|
| 41 |
-
}
|
| 42 |
-
}
|
| 43 |
-
|
| 44 |
-
# Data structures
|
| 45 |
-
class RepositoryInfo {
|
| 46 |
-
[string]$ConstructorName # NewAccountRepository
|
| 47 |
-
[string]$Domain # AccountRepository
|
| 48 |
-
[string]$VarName # accountRepository
|
| 49 |
-
[System.Collections.Generic.List[Parameter]]$Parameters
|
| 50 |
-
|
| 51 |
-
RepositoryInfo() {
|
| 52 |
-
$this.Parameters = [System.Collections.Generic.List[Parameter]]::new()
|
| 53 |
-
}
|
| 54 |
-
}
|
| 55 |
-
|
| 56 |
-
class Parameter {
|
| 57 |
-
[string]$Name
|
| 58 |
-
[string]$RawType
|
| 59 |
-
[string]$NormalizedType
|
| 60 |
-
}
|
| 61 |
-
|
| 62 |
-
function Get-LowerCamelCase {
|
| 63 |
-
param([string]$Text)
|
| 64 |
-
if ($Text.Length -eq 0) { return $Text }
|
| 65 |
-
return $Text.Substring(0, 1).ToLower() + $Text.Substring(1)
|
| 66 |
-
}
|
| 67 |
-
|
| 68 |
-
function Normalize-TypeName {
|
| 69 |
-
param([string]$TypeStr)
|
| 70 |
-
|
| 71 |
-
# Remove leading pointer
|
| 72 |
-
$cleaned = $TypeStr -replace '^\*+', ''
|
| 73 |
-
|
| 74 |
-
# Remove package prefix (everything before last dot)
|
| 75 |
-
if ($cleaned -match '\.([^.]+)$') {
|
| 76 |
-
$cleaned = $matches[1]
|
| 77 |
-
}
|
| 78 |
-
|
| 79 |
-
return $cleaned.Trim()
|
| 80 |
-
}
|
| 81 |
-
|
| 82 |
-
function Parse-GoFiles {
|
| 83 |
-
param([string]$Directory)
|
| 84 |
-
|
| 85 |
-
Write-ColorOutput "Scanning for repository constructors in $Directory..." "Cyan"
|
| 86 |
-
|
| 87 |
-
if (-not (Test-Path $Directory)) {
|
| 88 |
-
Write-ColorOutput "ERROR: Directory '$Directory' not found!" "Red"
|
| 89 |
-
exit 1
|
| 90 |
-
}
|
| 91 |
-
|
| 92 |
-
$goFiles = Get-ChildItem -Path $Directory -Filter "*.go" -Recurse -File
|
| 93 |
-
$repositories = [System.Collections.Generic.List[RepositoryInfo]]::new()
|
| 94 |
-
|
| 95 |
-
foreach ($file in $goFiles) {
|
| 96 |
-
$content = Get-Content $file.FullName -Raw
|
| 97 |
-
|
| 98 |
-
# Match function signatures (support multi-line)
|
| 99 |
-
# Pattern: func NewXxxRepository(...) XxxRepository
|
| 100 |
-
$pattern = '(?ms)func\s+(New[a-zA-Z0-9]+Repository)\s*\(([^)]*)\)\s+([a-zA-Z0-9*_.]+Repository)'
|
| 101 |
-
$matches = [regex]::Matches($content, $pattern)
|
| 102 |
-
|
| 103 |
-
foreach ($match in $matches) {
|
| 104 |
-
$constructorName = $match.Groups[1].Value
|
| 105 |
-
$paramsStr = $match.Groups[2].Value
|
| 106 |
-
$returnType = $match.Groups[3].Value
|
| 107 |
-
|
| 108 |
-
# Extract domain name (XxxRepository)
|
| 109 |
-
$domain = Normalize-TypeName $returnType
|
| 110 |
-
$varName = Get-LowerCamelCase $domain
|
| 111 |
-
|
| 112 |
-
$repo = [RepositoryInfo]::new()
|
| 113 |
-
$repo.ConstructorName = $constructorName
|
| 114 |
-
$repo.Domain = $domain
|
| 115 |
-
$repo.VarName = $varName
|
| 116 |
-
|
| 117 |
-
# Parse parameters
|
| 118 |
-
if ($paramsStr.Trim() -ne "") {
|
| 119 |
-
# Split by comma, but be careful with nested types
|
| 120 |
-
$paramList = $paramsStr -split ',\s*(?![^<>]*>)'
|
| 121 |
-
|
| 122 |
-
foreach ($param in $paramList) {
|
| 123 |
-
$param = $param.Trim()
|
| 124 |
-
if ($param -eq "") { continue }
|
| 125 |
-
|
| 126 |
-
# Split into name and type
|
| 127 |
-
$parts = $param -split '\s+', 2
|
| 128 |
-
|
| 129 |
-
$p = [Parameter]::new()
|
| 130 |
-
if ($parts.Count -eq 2) {
|
| 131 |
-
$p.Name = $parts[0]
|
| 132 |
-
$p.RawType = $parts[1]
|
| 133 |
-
} elseif ($parts.Count -eq 1) {
|
| 134 |
-
# Anonymous parameter - synthesize name
|
| 135 |
-
$p.Name = "param$($repo.Parameters.Count)"
|
| 136 |
-
$p.RawType = $parts[0]
|
| 137 |
-
} else {
|
| 138 |
-
continue
|
| 139 |
-
}
|
| 140 |
-
|
| 141 |
-
$p.NormalizedType = Normalize-TypeName $p.RawType
|
| 142 |
-
$repo.Parameters.Add($p)
|
| 143 |
-
}
|
| 144 |
-
}
|
| 145 |
-
|
| 146 |
-
$repositories.Add($repo)
|
| 147 |
-
Write-ColorOutput " Found: $constructorName" "Green"
|
| 148 |
-
}
|
| 149 |
-
}
|
| 150 |
-
|
| 151 |
-
if ($repositories.Count -eq 0) {
|
| 152 |
-
Write-ColorOutput "No repository constructors found matching pattern 'NewXxxRepository'!" "Red"
|
| 153 |
-
exit 1
|
| 154 |
-
}
|
| 155 |
-
|
| 156 |
-
Write-ColorOutput "`nTotal repositories discovered: $($repositories.Count)" "Blue"
|
| 157 |
-
return $repositories
|
| 158 |
-
}
|
| 159 |
-
|
| 160 |
-
function Resolve-RepositoryArgument {
|
| 161 |
-
param([Parameter]$Param)
|
| 162 |
-
|
| 163 |
-
$type = $Param.NormalizedType
|
| 164 |
-
$paramName = $Param.Name
|
| 165 |
-
|
| 166 |
-
# DEPENDENCY RESOLUTION RULES FOR REPOSITORIES
|
| 167 |
-
# ============================================
|
| 168 |
-
|
| 169 |
-
# 1. Database connection patterns
|
| 170 |
-
if ($type -match '^(DB|Database|Gorm|SqlDB|Connection)$' -or $paramName -match '^(db|database|conn|connection)$') {
|
| 171 |
-
return "db"
|
| 172 |
-
}
|
| 173 |
-
|
| 174 |
-
# 2. *gorm.DB (most common in Go GORM projects)
|
| 175 |
-
if ($Param.RawType -match 'gorm\.DB' -or $type -eq "DB") {
|
| 176 |
-
return "db"
|
| 177 |
-
}
|
| 178 |
-
|
| 179 |
-
# 3. *sql.DB (standard library)
|
| 180 |
-
if ($Param.RawType -match 'sql\.DB') {
|
| 181 |
-
return "db"
|
| 182 |
-
}
|
| 183 |
-
|
| 184 |
-
# ADD MORE SPECIAL CASES HERE:
|
| 185 |
-
# --------------------------------------------
|
| 186 |
-
# Example: Redis connection
|
| 187 |
-
# if ($type -eq "RedisClient" -or $paramName -match "redis") {
|
| 188 |
-
# return "redisClient"
|
| 189 |
-
# }
|
| 190 |
-
#
|
| 191 |
-
# Example: MongoDB connection
|
| 192 |
-
# if ($type -eq "MongoClient" -or $paramName -match "mongo") {
|
| 193 |
-
# return "mongoClient"
|
| 194 |
-
# }
|
| 195 |
-
#
|
| 196 |
-
# Example: Cache
|
| 197 |
-
# if ($type -eq "Cache" -or $paramName -match "cache") {
|
| 198 |
-
# return "cache"
|
| 199 |
-
# }
|
| 200 |
-
#
|
| 201 |
-
# Example: Logger
|
| 202 |
-
# if ($type -eq "Logger" -or $paramName -match "logger") {
|
| 203 |
-
# return "logger"
|
| 204 |
-
# }
|
| 205 |
-
# --------------------------------------------
|
| 206 |
-
|
| 207 |
-
# 4. Fallback: unresolved type
|
| 208 |
-
return "/* TODO: provide $($Param.RawType) */"
|
| 209 |
-
}
|
| 210 |
-
|
| 211 |
-
function Generate-ProviderCode {
|
| 212 |
-
param([System.Collections.Generic.List[RepositoryInfo]]$Repositories)
|
| 213 |
-
|
| 214 |
-
Write-ColorOutput "`nGenerating repositories provider code..." "Cyan"
|
| 215 |
-
|
| 216 |
-
# Sort repositories alphabetically for consistent output
|
| 217 |
-
$sortedRepos = $Repositories | Sort-Object -Property Domain
|
| 218 |
-
|
| 219 |
-
$sb = [System.Text.StringBuilder]::new()
|
| 220 |
-
[void]$sb.AppendLine("package provider")
|
| 221 |
-
[void]$sb.AppendLine()
|
| 222 |
-
[void]$sb.AppendLine("import `"$ModulePath`"")
|
| 223 |
-
[void]$sb.AppendLine()
|
| 224 |
-
|
| 225 |
-
# Interface
|
| 226 |
-
[void]$sb.AppendLine("type RepositoriesProvider interface {")
|
| 227 |
-
foreach ($repo in $sortedRepos) {
|
| 228 |
-
$line = "`tProvide$($repo.Domain)() repositories.$($repo.Domain)"
|
| 229 |
-
[void]$sb.AppendLine($line)
|
| 230 |
-
}
|
| 231 |
-
[void]$sb.AppendLine("}")
|
| 232 |
-
[void]$sb.AppendLine()
|
| 233 |
-
|
| 234 |
-
# Struct
|
| 235 |
-
[void]$sb.AppendLine("type repositoriesProvider struct {")
|
| 236 |
-
foreach ($repo in $sortedRepos) {
|
| 237 |
-
$line = "`t$($repo.VarName) repositories.$($repo.Domain)"
|
| 238 |
-
[void]$sb.AppendLine($line)
|
| 239 |
-
}
|
| 240 |
-
[void]$sb.AppendLine("}")
|
| 241 |
-
[void]$sb.AppendLine()
|
| 242 |
-
|
| 243 |
-
# Constructor
|
| 244 |
-
[void]$sb.AppendLine("func NewRepositoriesProvider(cfg ConfigProvider) RepositoriesProvider {")
|
| 245 |
-
[void]$sb.AppendLine("`tdbConfig := cfg.ProvideDatabaseConfig()")
|
| 246 |
-
[void]$sb.AppendLine("`tdb := dbConfig.GetInstance()")
|
| 247 |
-
[void]$sb.AppendLine()
|
| 248 |
-
|
| 249 |
-
# Initialize repositories
|
| 250 |
-
foreach ($repo in $sortedRepos) {
|
| 251 |
-
$args = @()
|
| 252 |
-
foreach ($param in $repo.Parameters) {
|
| 253 |
-
$args += Resolve-RepositoryArgument $param
|
| 254 |
-
}
|
| 255 |
-
$argsStr = $args -join ", "
|
| 256 |
-
$line = "`t$($repo.VarName) := repositories.$($repo.ConstructorName)($argsStr)"
|
| 257 |
-
[void]$sb.AppendLine($line)
|
| 258 |
-
}
|
| 259 |
-
|
| 260 |
-
[void]$sb.AppendLine()
|
| 261 |
-
[void]$sb.AppendLine("`treturn &repositoriesProvider{")
|
| 262 |
-
foreach ($repo in $sortedRepos) {
|
| 263 |
-
$line = "`t`t$($repo.VarName): $($repo.VarName),"
|
| 264 |
-
[void]$sb.AppendLine($line)
|
| 265 |
-
}
|
| 266 |
-
[void]$sb.AppendLine("`t}")
|
| 267 |
-
[void]$sb.AppendLine("}")
|
| 268 |
-
[void]$sb.AppendLine()
|
| 269 |
-
|
| 270 |
-
# Getter methods
|
| 271 |
-
foreach ($repo in $sortedRepos) {
|
| 272 |
-
[void]$sb.AppendLine("func (r *repositoriesProvider) Provide$($repo.Domain)() repositories.$($repo.Domain) {")
|
| 273 |
-
[void]$sb.AppendLine("`treturn r.$($repo.VarName)")
|
| 274 |
-
[void]$sb.AppendLine("}")
|
| 275 |
-
[void]$sb.AppendLine()
|
| 276 |
-
}
|
| 277 |
-
|
| 278 |
-
return $sb.ToString()
|
| 279 |
-
}
|
| 280 |
-
|
| 281 |
-
function Write-ProviderFile {
|
| 282 |
-
param([string]$Code, [string]$OutputPath)
|
| 283 |
-
|
| 284 |
-
Write-ColorOutput "Writing to $OutputPath..." "Cyan"
|
| 285 |
-
|
| 286 |
-
# Ensure directory exists
|
| 287 |
-
$dir = Split-Path $OutputPath -Parent
|
| 288 |
-
if ($dir -and -not (Test-Path $dir)) {
|
| 289 |
-
New-Item -ItemType Directory -Path $dir -Force | Out-Null
|
| 290 |
-
}
|
| 291 |
-
|
| 292 |
-
# Write file as UTF-8 without BOM
|
| 293 |
-
$utf8NoBom = [System.Text.UTF8Encoding]::new($false)
|
| 294 |
-
[System.IO.File]::WriteAllText($OutputPath, $Code, $utf8NoBom)
|
| 295 |
-
|
| 296 |
-
Write-ColorOutput " Successfully generated $OutputPath" "Green"
|
| 297 |
-
}
|
| 298 |
-
|
| 299 |
-
# ============================================
|
| 300 |
-
# MAIN EXECUTION
|
| 301 |
-
# ============================================
|
| 302 |
-
|
| 303 |
-
try {
|
| 304 |
-
Write-ColorOutput "`n=========================================" "Blue"
|
| 305 |
-
Write-ColorOutput " Go Repository Provider Generator v1.0" "Blue"
|
| 306 |
-
Write-ColorOutput "=========================================`n" "Blue"
|
| 307 |
-
|
| 308 |
-
# Step 1: Parse all repository constructors
|
| 309 |
-
$repositories = Parse-GoFiles -Directory $RepositoriesDir
|
| 310 |
-
|
| 311 |
-
# Step 2: Generate provider code
|
| 312 |
-
$code = Generate-ProviderCode -Repositories $repositories
|
| 313 |
-
|
| 314 |
-
# Step 3: Write to file
|
| 315 |
-
Write-ProviderFile -Code $code -OutputPath $OutputFile
|
| 316 |
-
|
| 317 |
-
Write-ColorOutput "`nSUCCESS! Repositories provider generated successfully.`n" "Green"
|
| 318 |
-
Write-ColorOutput "Next steps:" "Cyan"
|
| 319 |
-
Write-ColorOutput " 1. Review $OutputFile" "White"
|
| 320 |
-
Write-ColorOutput " 2. Fill any /* TODO: provide ... */ placeholders" "White"
|
| 321 |
-
Write-ColorOutput " 3. Run: go build ./provider" "White"
|
| 322 |
-
|
| 323 |
-
} catch {
|
| 324 |
-
Write-ColorOutput "`nERROR: $($_.Exception.Message)" "Red"
|
| 325 |
-
Write-ColorOutput "Stack trace: $($_.ScriptStackTrace)" "Yellow"
|
| 326 |
-
exit 1
|
| 327 |
}
|
|
|
|
| 1 |
+
#Requires -Version 5.1
|
| 2 |
+
<#
|
| 3 |
+
.SYNOPSIS
|
| 4 |
+
Automatic Dependency Injection Generator for Go Repositories
|
| 5 |
+
|
| 6 |
+
.DESCRIPTION
|
| 7 |
+
Scans ./repositories/ directory, discovers all repository constructors,
|
| 8 |
+
and generates provider/repositories_provider.go with full DI wiring.
|
| 9 |
+
Repositories typically depend on database connection from ConfigProvider.
|
| 10 |
+
|
| 11 |
+
.EXAMPLE
|
| 12 |
+
.\repository_injector.ps1
|
| 13 |
+
|
| 14 |
+
.NOTES
|
| 15 |
+
- Works with PowerShell 5.1+ and PowerShell 7+
|
| 16 |
+
- No external dependencies required
|
| 17 |
+
- Supports multi-line constructor signatures
|
| 18 |
+
- Repositories depend on database instance from ConfigProvider
|
| 19 |
+
#>
|
| 20 |
+
|
| 21 |
+
[CmdletBinding()]
|
| 22 |
+
param()
|
| 23 |
+
|
| 24 |
+
# Configuration
|
| 25 |
+
$RepositoriesDir = "./repositories"
|
| 26 |
+
$OutputFile = "provider/repositories_provider.go"
|
| 27 |
+
$ModulePath = "abdanhafidz.com/go-boilerplate/repositories"
|
| 28 |
+
|
| 29 |
+
# ANSI colors for better output
|
| 30 |
+
$script:UseColors = $Host.UI.SupportsVirtualTerminal
|
| 31 |
+
function Write-ColorOutput {
|
| 32 |
+
param([string]$Message, [string]$Color = "White")
|
| 33 |
+
if ($script:UseColors) {
|
| 34 |
+
$colors = @{
|
| 35 |
+
"Green" = "`e[32m"; "Yellow" = "`e[33m"; "Red" = "`e[31m"
|
| 36 |
+
"Cyan" = "`e[36m"; "Blue" = "`e[34m"; "Reset" = "`e[0m"
|
| 37 |
+
}
|
| 38 |
+
Write-Host "$($colors[$Color])$Message$($colors['Reset'])"
|
| 39 |
+
} else {
|
| 40 |
+
Write-Host $Message
|
| 41 |
+
}
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
# Data structures
|
| 45 |
+
class RepositoryInfo {
|
| 46 |
+
[string]$ConstructorName # NewAccountRepository
|
| 47 |
+
[string]$Domain # AccountRepository
|
| 48 |
+
[string]$VarName # accountRepository
|
| 49 |
+
[System.Collections.Generic.List[Parameter]]$Parameters
|
| 50 |
+
|
| 51 |
+
RepositoryInfo() {
|
| 52 |
+
$this.Parameters = [System.Collections.Generic.List[Parameter]]::new()
|
| 53 |
+
}
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
class Parameter {
|
| 57 |
+
[string]$Name
|
| 58 |
+
[string]$RawType
|
| 59 |
+
[string]$NormalizedType
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
function Get-LowerCamelCase {
|
| 63 |
+
param([string]$Text)
|
| 64 |
+
if ($Text.Length -eq 0) { return $Text }
|
| 65 |
+
return $Text.Substring(0, 1).ToLower() + $Text.Substring(1)
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
function Normalize-TypeName {
|
| 69 |
+
param([string]$TypeStr)
|
| 70 |
+
|
| 71 |
+
# Remove leading pointer
|
| 72 |
+
$cleaned = $TypeStr -replace '^\*+', ''
|
| 73 |
+
|
| 74 |
+
# Remove package prefix (everything before last dot)
|
| 75 |
+
if ($cleaned -match '\.([^.]+)$') {
|
| 76 |
+
$cleaned = $matches[1]
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
return $cleaned.Trim()
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
function Parse-GoFiles {
|
| 83 |
+
param([string]$Directory)
|
| 84 |
+
|
| 85 |
+
Write-ColorOutput "Scanning for repository constructors in $Directory..." "Cyan"
|
| 86 |
+
|
| 87 |
+
if (-not (Test-Path $Directory)) {
|
| 88 |
+
Write-ColorOutput "ERROR: Directory '$Directory' not found!" "Red"
|
| 89 |
+
exit 1
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
$goFiles = Get-ChildItem -Path $Directory -Filter "*.go" -Recurse -File
|
| 93 |
+
$repositories = [System.Collections.Generic.List[RepositoryInfo]]::new()
|
| 94 |
+
|
| 95 |
+
foreach ($file in $goFiles) {
|
| 96 |
+
$content = Get-Content $file.FullName -Raw
|
| 97 |
+
|
| 98 |
+
# Match function signatures (support multi-line)
|
| 99 |
+
# Pattern: func NewXxxRepository(...) XxxRepository
|
| 100 |
+
$pattern = '(?ms)func\s+(New[a-zA-Z0-9]+Repository)\s*\(([^)]*)\)\s+([a-zA-Z0-9*_.]+Repository)'
|
| 101 |
+
$matches = [regex]::Matches($content, $pattern)
|
| 102 |
+
|
| 103 |
+
foreach ($match in $matches) {
|
| 104 |
+
$constructorName = $match.Groups[1].Value
|
| 105 |
+
$paramsStr = $match.Groups[2].Value
|
| 106 |
+
$returnType = $match.Groups[3].Value
|
| 107 |
+
|
| 108 |
+
# Extract domain name (XxxRepository)
|
| 109 |
+
$domain = Normalize-TypeName $returnType
|
| 110 |
+
$varName = Get-LowerCamelCase $domain
|
| 111 |
+
|
| 112 |
+
$repo = [RepositoryInfo]::new()
|
| 113 |
+
$repo.ConstructorName = $constructorName
|
| 114 |
+
$repo.Domain = $domain
|
| 115 |
+
$repo.VarName = $varName
|
| 116 |
+
|
| 117 |
+
# Parse parameters
|
| 118 |
+
if ($paramsStr.Trim() -ne "") {
|
| 119 |
+
# Split by comma, but be careful with nested types
|
| 120 |
+
$paramList = $paramsStr -split ',\s*(?![^<>]*>)'
|
| 121 |
+
|
| 122 |
+
foreach ($param in $paramList) {
|
| 123 |
+
$param = $param.Trim()
|
| 124 |
+
if ($param -eq "") { continue }
|
| 125 |
+
|
| 126 |
+
# Split into name and type
|
| 127 |
+
$parts = $param -split '\s+', 2
|
| 128 |
+
|
| 129 |
+
$p = [Parameter]::new()
|
| 130 |
+
if ($parts.Count -eq 2) {
|
| 131 |
+
$p.Name = $parts[0]
|
| 132 |
+
$p.RawType = $parts[1]
|
| 133 |
+
} elseif ($parts.Count -eq 1) {
|
| 134 |
+
# Anonymous parameter - synthesize name
|
| 135 |
+
$p.Name = "param$($repo.Parameters.Count)"
|
| 136 |
+
$p.RawType = $parts[0]
|
| 137 |
+
} else {
|
| 138 |
+
continue
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
$p.NormalizedType = Normalize-TypeName $p.RawType
|
| 142 |
+
$repo.Parameters.Add($p)
|
| 143 |
+
}
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
$repositories.Add($repo)
|
| 147 |
+
Write-ColorOutput " Found: $constructorName" "Green"
|
| 148 |
+
}
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
if ($repositories.Count -eq 0) {
|
| 152 |
+
Write-ColorOutput "No repository constructors found matching pattern 'NewXxxRepository'!" "Red"
|
| 153 |
+
exit 1
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
Write-ColorOutput "`nTotal repositories discovered: $($repositories.Count)" "Blue"
|
| 157 |
+
return $repositories
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
function Resolve-RepositoryArgument {
|
| 161 |
+
param([Parameter]$Param)
|
| 162 |
+
|
| 163 |
+
$type = $Param.NormalizedType
|
| 164 |
+
$paramName = $Param.Name
|
| 165 |
+
|
| 166 |
+
# DEPENDENCY RESOLUTION RULES FOR REPOSITORIES
|
| 167 |
+
# ============================================
|
| 168 |
+
|
| 169 |
+
# 1. Database connection patterns
|
| 170 |
+
if ($type -match '^(DB|Database|Gorm|SqlDB|Connection)$' -or $paramName -match '^(db|database|conn|connection)$') {
|
| 171 |
+
return "db"
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
# 2. *gorm.DB (most common in Go GORM projects)
|
| 175 |
+
if ($Param.RawType -match 'gorm\.DB' -or $type -eq "DB") {
|
| 176 |
+
return "db"
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
# 3. *sql.DB (standard library)
|
| 180 |
+
if ($Param.RawType -match 'sql\.DB') {
|
| 181 |
+
return "db"
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
# ADD MORE SPECIAL CASES HERE:
|
| 185 |
+
# --------------------------------------------
|
| 186 |
+
# Example: Redis connection
|
| 187 |
+
# if ($type -eq "RedisClient" -or $paramName -match "redis") {
|
| 188 |
+
# return "redisClient"
|
| 189 |
+
# }
|
| 190 |
+
#
|
| 191 |
+
# Example: MongoDB connection
|
| 192 |
+
# if ($type -eq "MongoClient" -or $paramName -match "mongo") {
|
| 193 |
+
# return "mongoClient"
|
| 194 |
+
# }
|
| 195 |
+
#
|
| 196 |
+
# Example: Cache
|
| 197 |
+
# if ($type -eq "Cache" -or $paramName -match "cache") {
|
| 198 |
+
# return "cache"
|
| 199 |
+
# }
|
| 200 |
+
#
|
| 201 |
+
# Example: Logger
|
| 202 |
+
# if ($type -eq "Logger" -or $paramName -match "logger") {
|
| 203 |
+
# return "logger"
|
| 204 |
+
# }
|
| 205 |
+
# --------------------------------------------
|
| 206 |
+
|
| 207 |
+
# 4. Fallback: unresolved type
|
| 208 |
+
return "/* TODO: provide $($Param.RawType) */"
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
function Generate-ProviderCode {
|
| 212 |
+
param([System.Collections.Generic.List[RepositoryInfo]]$Repositories)
|
| 213 |
+
|
| 214 |
+
Write-ColorOutput "`nGenerating repositories provider code..." "Cyan"
|
| 215 |
+
|
| 216 |
+
# Sort repositories alphabetically for consistent output
|
| 217 |
+
$sortedRepos = $Repositories | Sort-Object -Property Domain
|
| 218 |
+
|
| 219 |
+
$sb = [System.Text.StringBuilder]::new()
|
| 220 |
+
[void]$sb.AppendLine("package provider")
|
| 221 |
+
[void]$sb.AppendLine()
|
| 222 |
+
[void]$sb.AppendLine("import `"$ModulePath`"")
|
| 223 |
+
[void]$sb.AppendLine()
|
| 224 |
+
|
| 225 |
+
# Interface
|
| 226 |
+
[void]$sb.AppendLine("type RepositoriesProvider interface {")
|
| 227 |
+
foreach ($repo in $sortedRepos) {
|
| 228 |
+
$line = "`tProvide$($repo.Domain)() repositories.$($repo.Domain)"
|
| 229 |
+
[void]$sb.AppendLine($line)
|
| 230 |
+
}
|
| 231 |
+
[void]$sb.AppendLine("}")
|
| 232 |
+
[void]$sb.AppendLine()
|
| 233 |
+
|
| 234 |
+
# Struct
|
| 235 |
+
[void]$sb.AppendLine("type repositoriesProvider struct {")
|
| 236 |
+
foreach ($repo in $sortedRepos) {
|
| 237 |
+
$line = "`t$($repo.VarName) repositories.$($repo.Domain)"
|
| 238 |
+
[void]$sb.AppendLine($line)
|
| 239 |
+
}
|
| 240 |
+
[void]$sb.AppendLine("}")
|
| 241 |
+
[void]$sb.AppendLine()
|
| 242 |
+
|
| 243 |
+
# Constructor
|
| 244 |
+
[void]$sb.AppendLine("func NewRepositoriesProvider(cfg ConfigProvider) RepositoriesProvider {")
|
| 245 |
+
[void]$sb.AppendLine("`tdbConfig := cfg.ProvideDatabaseConfig()")
|
| 246 |
+
[void]$sb.AppendLine("`tdb := dbConfig.GetInstance()")
|
| 247 |
+
[void]$sb.AppendLine()
|
| 248 |
+
|
| 249 |
+
# Initialize repositories
|
| 250 |
+
foreach ($repo in $sortedRepos) {
|
| 251 |
+
$args = @()
|
| 252 |
+
foreach ($param in $repo.Parameters) {
|
| 253 |
+
$args += Resolve-RepositoryArgument $param
|
| 254 |
+
}
|
| 255 |
+
$argsStr = $args -join ", "
|
| 256 |
+
$line = "`t$($repo.VarName) := repositories.$($repo.ConstructorName)($argsStr)"
|
| 257 |
+
[void]$sb.AppendLine($line)
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
[void]$sb.AppendLine()
|
| 261 |
+
[void]$sb.AppendLine("`treturn &repositoriesProvider{")
|
| 262 |
+
foreach ($repo in $sortedRepos) {
|
| 263 |
+
$line = "`t`t$($repo.VarName): $($repo.VarName),"
|
| 264 |
+
[void]$sb.AppendLine($line)
|
| 265 |
+
}
|
| 266 |
+
[void]$sb.AppendLine("`t}")
|
| 267 |
+
[void]$sb.AppendLine("}")
|
| 268 |
+
[void]$sb.AppendLine()
|
| 269 |
+
|
| 270 |
+
# Getter methods
|
| 271 |
+
foreach ($repo in $sortedRepos) {
|
| 272 |
+
[void]$sb.AppendLine("func (r *repositoriesProvider) Provide$($repo.Domain)() repositories.$($repo.Domain) {")
|
| 273 |
+
[void]$sb.AppendLine("`treturn r.$($repo.VarName)")
|
| 274 |
+
[void]$sb.AppendLine("}")
|
| 275 |
+
[void]$sb.AppendLine()
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
return $sb.ToString()
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
function Write-ProviderFile {
|
| 282 |
+
param([string]$Code, [string]$OutputPath)
|
| 283 |
+
|
| 284 |
+
Write-ColorOutput "Writing to $OutputPath..." "Cyan"
|
| 285 |
+
|
| 286 |
+
# Ensure directory exists
|
| 287 |
+
$dir = Split-Path $OutputPath -Parent
|
| 288 |
+
if ($dir -and -not (Test-Path $dir)) {
|
| 289 |
+
New-Item -ItemType Directory -Path $dir -Force | Out-Null
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
# Write file as UTF-8 without BOM
|
| 293 |
+
$utf8NoBom = [System.Text.UTF8Encoding]::new($false)
|
| 294 |
+
[System.IO.File]::WriteAllText($OutputPath, $Code, $utf8NoBom)
|
| 295 |
+
|
| 296 |
+
Write-ColorOutput " Successfully generated $OutputPath" "Green"
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
# ============================================
|
| 300 |
+
# MAIN EXECUTION
|
| 301 |
+
# ============================================
|
| 302 |
+
|
| 303 |
+
try {
|
| 304 |
+
Write-ColorOutput "`n=========================================" "Blue"
|
| 305 |
+
Write-ColorOutput " Go Repository Provider Generator v1.0" "Blue"
|
| 306 |
+
Write-ColorOutput "=========================================`n" "Blue"
|
| 307 |
+
|
| 308 |
+
# Step 1: Parse all repository constructors
|
| 309 |
+
$repositories = Parse-GoFiles -Directory $RepositoriesDir
|
| 310 |
+
|
| 311 |
+
# Step 2: Generate provider code
|
| 312 |
+
$code = Generate-ProviderCode -Repositories $repositories
|
| 313 |
+
|
| 314 |
+
# Step 3: Write to file
|
| 315 |
+
Write-ProviderFile -Code $code -OutputPath $OutputFile
|
| 316 |
+
|
| 317 |
+
Write-ColorOutput "`nSUCCESS! Repositories provider generated successfully.`n" "Green"
|
| 318 |
+
Write-ColorOutput "Next steps:" "Cyan"
|
| 319 |
+
Write-ColorOutput " 1. Review $OutputFile" "White"
|
| 320 |
+
Write-ColorOutput " 2. Fill any /* TODO: provide ... */ placeholders" "White"
|
| 321 |
+
Write-ColorOutput " 3. Run: go build ./provider" "White"
|
| 322 |
+
|
| 323 |
+
} catch {
|
| 324 |
+
Write-ColorOutput "`nERROR: $($_.Exception.Message)" "Red"
|
| 325 |
+
Write-ColorOutput "Stack trace: $($_.ScriptStackTrace)" "Yellow"
|
| 326 |
+
exit 1
|
| 327 |
}
|
do_inject_services.ps1
CHANGED
|
@@ -1,383 +1,383 @@
|
|
| 1 |
-
#Requires -Version 5.1
|
| 2 |
-
<#
|
| 3 |
-
.SYNOPSIS
|
| 4 |
-
Automatic Dependency Injection Generator for Go Services
|
| 5 |
-
|
| 6 |
-
.DESCRIPTION
|
| 7 |
-
Scans ./services/ directory, discovers service constructors, resolves dependencies,
|
| 8 |
-
performs topological sorting, and generates provider/services_provider.go with full DI wiring.
|
| 9 |
-
|
| 10 |
-
.EXAMPLE
|
| 11 |
-
.\service_injector.ps1
|
| 12 |
-
|
| 13 |
-
.NOTES
|
| 14 |
-
- Works with PowerShell 5.1+ and PowerShell 7+
|
| 15 |
-
- No external dependencies required
|
| 16 |
-
- Supports multi-line constructor signatures
|
| 17 |
-
- Handles dependency cycles with clear error messages
|
| 18 |
-
#>
|
| 19 |
-
|
| 20 |
-
[CmdletBinding()]
|
| 21 |
-
param()
|
| 22 |
-
|
| 23 |
-
# Configuration
|
| 24 |
-
$ServicesDir = "./services"
|
| 25 |
-
$OutputFile = "provider/services_provider.go"
|
| 26 |
-
$ModulePath = "abdanhafidz.com/go-boilerplate/services"
|
| 27 |
-
|
| 28 |
-
# ANSI colors for better output (fallback to plain text if not supported)
|
| 29 |
-
$script:UseColors = $Host.UI.SupportsVirtualTerminal
|
| 30 |
-
function Write-ColorOutput {
|
| 31 |
-
param([string]$Message, [string]$Color = "White")
|
| 32 |
-
if ($script:UseColors) {
|
| 33 |
-
$colors = @{
|
| 34 |
-
"Green" = "`e[32m"; "Yellow" = "`e[33m"; "Red" = "`e[31m"
|
| 35 |
-
"Cyan" = "`e[36m"; "Blue" = "`e[34m"; "Reset" = "`e[0m"
|
| 36 |
-
}
|
| 37 |
-
Write-Host "$($colors[$Color])$Message$($colors['Reset'])"
|
| 38 |
-
} else {
|
| 39 |
-
Write-Host $Message
|
| 40 |
-
}
|
| 41 |
-
}
|
| 42 |
-
|
| 43 |
-
# Data structures
|
| 44 |
-
class ServiceInfo {
|
| 45 |
-
[string]$ConstructorName # NewAccountService
|
| 46 |
-
[string]$Domain # AccountService
|
| 47 |
-
[string]$VarName # accountService
|
| 48 |
-
[System.Collections.Generic.List[Parameter]]$Parameters
|
| 49 |
-
[System.Collections.Generic.List[string]]$ServiceDependencies
|
| 50 |
-
|
| 51 |
-
ServiceInfo() {
|
| 52 |
-
$this.Parameters = [System.Collections.Generic.List[Parameter]]::new()
|
| 53 |
-
$this.ServiceDependencies = [System.Collections.Generic.List[string]]::new()
|
| 54 |
-
}
|
| 55 |
-
}
|
| 56 |
-
|
| 57 |
-
class Parameter {
|
| 58 |
-
[string]$Name
|
| 59 |
-
[string]$RawType
|
| 60 |
-
[string]$NormalizedType
|
| 61 |
-
}
|
| 62 |
-
|
| 63 |
-
function Get-LowerCamelCase {
|
| 64 |
-
param([string]$Text)
|
| 65 |
-
if ($Text.Length -eq 0) { return $Text }
|
| 66 |
-
return $Text.Substring(0, 1).ToLower() + $Text.Substring(1)
|
| 67 |
-
}
|
| 68 |
-
|
| 69 |
-
function Normalize-TypeName {
|
| 70 |
-
param([string]$TypeStr)
|
| 71 |
-
|
| 72 |
-
# Remove leading pointer
|
| 73 |
-
$cleaned = $TypeStr -replace '^\*+', ''
|
| 74 |
-
|
| 75 |
-
# Remove package prefix (everything before last dot)
|
| 76 |
-
if ($cleaned -match '\.([^.]+)$') {
|
| 77 |
-
$cleaned = $matches[1]
|
| 78 |
-
}
|
| 79 |
-
|
| 80 |
-
return $cleaned.Trim()
|
| 81 |
-
}
|
| 82 |
-
|
| 83 |
-
function Parse-GoFiles {
|
| 84 |
-
param([string]$Directory)
|
| 85 |
-
|
| 86 |
-
Write-ColorOutput "Scanning for service constructors in $Directory..." "Cyan"
|
| 87 |
-
|
| 88 |
-
if (-not (Test-Path $Directory)) {
|
| 89 |
-
Write-ColorOutput "ERROR: Directory '$Directory' not found!" "Red"
|
| 90 |
-
exit 1
|
| 91 |
-
}
|
| 92 |
-
|
| 93 |
-
$goFiles = Get-ChildItem -Path $Directory -Filter "*.go" -Recurse -File
|
| 94 |
-
$services = [System.Collections.Generic.List[ServiceInfo]]::new()
|
| 95 |
-
|
| 96 |
-
foreach ($file in $goFiles) {
|
| 97 |
-
$content = Get-Content $file.FullName -Raw
|
| 98 |
-
|
| 99 |
-
# Match function signatures (support multi-line)
|
| 100 |
-
# Pattern: func NewXxxService(...) XxxService
|
| 101 |
-
$pattern = '(?ms)func\s+(New[a-zA-Z0-9]+Service)\s*\(([^)]*)\)\s+([a-zA-Z0-9*_.]+Service)'
|
| 102 |
-
$matches = [regex]::Matches($content, $pattern)
|
| 103 |
-
|
| 104 |
-
foreach ($match in $matches) {
|
| 105 |
-
$constructorName = $match.Groups[1].Value
|
| 106 |
-
$paramsStr = $match.Groups[2].Value
|
| 107 |
-
$returnType = $match.Groups[3].Value
|
| 108 |
-
|
| 109 |
-
# Extract domain name (XxxService)
|
| 110 |
-
$domain = Normalize-TypeName $returnType
|
| 111 |
-
$varName = Get-LowerCamelCase $domain
|
| 112 |
-
|
| 113 |
-
$service = [ServiceInfo]::new()
|
| 114 |
-
$service.ConstructorName = $constructorName
|
| 115 |
-
$service.Domain = $domain
|
| 116 |
-
$service.VarName = $varName
|
| 117 |
-
|
| 118 |
-
# Parse parameters
|
| 119 |
-
if ($paramsStr.Trim() -ne "") {
|
| 120 |
-
# Split by comma, but be careful with nested types
|
| 121 |
-
$paramList = $paramsStr -split ',\s*(?![^<>]*>)'
|
| 122 |
-
|
| 123 |
-
foreach ($param in $paramList) {
|
| 124 |
-
$param = $param.Trim()
|
| 125 |
-
if ($param -eq "") { continue }
|
| 126 |
-
|
| 127 |
-
# Split into name and type
|
| 128 |
-
$parts = $param -split '\s+', 2
|
| 129 |
-
|
| 130 |
-
$p = [Parameter]::new()
|
| 131 |
-
if ($parts.Count -eq 2) {
|
| 132 |
-
$p.Name = $parts[0]
|
| 133 |
-
$p.RawType = $parts[1]
|
| 134 |
-
} elseif ($parts.Count -eq 1) {
|
| 135 |
-
# Anonymous parameter - synthesize name
|
| 136 |
-
$p.Name = "param$($service.Parameters.Count)"
|
| 137 |
-
$p.RawType = $parts[0]
|
| 138 |
-
} else {
|
| 139 |
-
continue
|
| 140 |
-
}
|
| 141 |
-
|
| 142 |
-
$p.NormalizedType = Normalize-TypeName $p.RawType
|
| 143 |
-
$service.Parameters.Add($p)
|
| 144 |
-
|
| 145 |
-
# Track service dependencies
|
| 146 |
-
if ($p.NormalizedType -match 'Service$') {
|
| 147 |
-
$service.ServiceDependencies.Add($p.NormalizedType)
|
| 148 |
-
}
|
| 149 |
-
}
|
| 150 |
-
}
|
| 151 |
-
|
| 152 |
-
$services.Add($service)
|
| 153 |
-
Write-ColorOutput " Found: $constructorName" "Green"
|
| 154 |
-
}
|
| 155 |
-
}
|
| 156 |
-
|
| 157 |
-
if ($services.Count -eq 0) {
|
| 158 |
-
Write-ColorOutput "No service constructors found matching pattern 'NewXxxService'!" "Red"
|
| 159 |
-
exit 1
|
| 160 |
-
}
|
| 161 |
-
|
| 162 |
-
Write-ColorOutput "`nTotal services discovered: $($services.Count)" "Blue"
|
| 163 |
-
return $services
|
| 164 |
-
}
|
| 165 |
-
|
| 166 |
-
function Get-TopologicalOrder {
|
| 167 |
-
param([System.Collections.Generic.List[ServiceInfo]]$Services)
|
| 168 |
-
|
| 169 |
-
Write-ColorOutput "`nBuilding dependency graph..." "Cyan"
|
| 170 |
-
|
| 171 |
-
# Build adjacency list
|
| 172 |
-
$graph = @{}
|
| 173 |
-
$inDegree = @{}
|
| 174 |
-
$domainToService = @{}
|
| 175 |
-
|
| 176 |
-
foreach ($svc in $Services) {
|
| 177 |
-
$graph[$svc.Domain] = [System.Collections.Generic.List[string]]::new()
|
| 178 |
-
$inDegree[$svc.Domain] = 0
|
| 179 |
-
$domainToService[$svc.Domain] = $svc
|
| 180 |
-
}
|
| 181 |
-
|
| 182 |
-
# Build edges
|
| 183 |
-
foreach ($svc in $Services) {
|
| 184 |
-
foreach ($dep in $svc.ServiceDependencies) {
|
| 185 |
-
if ($graph.ContainsKey($dep)) {
|
| 186 |
-
$graph[$dep].Add($svc.Domain)
|
| 187 |
-
$inDegree[$svc.Domain]++
|
| 188 |
-
}
|
| 189 |
-
}
|
| 190 |
-
}
|
| 191 |
-
|
| 192 |
-
# Kahn's algorithm for topological sort
|
| 193 |
-
$queue = [System.Collections.Generic.Queue[string]]::new()
|
| 194 |
-
foreach ($domain in $inDegree.Keys) {
|
| 195 |
-
if ($inDegree[$domain] -eq 0) {
|
| 196 |
-
$queue.Enqueue($domain)
|
| 197 |
-
}
|
| 198 |
-
}
|
| 199 |
-
|
| 200 |
-
$sorted = [System.Collections.Generic.List[string]]::new()
|
| 201 |
-
|
| 202 |
-
while ($queue.Count -gt 0) {
|
| 203 |
-
$current = $queue.Dequeue()
|
| 204 |
-
$sorted.Add($current)
|
| 205 |
-
|
| 206 |
-
foreach ($neighbor in $graph[$current]) {
|
| 207 |
-
$inDegree[$neighbor]--
|
| 208 |
-
if ($inDegree[$neighbor] -eq 0) {
|
| 209 |
-
$queue.Enqueue($neighbor)
|
| 210 |
-
}
|
| 211 |
-
}
|
| 212 |
-
}
|
| 213 |
-
|
| 214 |
-
# Check for cycles
|
| 215 |
-
if ($sorted.Count -ne $Services.Count) {
|
| 216 |
-
$remaining = $inDegree.Keys | Where-Object { $inDegree[$_] -gt 0 }
|
| 217 |
-
Write-ColorOutput "`nERROR: Circular dependency detected!" "Red"
|
| 218 |
-
Write-ColorOutput "Services involved in cycle: $($remaining -join ', ')" "Yellow"
|
| 219 |
-
exit 1
|
| 220 |
-
}
|
| 221 |
-
|
| 222 |
-
Write-ColorOutput " Dependency graph validated (no cycles)" "Green"
|
| 223 |
-
Write-ColorOutput " Topological order: $($sorted -join ' -> ')" "Blue"
|
| 224 |
-
|
| 225 |
-
# Return services in topological order
|
| 226 |
-
return $sorted | ForEach-Object { $domainToService[$_] }
|
| 227 |
-
}
|
| 228 |
-
|
| 229 |
-
function Resolve-ConstructorArgument {
|
| 230 |
-
param([Parameter]$Param)
|
| 231 |
-
|
| 232 |
-
$type = $Param.NormalizedType
|
| 233 |
-
|
| 234 |
-
# SPECIAL CASE MAPPINGS - Add more here as needed
|
| 235 |
-
# ============================================
|
| 236 |
-
|
| 237 |
-
# 1. JWT secret string
|
| 238 |
-
if ($Param.RawType -eq "string" -and $Param.Name -match "secret|key") {
|
| 239 |
-
return "configProvider.ProvideJWTConfig().GetSecretKey()"
|
| 240 |
-
}
|
| 241 |
-
|
| 242 |
-
# 2. Repository pattern: XxxxRepository -> repoProvider.ProvideXxxxRepository()
|
| 243 |
-
if ($type -match '^(.+)Repository$') {
|
| 244 |
-
$repoName = $matches[1]
|
| 245 |
-
return "repoProvider.Provide${repoName}Repository()"
|
| 246 |
-
}
|
| 247 |
-
|
| 248 |
-
# 3. Service pattern: XxxxService -> use variable (will be constructed before this)
|
| 249 |
-
if ($type -match 'Service$') {
|
| 250 |
-
return (Get-LowerCamelCase $type)
|
| 251 |
-
}
|
| 252 |
-
|
| 253 |
-
# ADD MORE SPECIAL CASES HERE:
|
| 254 |
-
# --------------------------------------------
|
| 255 |
-
# Example: Mail config
|
| 256 |
-
# if ($type -eq "MailConfig") {
|
| 257 |
-
# return "configProvider.ProvideMailConfig()"
|
| 258 |
-
# }
|
| 259 |
-
#
|
| 260 |
-
# Example: Redis client
|
| 261 |
-
# if ($type -eq "RedisClient") {
|
| 262 |
-
# return "configProvider.ProvideRedisClient()"
|
| 263 |
-
# }
|
| 264 |
-
# --------------------------------------------
|
| 265 |
-
|
| 266 |
-
# 4. Fallback: unresolved type
|
| 267 |
-
return "/* TODO: provide $($Param.RawType) */"
|
| 268 |
-
}
|
| 269 |
-
|
| 270 |
-
function Generate-ProviderCode {
|
| 271 |
-
param([System.Collections.Generic.List[ServiceInfo]]$ServicesInOrder)
|
| 272 |
-
|
| 273 |
-
Write-ColorOutput "`nGenerating provider code..." "Cyan"
|
| 274 |
-
|
| 275 |
-
$sb = [System.Text.StringBuilder]::new()
|
| 276 |
-
[void]$sb.AppendLine("package provider")
|
| 277 |
-
[void]$sb.AppendLine()
|
| 278 |
-
[void]$sb.AppendLine("import `"$ModulePath`"")
|
| 279 |
-
[void]$sb.AppendLine()
|
| 280 |
-
|
| 281 |
-
# Interface
|
| 282 |
-
[void]$sb.AppendLine("type ServicesProvider interface {")
|
| 283 |
-
foreach ($svc in $ServicesInOrder) {
|
| 284 |
-
$line = "`tProvide$($svc.Domain)() services.$($svc.Domain)"
|
| 285 |
-
[void]$sb.AppendLine($line)
|
| 286 |
-
}
|
| 287 |
-
[void]$sb.AppendLine("}")
|
| 288 |
-
[void]$sb.AppendLine()
|
| 289 |
-
|
| 290 |
-
# Struct
|
| 291 |
-
[void]$sb.AppendLine("type servicesProvider struct {")
|
| 292 |
-
foreach ($svc in $ServicesInOrder) {
|
| 293 |
-
$line = "`t$($svc.VarName) services.$($svc.Domain)"
|
| 294 |
-
[void]$sb.AppendLine($line)
|
| 295 |
-
}
|
| 296 |
-
[void]$sb.AppendLine("}")
|
| 297 |
-
[void]$sb.AppendLine()
|
| 298 |
-
|
| 299 |
-
# Constructor
|
| 300 |
-
[void]$sb.AppendLine("func NewServicesProvider(repoProvider RepositoriesProvider, configProvider ConfigProvider) ServicesProvider {")
|
| 301 |
-
|
| 302 |
-
# Initialize services in topological order
|
| 303 |
-
foreach ($svc in $ServicesInOrder) {
|
| 304 |
-
$args = @()
|
| 305 |
-
foreach ($param in $svc.Parameters) {
|
| 306 |
-
$args += Resolve-ConstructorArgument $param
|
| 307 |
-
}
|
| 308 |
-
$argsStr = $args -join ", "
|
| 309 |
-
$line = "`t$($svc.VarName) := services.$($svc.ConstructorName)($argsStr)"
|
| 310 |
-
[void]$sb.AppendLine($line)
|
| 311 |
-
}
|
| 312 |
-
|
| 313 |
-
[void]$sb.AppendLine()
|
| 314 |
-
[void]$sb.AppendLine("`treturn &servicesProvider{")
|
| 315 |
-
foreach ($svc in $ServicesInOrder) {
|
| 316 |
-
$line = "`t`t$($svc.VarName): $($svc.VarName),"
|
| 317 |
-
[void]$sb.AppendLine($line)
|
| 318 |
-
}
|
| 319 |
-
[void]$sb.AppendLine("`t}")
|
| 320 |
-
[void]$sb.AppendLine("}")
|
| 321 |
-
[void]$sb.AppendLine()
|
| 322 |
-
|
| 323 |
-
# Getter methods
|
| 324 |
-
foreach ($svc in $ServicesInOrder) {
|
| 325 |
-
[void]$sb.AppendLine("func (s *servicesProvider) Provide$($svc.Domain)() services.$($svc.Domain) {")
|
| 326 |
-
[void]$sb.AppendLine("`treturn s.$($svc.VarName)")
|
| 327 |
-
[void]$sb.AppendLine("}")
|
| 328 |
-
[void]$sb.AppendLine()
|
| 329 |
-
}
|
| 330 |
-
|
| 331 |
-
return $sb.ToString()
|
| 332 |
-
}
|
| 333 |
-
|
| 334 |
-
function Write-ProviderFile {
|
| 335 |
-
param([string]$Code, [string]$OutputPath)
|
| 336 |
-
|
| 337 |
-
Write-ColorOutput "Writing to $OutputPath..." "Cyan"
|
| 338 |
-
|
| 339 |
-
# Ensure directory exists
|
| 340 |
-
$dir = Split-Path $OutputPath -Parent
|
| 341 |
-
if ($dir -and -not (Test-Path $dir)) {
|
| 342 |
-
New-Item -ItemType Directory -Path $dir -Force | Out-Null
|
| 343 |
-
}
|
| 344 |
-
|
| 345 |
-
# Write file as UTF-8 without BOM
|
| 346 |
-
$utf8NoBom = [System.Text.UTF8Encoding]::new($false)
|
| 347 |
-
[System.IO.File]::WriteAllText($OutputPath, $Code, $utf8NoBom)
|
| 348 |
-
|
| 349 |
-
Write-ColorOutput " Successfully generated $OutputPath" "Green"
|
| 350 |
-
}
|
| 351 |
-
|
| 352 |
-
# ============================================
|
| 353 |
-
# MAIN EXECUTION
|
| 354 |
-
# ============================================
|
| 355 |
-
|
| 356 |
-
try {
|
| 357 |
-
Write-ColorOutput "`n=========================================" "Blue"
|
| 358 |
-
Write-ColorOutput " Go Service Dependency Injector v1.0" "Blue"
|
| 359 |
-
Write-ColorOutput "=========================================`n" "Blue"
|
| 360 |
-
|
| 361 |
-
# Step 1: Parse all service constructors
|
| 362 |
-
$services = Parse-GoFiles -Directory $ServicesDir
|
| 363 |
-
|
| 364 |
-
# Step 2: Perform topological sort
|
| 365 |
-
$sortedServices = Get-TopologicalOrder -Services $services
|
| 366 |
-
|
| 367 |
-
# Step 3: Generate provider code
|
| 368 |
-
$code = Generate-ProviderCode -ServicesInOrder $sortedServices
|
| 369 |
-
|
| 370 |
-
# Step 4: Write to file
|
| 371 |
-
Write-ProviderFile -Code $code -OutputPath $OutputFile
|
| 372 |
-
|
| 373 |
-
Write-ColorOutput "`nSUCCESS! Provider generated successfully.`n" "Green"
|
| 374 |
-
Write-ColorOutput "Next steps:" "Cyan"
|
| 375 |
-
Write-ColorOutput " 1. Review $OutputFile" "White"
|
| 376 |
-
Write-ColorOutput " 2. Fill any /* TODO: provide ... */ placeholders" "White"
|
| 377 |
-
Write-ColorOutput " 3. Run: go build ./provider" "White"
|
| 378 |
-
|
| 379 |
-
} catch {
|
| 380 |
-
Write-ColorOutput "`nERROR: $($_.Exception.Message)" "Red"
|
| 381 |
-
Write-ColorOutput "Stack trace: $($_.ScriptStackTrace)" "Yellow"
|
| 382 |
-
exit 1
|
| 383 |
}
|
|
|
|
| 1 |
+
#Requires -Version 5.1
|
| 2 |
+
<#
|
| 3 |
+
.SYNOPSIS
|
| 4 |
+
Automatic Dependency Injection Generator for Go Services
|
| 5 |
+
|
| 6 |
+
.DESCRIPTION
|
| 7 |
+
Scans ./services/ directory, discovers service constructors, resolves dependencies,
|
| 8 |
+
performs topological sorting, and generates provider/services_provider.go with full DI wiring.
|
| 9 |
+
|
| 10 |
+
.EXAMPLE
|
| 11 |
+
.\service_injector.ps1
|
| 12 |
+
|
| 13 |
+
.NOTES
|
| 14 |
+
- Works with PowerShell 5.1+ and PowerShell 7+
|
| 15 |
+
- No external dependencies required
|
| 16 |
+
- Supports multi-line constructor signatures
|
| 17 |
+
- Handles dependency cycles with clear error messages
|
| 18 |
+
#>
|
| 19 |
+
|
| 20 |
+
[CmdletBinding()]
|
| 21 |
+
param()
|
| 22 |
+
|
| 23 |
+
# Configuration
|
| 24 |
+
$ServicesDir = "./services"
|
| 25 |
+
$OutputFile = "provider/services_provider.go"
|
| 26 |
+
$ModulePath = "abdanhafidz.com/go-boilerplate/services"
|
| 27 |
+
|
| 28 |
+
# ANSI colors for better output (fallback to plain text if not supported)
|
| 29 |
+
$script:UseColors = $Host.UI.SupportsVirtualTerminal
|
| 30 |
+
function Write-ColorOutput {
|
| 31 |
+
param([string]$Message, [string]$Color = "White")
|
| 32 |
+
if ($script:UseColors) {
|
| 33 |
+
$colors = @{
|
| 34 |
+
"Green" = "`e[32m"; "Yellow" = "`e[33m"; "Red" = "`e[31m"
|
| 35 |
+
"Cyan" = "`e[36m"; "Blue" = "`e[34m"; "Reset" = "`e[0m"
|
| 36 |
+
}
|
| 37 |
+
Write-Host "$($colors[$Color])$Message$($colors['Reset'])"
|
| 38 |
+
} else {
|
| 39 |
+
Write-Host $Message
|
| 40 |
+
}
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
# Data structures
|
| 44 |
+
class ServiceInfo {
|
| 45 |
+
[string]$ConstructorName # NewAccountService
|
| 46 |
+
[string]$Domain # AccountService
|
| 47 |
+
[string]$VarName # accountService
|
| 48 |
+
[System.Collections.Generic.List[Parameter]]$Parameters
|
| 49 |
+
[System.Collections.Generic.List[string]]$ServiceDependencies
|
| 50 |
+
|
| 51 |
+
ServiceInfo() {
|
| 52 |
+
$this.Parameters = [System.Collections.Generic.List[Parameter]]::new()
|
| 53 |
+
$this.ServiceDependencies = [System.Collections.Generic.List[string]]::new()
|
| 54 |
+
}
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
class Parameter {
|
| 58 |
+
[string]$Name
|
| 59 |
+
[string]$RawType
|
| 60 |
+
[string]$NormalizedType
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
function Get-LowerCamelCase {
|
| 64 |
+
param([string]$Text)
|
| 65 |
+
if ($Text.Length -eq 0) { return $Text }
|
| 66 |
+
return $Text.Substring(0, 1).ToLower() + $Text.Substring(1)
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
function Normalize-TypeName {
|
| 70 |
+
param([string]$TypeStr)
|
| 71 |
+
|
| 72 |
+
# Remove leading pointer
|
| 73 |
+
$cleaned = $TypeStr -replace '^\*+', ''
|
| 74 |
+
|
| 75 |
+
# Remove package prefix (everything before last dot)
|
| 76 |
+
if ($cleaned -match '\.([^.]+)$') {
|
| 77 |
+
$cleaned = $matches[1]
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
return $cleaned.Trim()
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
function Parse-GoFiles {
|
| 84 |
+
param([string]$Directory)
|
| 85 |
+
|
| 86 |
+
Write-ColorOutput "Scanning for service constructors in $Directory..." "Cyan"
|
| 87 |
+
|
| 88 |
+
if (-not (Test-Path $Directory)) {
|
| 89 |
+
Write-ColorOutput "ERROR: Directory '$Directory' not found!" "Red"
|
| 90 |
+
exit 1
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
$goFiles = Get-ChildItem -Path $Directory -Filter "*.go" -Recurse -File
|
| 94 |
+
$services = [System.Collections.Generic.List[ServiceInfo]]::new()
|
| 95 |
+
|
| 96 |
+
foreach ($file in $goFiles) {
|
| 97 |
+
$content = Get-Content $file.FullName -Raw
|
| 98 |
+
|
| 99 |
+
# Match function signatures (support multi-line)
|
| 100 |
+
# Pattern: func NewXxxService(...) XxxService
|
| 101 |
+
$pattern = '(?ms)func\s+(New[a-zA-Z0-9]+Service)\s*\(([^)]*)\)\s+([a-zA-Z0-9*_.]+Service)'
|
| 102 |
+
$matches = [regex]::Matches($content, $pattern)
|
| 103 |
+
|
| 104 |
+
foreach ($match in $matches) {
|
| 105 |
+
$constructorName = $match.Groups[1].Value
|
| 106 |
+
$paramsStr = $match.Groups[2].Value
|
| 107 |
+
$returnType = $match.Groups[3].Value
|
| 108 |
+
|
| 109 |
+
# Extract domain name (XxxService)
|
| 110 |
+
$domain = Normalize-TypeName $returnType
|
| 111 |
+
$varName = Get-LowerCamelCase $domain
|
| 112 |
+
|
| 113 |
+
$service = [ServiceInfo]::new()
|
| 114 |
+
$service.ConstructorName = $constructorName
|
| 115 |
+
$service.Domain = $domain
|
| 116 |
+
$service.VarName = $varName
|
| 117 |
+
|
| 118 |
+
# Parse parameters
|
| 119 |
+
if ($paramsStr.Trim() -ne "") {
|
| 120 |
+
# Split by comma, but be careful with nested types
|
| 121 |
+
$paramList = $paramsStr -split ',\s*(?![^<>]*>)'
|
| 122 |
+
|
| 123 |
+
foreach ($param in $paramList) {
|
| 124 |
+
$param = $param.Trim()
|
| 125 |
+
if ($param -eq "") { continue }
|
| 126 |
+
|
| 127 |
+
# Split into name and type
|
| 128 |
+
$parts = $param -split '\s+', 2
|
| 129 |
+
|
| 130 |
+
$p = [Parameter]::new()
|
| 131 |
+
if ($parts.Count -eq 2) {
|
| 132 |
+
$p.Name = $parts[0]
|
| 133 |
+
$p.RawType = $parts[1]
|
| 134 |
+
} elseif ($parts.Count -eq 1) {
|
| 135 |
+
# Anonymous parameter - synthesize name
|
| 136 |
+
$p.Name = "param$($service.Parameters.Count)"
|
| 137 |
+
$p.RawType = $parts[0]
|
| 138 |
+
} else {
|
| 139 |
+
continue
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
$p.NormalizedType = Normalize-TypeName $p.RawType
|
| 143 |
+
$service.Parameters.Add($p)
|
| 144 |
+
|
| 145 |
+
# Track service dependencies
|
| 146 |
+
if ($p.NormalizedType -match 'Service$') {
|
| 147 |
+
$service.ServiceDependencies.Add($p.NormalizedType)
|
| 148 |
+
}
|
| 149 |
+
}
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
$services.Add($service)
|
| 153 |
+
Write-ColorOutput " Found: $constructorName" "Green"
|
| 154 |
+
}
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
if ($services.Count -eq 0) {
|
| 158 |
+
Write-ColorOutput "No service constructors found matching pattern 'NewXxxService'!" "Red"
|
| 159 |
+
exit 1
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
Write-ColorOutput "`nTotal services discovered: $($services.Count)" "Blue"
|
| 163 |
+
return $services
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
function Get-TopologicalOrder {
|
| 167 |
+
param([System.Collections.Generic.List[ServiceInfo]]$Services)
|
| 168 |
+
|
| 169 |
+
Write-ColorOutput "`nBuilding dependency graph..." "Cyan"
|
| 170 |
+
|
| 171 |
+
# Build adjacency list
|
| 172 |
+
$graph = @{}
|
| 173 |
+
$inDegree = @{}
|
| 174 |
+
$domainToService = @{}
|
| 175 |
+
|
| 176 |
+
foreach ($svc in $Services) {
|
| 177 |
+
$graph[$svc.Domain] = [System.Collections.Generic.List[string]]::new()
|
| 178 |
+
$inDegree[$svc.Domain] = 0
|
| 179 |
+
$domainToService[$svc.Domain] = $svc
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
# Build edges
|
| 183 |
+
foreach ($svc in $Services) {
|
| 184 |
+
foreach ($dep in $svc.ServiceDependencies) {
|
| 185 |
+
if ($graph.ContainsKey($dep)) {
|
| 186 |
+
$graph[$dep].Add($svc.Domain)
|
| 187 |
+
$inDegree[$svc.Domain]++
|
| 188 |
+
}
|
| 189 |
+
}
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
# Kahn's algorithm for topological sort
|
| 193 |
+
$queue = [System.Collections.Generic.Queue[string]]::new()
|
| 194 |
+
foreach ($domain in $inDegree.Keys) {
|
| 195 |
+
if ($inDegree[$domain] -eq 0) {
|
| 196 |
+
$queue.Enqueue($domain)
|
| 197 |
+
}
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
$sorted = [System.Collections.Generic.List[string]]::new()
|
| 201 |
+
|
| 202 |
+
while ($queue.Count -gt 0) {
|
| 203 |
+
$current = $queue.Dequeue()
|
| 204 |
+
$sorted.Add($current)
|
| 205 |
+
|
| 206 |
+
foreach ($neighbor in $graph[$current]) {
|
| 207 |
+
$inDegree[$neighbor]--
|
| 208 |
+
if ($inDegree[$neighbor] -eq 0) {
|
| 209 |
+
$queue.Enqueue($neighbor)
|
| 210 |
+
}
|
| 211 |
+
}
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
# Check for cycles
|
| 215 |
+
if ($sorted.Count -ne $Services.Count) {
|
| 216 |
+
$remaining = $inDegree.Keys | Where-Object { $inDegree[$_] -gt 0 }
|
| 217 |
+
Write-ColorOutput "`nERROR: Circular dependency detected!" "Red"
|
| 218 |
+
Write-ColorOutput "Services involved in cycle: $($remaining -join ', ')" "Yellow"
|
| 219 |
+
exit 1
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
Write-ColorOutput " Dependency graph validated (no cycles)" "Green"
|
| 223 |
+
Write-ColorOutput " Topological order: $($sorted -join ' -> ')" "Blue"
|
| 224 |
+
|
| 225 |
+
# Return services in topological order
|
| 226 |
+
return $sorted | ForEach-Object { $domainToService[$_] }
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
function Resolve-ConstructorArgument {
|
| 230 |
+
param([Parameter]$Param)
|
| 231 |
+
|
| 232 |
+
$type = $Param.NormalizedType
|
| 233 |
+
|
| 234 |
+
# SPECIAL CASE MAPPINGS - Add more here as needed
|
| 235 |
+
# ============================================
|
| 236 |
+
|
| 237 |
+
# 1. JWT secret string
|
| 238 |
+
if ($Param.RawType -eq "string" -and $Param.Name -match "secret|key") {
|
| 239 |
+
return "configProvider.ProvideJWTConfig().GetSecretKey()"
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
# 2. Repository pattern: XxxxRepository -> repoProvider.ProvideXxxxRepository()
|
| 243 |
+
if ($type -match '^(.+)Repository$') {
|
| 244 |
+
$repoName = $matches[1]
|
| 245 |
+
return "repoProvider.Provide${repoName}Repository()"
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
# 3. Service pattern: XxxxService -> use variable (will be constructed before this)
|
| 249 |
+
if ($type -match 'Service$') {
|
| 250 |
+
return (Get-LowerCamelCase $type)
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
# ADD MORE SPECIAL CASES HERE:
|
| 254 |
+
# --------------------------------------------
|
| 255 |
+
# Example: Mail config
|
| 256 |
+
# if ($type -eq "MailConfig") {
|
| 257 |
+
# return "configProvider.ProvideMailConfig()"
|
| 258 |
+
# }
|
| 259 |
+
#
|
| 260 |
+
# Example: Redis client
|
| 261 |
+
# if ($type -eq "RedisClient") {
|
| 262 |
+
# return "configProvider.ProvideRedisClient()"
|
| 263 |
+
# }
|
| 264 |
+
# --------------------------------------------
|
| 265 |
+
|
| 266 |
+
# 4. Fallback: unresolved type
|
| 267 |
+
return "/* TODO: provide $($Param.RawType) */"
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
function Generate-ProviderCode {
|
| 271 |
+
param([System.Collections.Generic.List[ServiceInfo]]$ServicesInOrder)
|
| 272 |
+
|
| 273 |
+
Write-ColorOutput "`nGenerating provider code..." "Cyan"
|
| 274 |
+
|
| 275 |
+
$sb = [System.Text.StringBuilder]::new()
|
| 276 |
+
[void]$sb.AppendLine("package provider")
|
| 277 |
+
[void]$sb.AppendLine()
|
| 278 |
+
[void]$sb.AppendLine("import `"$ModulePath`"")
|
| 279 |
+
[void]$sb.AppendLine()
|
| 280 |
+
|
| 281 |
+
# Interface
|
| 282 |
+
[void]$sb.AppendLine("type ServicesProvider interface {")
|
| 283 |
+
foreach ($svc in $ServicesInOrder) {
|
| 284 |
+
$line = "`tProvide$($svc.Domain)() services.$($svc.Domain)"
|
| 285 |
+
[void]$sb.AppendLine($line)
|
| 286 |
+
}
|
| 287 |
+
[void]$sb.AppendLine("}")
|
| 288 |
+
[void]$sb.AppendLine()
|
| 289 |
+
|
| 290 |
+
# Struct
|
| 291 |
+
[void]$sb.AppendLine("type servicesProvider struct {")
|
| 292 |
+
foreach ($svc in $ServicesInOrder) {
|
| 293 |
+
$line = "`t$($svc.VarName) services.$($svc.Domain)"
|
| 294 |
+
[void]$sb.AppendLine($line)
|
| 295 |
+
}
|
| 296 |
+
[void]$sb.AppendLine("}")
|
| 297 |
+
[void]$sb.AppendLine()
|
| 298 |
+
|
| 299 |
+
# Constructor
|
| 300 |
+
[void]$sb.AppendLine("func NewServicesProvider(repoProvider RepositoriesProvider, configProvider ConfigProvider) ServicesProvider {")
|
| 301 |
+
|
| 302 |
+
# Initialize services in topological order
|
| 303 |
+
foreach ($svc in $ServicesInOrder) {
|
| 304 |
+
$args = @()
|
| 305 |
+
foreach ($param in $svc.Parameters) {
|
| 306 |
+
$args += Resolve-ConstructorArgument $param
|
| 307 |
+
}
|
| 308 |
+
$argsStr = $args -join ", "
|
| 309 |
+
$line = "`t$($svc.VarName) := services.$($svc.ConstructorName)($argsStr)"
|
| 310 |
+
[void]$sb.AppendLine($line)
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
[void]$sb.AppendLine()
|
| 314 |
+
[void]$sb.AppendLine("`treturn &servicesProvider{")
|
| 315 |
+
foreach ($svc in $ServicesInOrder) {
|
| 316 |
+
$line = "`t`t$($svc.VarName): $($svc.VarName),"
|
| 317 |
+
[void]$sb.AppendLine($line)
|
| 318 |
+
}
|
| 319 |
+
[void]$sb.AppendLine("`t}")
|
| 320 |
+
[void]$sb.AppendLine("}")
|
| 321 |
+
[void]$sb.AppendLine()
|
| 322 |
+
|
| 323 |
+
# Getter methods
|
| 324 |
+
foreach ($svc in $ServicesInOrder) {
|
| 325 |
+
[void]$sb.AppendLine("func (s *servicesProvider) Provide$($svc.Domain)() services.$($svc.Domain) {")
|
| 326 |
+
[void]$sb.AppendLine("`treturn s.$($svc.VarName)")
|
| 327 |
+
[void]$sb.AppendLine("}")
|
| 328 |
+
[void]$sb.AppendLine()
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
return $sb.ToString()
|
| 332 |
+
}
|
| 333 |
+
|
| 334 |
+
function Write-ProviderFile {
|
| 335 |
+
param([string]$Code, [string]$OutputPath)
|
| 336 |
+
|
| 337 |
+
Write-ColorOutput "Writing to $OutputPath..." "Cyan"
|
| 338 |
+
|
| 339 |
+
# Ensure directory exists
|
| 340 |
+
$dir = Split-Path $OutputPath -Parent
|
| 341 |
+
if ($dir -and -not (Test-Path $dir)) {
|
| 342 |
+
New-Item -ItemType Directory -Path $dir -Force | Out-Null
|
| 343 |
+
}
|
| 344 |
+
|
| 345 |
+
# Write file as UTF-8 without BOM
|
| 346 |
+
$utf8NoBom = [System.Text.UTF8Encoding]::new($false)
|
| 347 |
+
[System.IO.File]::WriteAllText($OutputPath, $Code, $utf8NoBom)
|
| 348 |
+
|
| 349 |
+
Write-ColorOutput " Successfully generated $OutputPath" "Green"
|
| 350 |
+
}
|
| 351 |
+
|
| 352 |
+
# ============================================
|
| 353 |
+
# MAIN EXECUTION
|
| 354 |
+
# ============================================
|
| 355 |
+
|
| 356 |
+
try {
|
| 357 |
+
Write-ColorOutput "`n=========================================" "Blue"
|
| 358 |
+
Write-ColorOutput " Go Service Dependency Injector v1.0" "Blue"
|
| 359 |
+
Write-ColorOutput "=========================================`n" "Blue"
|
| 360 |
+
|
| 361 |
+
# Step 1: Parse all service constructors
|
| 362 |
+
$services = Parse-GoFiles -Directory $ServicesDir
|
| 363 |
+
|
| 364 |
+
# Step 2: Perform topological sort
|
| 365 |
+
$sortedServices = Get-TopologicalOrder -Services $services
|
| 366 |
+
|
| 367 |
+
# Step 3: Generate provider code
|
| 368 |
+
$code = Generate-ProviderCode -ServicesInOrder $sortedServices
|
| 369 |
+
|
| 370 |
+
# Step 4: Write to file
|
| 371 |
+
Write-ProviderFile -Code $code -OutputPath $OutputFile
|
| 372 |
+
|
| 373 |
+
Write-ColorOutput "`nSUCCESS! Provider generated successfully.`n" "Green"
|
| 374 |
+
Write-ColorOutput "Next steps:" "Cyan"
|
| 375 |
+
Write-ColorOutput " 1. Review $OutputFile" "White"
|
| 376 |
+
Write-ColorOutput " 2. Fill any /* TODO: provide ... */ placeholders" "White"
|
| 377 |
+
Write-ColorOutput " 3. Run: go build ./provider" "White"
|
| 378 |
+
|
| 379 |
+
} catch {
|
| 380 |
+
Write-ColorOutput "`nERROR: $($_.Exception.Message)" "Red"
|
| 381 |
+
Write-ColorOutput "Stack trace: $($_.ScriptStackTrace)" "Yellow"
|
| 382 |
+
exit 1
|
| 383 |
}
|
go.mod
CHANGED
|
@@ -57,6 +57,7 @@ require (
|
|
| 57 |
github.com/quic-go/qpack v0.5.1 // indirect
|
| 58 |
github.com/quic-go/quic-go v0.55.0 // indirect
|
| 59 |
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
|
|
|
| 60 |
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
| 61 |
github.com/ugorji/go/codec v1.3.0 // indirect
|
| 62 |
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
|
|
|
| 57 |
github.com/quic-go/qpack v0.5.1 // indirect
|
| 58 |
github.com/quic-go/quic-go v0.55.0 // indirect
|
| 59 |
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
| 60 |
+
github.com/supabase-community/storage-go v0.8.1 // indirect
|
| 61 |
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
| 62 |
github.com/ugorji/go/codec v1.3.0 // indirect
|
| 63 |
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
go.sum
CHANGED
|
@@ -140,6 +140,8 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
|
|
| 140 |
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
| 141 |
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
| 142 |
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
|
|
|
|
|
|
| 143 |
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
| 144 |
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
| 145 |
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
|
|
|
| 140 |
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
| 141 |
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
| 142 |
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
| 143 |
+
github.com/supabase-community/storage-go v0.8.1 h1:EwD0vr+ADBIjBWH8G69AxWuvdFhifv64cfE/sjRky6I=
|
| 144 |
+
github.com/supabase-community/storage-go v0.8.1/go.mod h1:oBKcJf5rcUXy3Uj9eS5wR6mvpwbmvkjOtAA+4tGcdvQ=
|
| 145 |
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
| 146 |
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
| 147 |
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
middleware/authentication_middleware.go
CHANGED
|
@@ -1,48 +1,48 @@
|
|
| 1 |
-
package middleware
|
| 2 |
-
|
| 3 |
-
import (
|
| 4 |
-
"errors"
|
| 5 |
-
"fmt"
|
| 6 |
-
"strings"
|
| 7 |
-
|
| 8 |
-
http_error "abdanhafidz.com/go-boilerplate/models/error"
|
| 9 |
-
"abdanhafidz.com/go-boilerplate/services"
|
| 10 |
-
utils "abdanhafidz.com/go-boilerplate/utils"
|
| 11 |
-
"github.com/gin-gonic/gin"
|
| 12 |
-
)
|
| 13 |
-
|
| 14 |
-
type AuthenticationMiddleware interface {
|
| 15 |
-
VerifyAccount(ctx *gin.Context)
|
| 16 |
-
}
|
| 17 |
-
type authenticationMiddleware struct {
|
| 18 |
-
jwtService services.JWTService
|
| 19 |
-
}
|
| 20 |
-
|
| 21 |
-
func NewAuthenticationMiddleware(jwtService services.JWTService) AuthenticationMiddleware {
|
| 22 |
-
return &authenticationMiddleware{
|
| 23 |
-
jwtService: jwtService,
|
| 24 |
-
}
|
| 25 |
-
}
|
| 26 |
-
func (m *authenticationMiddleware) VerifyAccount(c *gin.Context) {
|
| 27 |
-
|
| 28 |
-
authorizationBearer := c.Request.Header["Authorization"]
|
| 29 |
-
|
| 30 |
-
if authorizationBearer != nil {
|
| 31 |
-
token := strings.Split(authorizationBearer[0], " ")[1]
|
| 32 |
-
claim, err := m.jwtService.ValidateToken(c.Request.Context(), token)
|
| 33 |
-
|
| 34 |
-
if err != nil && errors.Is(err, http_error.INVALID_TOKEN) {
|
| 35 |
-
utils.ResponseFAILED(c, claim, http_error.INVALID_TOKEN)
|
| 36 |
-
c.Abort()
|
| 37 |
-
return
|
| 38 |
-
}
|
| 39 |
-
fmt.Println("Claims:", claim)
|
| 40 |
-
c.Set("account_id", claim.AccountId)
|
| 41 |
-
c.Next()
|
| 42 |
-
|
| 43 |
-
} else {
|
| 44 |
-
utils.ResponseFAILED(c, "Empty Token", http_error.UNAUTHORIZED)
|
| 45 |
-
return
|
| 46 |
-
}
|
| 47 |
-
|
| 48 |
-
}
|
|
|
|
| 1 |
+
package middleware
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"errors"
|
| 5 |
+
"fmt"
|
| 6 |
+
"strings"
|
| 7 |
+
|
| 8 |
+
http_error "abdanhafidz.com/go-boilerplate/models/error"
|
| 9 |
+
"abdanhafidz.com/go-boilerplate/services"
|
| 10 |
+
utils "abdanhafidz.com/go-boilerplate/utils"
|
| 11 |
+
"github.com/gin-gonic/gin"
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
type AuthenticationMiddleware interface {
|
| 15 |
+
VerifyAccount(ctx *gin.Context)
|
| 16 |
+
}
|
| 17 |
+
type authenticationMiddleware struct {
|
| 18 |
+
jwtService services.JWTService
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
func NewAuthenticationMiddleware(jwtService services.JWTService) AuthenticationMiddleware {
|
| 22 |
+
return &authenticationMiddleware{
|
| 23 |
+
jwtService: jwtService,
|
| 24 |
+
}
|
| 25 |
+
}
|
| 26 |
+
func (m *authenticationMiddleware) VerifyAccount(c *gin.Context) {
|
| 27 |
+
|
| 28 |
+
authorizationBearer := c.Request.Header["Authorization"]
|
| 29 |
+
|
| 30 |
+
if authorizationBearer != nil {
|
| 31 |
+
token := strings.Split(authorizationBearer[0], " ")[1]
|
| 32 |
+
claim, err := m.jwtService.ValidateToken(c.Request.Context(), token)
|
| 33 |
+
|
| 34 |
+
if err != nil && errors.Is(err, http_error.INVALID_TOKEN) {
|
| 35 |
+
utils.ResponseFAILED(c, claim, http_error.INVALID_TOKEN)
|
| 36 |
+
c.Abort()
|
| 37 |
+
return
|
| 38 |
+
}
|
| 39 |
+
fmt.Println("Claims:", claim)
|
| 40 |
+
c.Set("account_id", claim.AccountId)
|
| 41 |
+
c.Next()
|
| 42 |
+
|
| 43 |
+
} else {
|
| 44 |
+
utils.ResponseFAILED(c, "Empty Token", http_error.UNAUTHORIZED)
|
| 45 |
+
return
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
}
|
middleware/authorization_middleware.go
CHANGED
|
@@ -1,43 +1,43 @@
|
|
| 1 |
-
package middleware
|
| 2 |
-
|
| 3 |
-
import (
|
| 4 |
-
http_error "abdanhafidz.com/go-boilerplate/models/error"
|
| 5 |
-
"abdanhafidz.com/go-boilerplate/services"
|
| 6 |
-
utils "abdanhafidz.com/go-boilerplate/utils"
|
| 7 |
-
"github.com/gin-gonic/gin"
|
| 8 |
-
"github.com/google/uuid"
|
| 9 |
-
)
|
| 10 |
-
|
| 11 |
-
type AuthorizationMiddleware interface {
|
| 12 |
-
AuthorizeUserToEvent(ctx *gin.Context)
|
| 13 |
-
}
|
| 14 |
-
type authorizationMiddleware struct {
|
| 15 |
-
eventService services.EventService
|
| 16 |
-
}
|
| 17 |
-
|
| 18 |
-
func NewAuthorizationMiddleware(eventService services.EventService) AuthorizationMiddleware {
|
| 19 |
-
return &authorizationMiddleware{
|
| 20 |
-
eventService: eventService,
|
| 21 |
-
}
|
| 22 |
-
}
|
| 23 |
-
|
| 24 |
-
func (m *authorizationMiddleware) AuthorizeUserToEvent(c *gin.Context) {
|
| 25 |
-
|
| 26 |
-
eventSlug := c.Param("slug")
|
| 27 |
-
accountId, exists := c.Get("account_id")
|
| 28 |
-
if !exists {
|
| 29 |
-
utils.ResponseFAILED(c, eventSlug, http_error.
|
| 30 |
-
c.Abort()
|
| 31 |
-
return
|
| 32 |
-
}
|
| 33 |
-
|
| 34 |
-
err := m.eventService.AuthorizeUserToEvent(c.Request.Context(), eventSlug, accountId.(uuid.UUID))
|
| 35 |
-
|
| 36 |
-
if err != nil {
|
| 37 |
-
utils.ResponseFAILED(c, eventSlug, err)
|
| 38 |
-
c.Abort()
|
| 39 |
-
return
|
| 40 |
-
}
|
| 41 |
-
|
| 42 |
-
c.Next()
|
| 43 |
-
}
|
|
|
|
| 1 |
+
package middleware
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
http_error "abdanhafidz.com/go-boilerplate/models/error"
|
| 5 |
+
"abdanhafidz.com/go-boilerplate/services"
|
| 6 |
+
utils "abdanhafidz.com/go-boilerplate/utils"
|
| 7 |
+
"github.com/gin-gonic/gin"
|
| 8 |
+
"github.com/google/uuid"
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
type AuthorizationMiddleware interface {
|
| 12 |
+
AuthorizeUserToEvent(ctx *gin.Context)
|
| 13 |
+
}
|
| 14 |
+
type authorizationMiddleware struct {
|
| 15 |
+
eventService services.EventService
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
func NewAuthorizationMiddleware(eventService services.EventService) AuthorizationMiddleware {
|
| 19 |
+
return &authorizationMiddleware{
|
| 20 |
+
eventService: eventService,
|
| 21 |
+
}
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
func (m *authorizationMiddleware) AuthorizeUserToEvent(c *gin.Context) {
|
| 25 |
+
|
| 26 |
+
eventSlug := c.Param("slug")
|
| 27 |
+
accountId, exists := c.Get("account_id")
|
| 28 |
+
if !exists {
|
| 29 |
+
utils.ResponseFAILED(c, eventSlug, http_error.NOT_FOUND_ERROR)
|
| 30 |
+
c.Abort()
|
| 31 |
+
return
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
err := m.eventService.AuthorizeUserToEvent(c.Request.Context(), eventSlug, accountId.(uuid.UUID))
|
| 35 |
+
|
| 36 |
+
if err != nil {
|
| 37 |
+
utils.ResponseFAILED(c, eventSlug, err)
|
| 38 |
+
c.Abort()
|
| 39 |
+
return
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
c.Next()
|
| 43 |
+
}
|
middleware/middleware.go
CHANGED
|
@@ -1 +1 @@
|
|
| 1 |
-
package middleware
|
|
|
|
| 1 |
+
package middleware
|
models/dto/academy_dto.go
CHANGED
|
@@ -1,30 +1,30 @@
|
|
| 1 |
-
package dto
|
| 2 |
-
|
| 3 |
-
import "github.com/google/uuid"
|
| 4 |
-
|
| 5 |
-
type CreateAcademyRequest struct {
|
| 6 |
-
Title string `json:"title" binding:"required"`
|
| 7 |
-
Slug string `json:"slug"`
|
| 8 |
-
Description string `json:"description"`
|
| 9 |
-
ImageUrl string `json:"image_url"`
|
| 10 |
-
}
|
| 11 |
-
|
| 12 |
-
type UpdateAcademyRequest struct {
|
| 13 |
-
Title string `json:"title"`
|
| 14 |
-
Slug string `json:"slug"`
|
| 15 |
-
Description string `json:"description"`
|
| 16 |
-
ImageUrl string `json:"image_url"`
|
| 17 |
-
}
|
| 18 |
-
|
| 19 |
-
type CreateMaterialRequest struct {
|
| 20 |
-
AcademyId uuid.UUID `json:"academy_id" binding:"required"`
|
| 21 |
-
Title string `json:"title" binding:"required"`
|
| 22 |
-
Slug string `json:"slug"`
|
| 23 |
-
Description string `json:"description"`
|
| 24 |
-
}
|
| 25 |
-
|
| 26 |
-
type CreateContentRequest struct {
|
| 27 |
-
MaterialId uuid.UUID `json:"material_id" binding:"required"`
|
| 28 |
-
Title string `json:"title" binding:"required"`
|
| 29 |
-
Contents string `json:"contents"`
|
| 30 |
-
}
|
|
|
|
| 1 |
+
package dto
|
| 2 |
+
|
| 3 |
+
import "github.com/google/uuid"
|
| 4 |
+
|
| 5 |
+
type CreateAcademyRequest struct {
|
| 6 |
+
Title string `json:"title" binding:"required"`
|
| 7 |
+
Slug string `json:"slug"`
|
| 8 |
+
Description string `json:"description"`
|
| 9 |
+
ImageUrl string `json:"image_url"`
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
type UpdateAcademyRequest struct {
|
| 13 |
+
Title string `json:"title"`
|
| 14 |
+
Slug string `json:"slug"`
|
| 15 |
+
Description string `json:"description"`
|
| 16 |
+
ImageUrl string `json:"image_url"`
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
type CreateMaterialRequest struct {
|
| 20 |
+
AcademyId uuid.UUID `json:"academy_id" binding:"required"`
|
| 21 |
+
Title string `json:"title" binding:"required"`
|
| 22 |
+
Slug string `json:"slug"`
|
| 23 |
+
Description string `json:"description"`
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
type CreateContentRequest struct {
|
| 27 |
+
MaterialId uuid.UUID `json:"material_id" binding:"required"`
|
| 28 |
+
Title string `json:"title" binding:"required"`
|
| 29 |
+
Contents string `json:"contents"`
|
| 30 |
+
}
|
models/dto/account_details_dto.go
CHANGED
|
@@ -1,19 +1,19 @@
|
|
| 1 |
-
package dto
|
| 2 |
-
|
| 3 |
-
import (
|
| 4 |
-
entity "abdanhafidz.com/go-boilerplate/models/entity"
|
| 5 |
-
)
|
| 6 |
-
|
| 7 |
-
type AccountDetailResponse struct {
|
| 8 |
-
Account entity.Account `json:"account"`
|
| 9 |
-
Details entity.AccountDetail `json:"details"`
|
| 10 |
-
}
|
| 11 |
-
|
| 12 |
-
type UpdateAccountDetailRequest struct {
|
| 13 |
-
FullName *string `json:"full_name"`
|
| 14 |
-
SchoolName *string `json:"school_name"`
|
| 15 |
-
Province *string `json:"province"`
|
| 16 |
-
City *string `json:"city"`
|
| 17 |
-
Avatar *string `json:"avatar"`
|
| 18 |
-
PhoneNumber *string `json:"phone_number"`
|
| 19 |
-
}
|
|
|
|
| 1 |
+
package dto
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
entity "abdanhafidz.com/go-boilerplate/models/entity"
|
| 5 |
+
)
|
| 6 |
+
|
| 7 |
+
type AccountDetailResponse struct {
|
| 8 |
+
Account entity.Account `json:"account"`
|
| 9 |
+
Details entity.AccountDetail `json:"details"`
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
type UpdateAccountDetailRequest struct {
|
| 13 |
+
FullName *string `json:"full_name"`
|
| 14 |
+
SchoolName *string `json:"school_name"`
|
| 15 |
+
Province *string `json:"province"`
|
| 16 |
+
City *string `json:"city"`
|
| 17 |
+
Avatar *string `json:"avatar"`
|
| 18 |
+
PhoneNumber *string `json:"phone_number"`
|
| 19 |
+
}
|
models/dto/event_dto.go
CHANGED
|
@@ -1,20 +1,20 @@
|
|
| 1 |
-
package dto
|
| 2 |
-
|
| 3 |
-
import (
|
| 4 |
-
entity "abdanhafidz.com/go-boilerplate/models/entity"
|
| 5 |
-
)
|
| 6 |
-
|
| 7 |
-
type EventDetailResponse struct {
|
| 8 |
-
Data *entity.Events
|
| 9 |
-
RegisterStatus int `json:"register_status" binding:"required"`
|
| 10 |
-
}
|
| 11 |
-
|
| 12 |
-
type JoinEventRequest struct {
|
| 13 |
-
EventCode string `json:"event_code" binding:"required"`
|
| 14 |
-
}
|
| 15 |
-
|
| 16 |
-
type EventStatus struct {
|
| 17 |
-
IsHasNotStarted bool
|
| 18 |
-
IsOnGoing bool
|
| 19 |
-
IsFinished bool
|
| 20 |
-
}
|
|
|
|
| 1 |
+
package dto
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
entity "abdanhafidz.com/go-boilerplate/models/entity"
|
| 5 |
+
)
|
| 6 |
+
|
| 7 |
+
type EventDetailResponse struct {
|
| 8 |
+
Data *entity.Events
|
| 9 |
+
RegisterStatus int `json:"register_status" binding:"required"`
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
type JoinEventRequest struct {
|
| 13 |
+
EventCode string `json:"event_code" binding:"required"`
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
type EventStatus struct {
|
| 17 |
+
IsHasNotStarted bool
|
| 18 |
+
IsOnGoing bool
|
| 19 |
+
IsFinished bool
|
| 20 |
+
}
|
models/dto/exam_dto.go
CHANGED
|
@@ -1,23 +1,23 @@
|
|
| 1 |
-
package dto
|
| 2 |
-
|
| 3 |
-
import (
|
| 4 |
-
entity "abdanhafidz.com/go-boilerplate/models/entity"
|
| 5 |
-
"github.com/google/uuid"
|
| 6 |
-
)
|
| 7 |
-
|
| 8 |
-
type UserExamStatus struct {
|
| 9 |
-
IsNotAttempt bool
|
| 10 |
-
IsOnAttempt bool
|
| 11 |
-
IsSubmitted bool
|
| 12 |
-
IsTimeOut bool
|
| 13 |
-
}
|
| 14 |
-
|
| 15 |
-
type AnswerWithQuestion struct {
|
| 16 |
-
Answer entity.ExamEventAnswer `json:"answer"`
|
| 17 |
-
Question entity.Questions `json:"question"`
|
| 18 |
-
}
|
| 19 |
-
|
| 20 |
-
type AnswerExamEventRequest struct {
|
| 21 |
-
QuestionId uuid.UUID `json:"question_id" binding:"required"`
|
| 22 |
-
Answer []string `json:"answer"`
|
| 23 |
-
}
|
|
|
|
| 1 |
+
package dto
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
entity "abdanhafidz.com/go-boilerplate/models/entity"
|
| 5 |
+
"github.com/google/uuid"
|
| 6 |
+
)
|
| 7 |
+
|
| 8 |
+
type UserExamStatus struct {
|
| 9 |
+
IsNotAttempt bool
|
| 10 |
+
IsOnAttempt bool
|
| 11 |
+
IsSubmitted bool
|
| 12 |
+
IsTimeOut bool
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
type AnswerWithQuestion struct {
|
| 16 |
+
Answer entity.ExamEventAnswer `json:"answer"`
|
| 17 |
+
Question entity.Questions `json:"question"`
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
type AnswerExamEventRequest struct {
|
| 21 |
+
QuestionId uuid.UUID `json:"question_id" binding:"required"`
|
| 22 |
+
Answer []string `json:"answer"`
|
| 23 |
+
}
|
models/dto/option_dto.go
CHANGED
|
@@ -1,12 +1,12 @@
|
|
| 1 |
-
package dto
|
| 2 |
-
|
| 3 |
-
import entity "abdanhafidz.com/go-boilerplate/models/entity"
|
| 4 |
-
|
| 5 |
-
type OptionsRequest struct {
|
| 6 |
-
OptionName string `json:"option_name" binding:"required"`
|
| 7 |
-
OptionValue []string `json:"option_values" binding:"required"`
|
| 8 |
-
}
|
| 9 |
-
|
| 10 |
-
type OptionsResponse struct {
|
| 11 |
-
Options []entity.Options `json:"options"`
|
| 12 |
-
}
|
|
|
|
| 1 |
+
package dto
|
| 2 |
+
|
| 3 |
+
import entity "abdanhafidz.com/go-boilerplate/models/entity"
|
| 4 |
+
|
| 5 |
+
type OptionsRequest struct {
|
| 6 |
+
OptionName string `json:"option_name" binding:"required"`
|
| 7 |
+
OptionValue []string `json:"option_values" binding:"required"`
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
type OptionsResponse struct {
|
| 11 |
+
Options []entity.Options `json:"options"`
|
| 12 |
+
}
|
models/dto/upload_dto.go
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package dto
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
// Gunakan path yang SAMA PERSIS dengan yang ada di OptionsRequest
|
| 5 |
+
entity "abdanhafidz.com/go-boilerplate/models/entity"
|
| 6 |
+
|
| 7 |
+
"github.com/google/uuid"
|
| 8 |
+
"time"
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
type FileResponse struct {
|
| 12 |
+
Id uuid.UUID `json:"id"`
|
| 13 |
+
OriginalName string `json:"original_name"`
|
| 14 |
+
URL string `json:"url"`
|
| 15 |
+
MimeType string `json:"mime_type"`
|
| 16 |
+
Size int64 `json:"size"`
|
| 17 |
+
CreatedAt time.Time `json:"created_at"`
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
type FileUploadResponse struct {
|
| 21 |
+
Status string `json:"status"`
|
| 22 |
+
Message string `json:"message"`
|
| 23 |
+
Data []FileResponse `json:"data"`
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
type FileResponseSingle struct {
|
| 27 |
+
Status string `json:"status"`
|
| 28 |
+
Message string `json:"message"`
|
| 29 |
+
Data FileResponse `json:"data"`
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
func FormatFileResponse(f *entity.File, baseURL string) FileResponse {
|
| 33 |
+
fullURL := baseURL + f.Path
|
| 34 |
+
|
| 35 |
+
return FileResponse{
|
| 36 |
+
Id: f.Id,
|
| 37 |
+
OriginalName: f.OriginalName,
|
| 38 |
+
URL: fullURL,
|
| 39 |
+
MimeType: f.MimeType,
|
| 40 |
+
Size: f.Size,
|
| 41 |
+
CreatedAt: f.CreatedAt,
|
| 42 |
+
}
|
| 43 |
+
}
|
models/entity/contant.go
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package models
|
| 2 |
+
|
| 3 |
+
const (
|
| 4 |
+
StatusNotStarted = "NOT_STARTED"
|
| 5 |
+
StatusInProgress = "IN_PROGRESS"
|
| 6 |
+
StatusCompleted = "COMPLETED"
|
| 7 |
+
)
|
models/entity/entity.go
CHANGED
|
@@ -332,3 +332,18 @@ type AcademyContentProgress struct {
|
|
| 332 |
}
|
| 333 |
|
| 334 |
func (AcademyContentProgress) TableName() string { return "academy_content_progress" }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 332 |
}
|
| 333 |
|
| 334 |
func (AcademyContentProgress) TableName() string { return "academy_content_progress" }
|
| 335 |
+
|
| 336 |
+
type File struct {
|
| 337 |
+
Id uuid.UUID `gorm:"type:uuid;primary_key;default:uuid_generate_v4()" json:"id"`
|
| 338 |
+
OriginalName string `json:"original_name,omitempty"`
|
| 339 |
+
StoredName string `json:"stored_name,omitempty"`
|
| 340 |
+
MimeType string `json:"mime_type,omitempty"`
|
| 341 |
+
Size int64 `json:"size,omitempty"`
|
| 342 |
+
Path string `json:"path,omitempty"`
|
| 343 |
+
Context string `json:"context,omitempty"`
|
| 344 |
+
AccountId uuid.UUID `json:"account_id,omitempty"`
|
| 345 |
+
CreatedAt time.Time `json:"created_at,omitempty"`
|
| 346 |
+
Account *Account `gorm:"foreignKey:AccountId" json:"account,omitempty"`
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
func (File) TableName() string { return "files" }
|
models/error/error.go
CHANGED
|
@@ -3,25 +3,49 @@ package http_error
|
|
| 3 |
import "errors"
|
| 4 |
|
| 5 |
var (
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
ALREADY_REGISTERED_TO_EVENT = errors.New("Account already registered to this event")
|
| 19 |
-
EMAIL_ALREADY_EXISTS = errors.New("Email already registered")
|
| 20 |
NOT_REGISTERED_TO_EVENT = errors.New("Account is not registered to this event")
|
| 21 |
-
INVALID_OTP = errors.New("Invalid OTP Code")
|
| 22 |
ERR_PROBLEM_SET_NOT_FOUND = errors.New("problem set not found")
|
| 23 |
ERR_QUESTION_NOT_FOUND = errors.New("question not found")
|
| 24 |
EVENT_FINISHED = errors.New("The event has ended, you were disallowed to do the exam!")
|
| 25 |
EVENT_NOT_STARTED = errors.New("Take it easy, event hasn't starting yet! you cannot do the exam!")
|
| 26 |
EXAMS_SUBMITTED = errors.New("You've submitted the exam, you were diasallowed to answer the question!")
|
| 27 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
import "errors"
|
| 4 |
|
| 5 |
var (
|
| 6 |
+
// ================= GENERAL =================
|
| 7 |
+
BAD_REQUEST_ERROR = errors.New("Invalid Request Format !")
|
| 8 |
+
INTERNAL_SERVER_ERROR = errors.New("Internal Server Error!")
|
| 9 |
+
TIMEOUT = errors.New("Server took to long respond!")
|
| 10 |
+
NOT_FOUND_ERROR = errors.New("Resource not found.")
|
| 11 |
+
DUPLICATE_DATA = errors.New("Duplicate data !")
|
| 12 |
+
INVALID_DATA_PAYLOAD = errors.New("Invalid data payload provided.")
|
| 13 |
+
|
| 14 |
+
// ================= AUTH & ACCOUNT =================
|
| 15 |
+
UNAUTHORIZED = errors.New("Unauthorized, you don't have permission to access this service!")
|
| 16 |
+
EXISTING_ACCOUNT = errors.New("There is existing account!")
|
| 17 |
+
INVALID_TOKEN = errors.New("Invalid Authentication Payload!")
|
| 18 |
+
ACCOUNT_NOT_FOUND = errors.New("There is no account with given credential!")
|
| 19 |
+
WRONG_PASSWORD = errors.New("Your password is wrong for given account credential, please recheck!")
|
| 20 |
+
INVALID_ACCOUNT_DIGITS = errors.New("Your account 3 digits is not found in account number data")
|
| 21 |
+
EXPIRED_TOKEN = errors.New("Token expired")
|
| 22 |
+
INVALID_OTP = errors.New("Invalid OTP Code")
|
| 23 |
+
EMAIL_ALREADY_EXISTS = errors.New("Email already registered")
|
| 24 |
+
|
| 25 |
+
// ================= EVENT & EXAM =================
|
| 26 |
ALREADY_REGISTERED_TO_EVENT = errors.New("Account already registered to this event")
|
|
|
|
| 27 |
NOT_REGISTERED_TO_EVENT = errors.New("Account is not registered to this event")
|
|
|
|
| 28 |
ERR_PROBLEM_SET_NOT_FOUND = errors.New("problem set not found")
|
| 29 |
ERR_QUESTION_NOT_FOUND = errors.New("question not found")
|
| 30 |
EVENT_FINISHED = errors.New("The event has ended, you were disallowed to do the exam!")
|
| 31 |
EVENT_NOT_STARTED = errors.New("Take it easy, event hasn't starting yet! you cannot do the exam!")
|
| 32 |
EXAMS_SUBMITTED = errors.New("You've submitted the exam, you were diasallowed to answer the question!")
|
| 33 |
+
|
| 34 |
+
// ================= FILE UPLOAD =================
|
| 35 |
+
FILE_TOO_LARGE = errors.New("File size exceeds the maximum limit!")
|
| 36 |
+
INVALID_FILE_TYPE = errors.New("File type is not permitted for the selected context.")
|
| 37 |
+
UPLOAD_FAILED = errors.New("Failed to upload file to storage provider.")
|
| 38 |
+
PARTIAL_UPLOAD_FAILURE = errors.New("Some files failed validation or upload.")
|
| 39 |
+
|
| 40 |
+
// ================= ACADEMY =================
|
| 41 |
+
TITLE_REQUIRED = errors.New("Title cannot be empty!")
|
| 42 |
+
SLUG_REQUIRED = errors.New("Slug cannot be empty!")
|
| 43 |
+
ACADEMY_ID_REQUIRED = errors.New("Academy ID is required!")
|
| 44 |
+
MATERIAL_ID_REQUIRED = errors.New("Material ID is required!")
|
| 45 |
+
|
| 46 |
+
ACADEMY_NOT_FOUND = errors.New("Academy not found!")
|
| 47 |
+
MATERIAL_NOT_FOUND = errors.New("Material not found!")
|
| 48 |
+
CONTENT_NOT_FOUND = errors.New("Content not found!")
|
| 49 |
+
ACADEMY_HAS_MATERIALS = errors.New("Cannot delete academy because it still has materials!")
|
| 50 |
+
MATERIAL_HAS_CONTENTS = errors.New("Cannot delete material because it still has contents!")
|
| 51 |
+
)
|
provider/config_provider.go
CHANGED
|
@@ -1,38 +1,59 @@
|
|
| 1 |
-
package provider
|
| 2 |
-
|
| 3 |
-
import "abdanhafidz.com/go-boilerplate/config"
|
| 4 |
-
|
| 5 |
-
type ConfigProvider interface {
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package provider
|
| 2 |
+
|
| 3 |
+
import "abdanhafidz.com/go-boilerplate/config"
|
| 4 |
+
|
| 5 |
+
type ConfigProvider interface {
|
| 6 |
+
ProvideJWTConfig() config.JWTConfig
|
| 7 |
+
ProvideEnvConfig() config.EnvConfig
|
| 8 |
+
ProvideDatabaseConfig() config.DatabaseConfig
|
| 9 |
+
ProvideSupabaseConfig() config.SupabaseConfig
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
type configProvider struct {
|
| 13 |
+
jWTConfig config.JWTConfig
|
| 14 |
+
envConfig config.EnvConfig
|
| 15 |
+
databaseConfig config.DatabaseConfig
|
| 16 |
+
supabaseConfig config.SupabaseConfig
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
func NewConfigProvider() ConfigProvider {
|
| 20 |
+
envConfig := config.NewEnvConfig("Asia/Jakarta")
|
| 21 |
+
jWTConfig := config.NewJWTConfig(envConfig.GetSalt())
|
| 22 |
+
|
| 23 |
+
databaseConfig := config.NewDatabaseConfig(
|
| 24 |
+
envConfig.GetDatabaseHost(),
|
| 25 |
+
envConfig.GetDatabaseUser(),
|
| 26 |
+
envConfig.GetDatabasePassword(),
|
| 27 |
+
envConfig.GetDatabaseName(),
|
| 28 |
+
envConfig.GetDatabasePort(),
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
supabaseConfig := config.NewSupabaseConfig(
|
| 32 |
+
envConfig.GetSupabaseURL(),
|
| 33 |
+
envConfig.GetSupabaseKey(),
|
| 34 |
+
envConfig.GetSupabaseBucket(),
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
return &configProvider{
|
| 38 |
+
jWTConfig: jWTConfig,
|
| 39 |
+
envConfig: envConfig,
|
| 40 |
+
databaseConfig: databaseConfig,
|
| 41 |
+
supabaseConfig: supabaseConfig,
|
| 42 |
+
}
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
func (c *configProvider) ProvideJWTConfig() config.JWTConfig {
|
| 46 |
+
return c.jWTConfig
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
func (c *configProvider) ProvideEnvConfig() config.EnvConfig {
|
| 50 |
+
return c.envConfig
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
func (c *configProvider) ProvideDatabaseConfig() config.DatabaseConfig {
|
| 54 |
+
return c.databaseConfig
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
func (c *configProvider) ProvideSupabaseConfig() config.SupabaseConfig {
|
| 58 |
+
return c.supabaseConfig
|
| 59 |
+
}
|
provider/controller_provider.go
CHANGED
|
@@ -1,90 +1,106 @@
|
|
| 1 |
-
package provider
|
| 2 |
-
|
| 3 |
-
import "abdanhafidz.com/go-boilerplate/controllers"
|
| 4 |
-
|
| 5 |
-
type ControllerProvider interface {
|
| 6 |
-
ProvideAcademyController() controllers.AcademyController
|
| 7 |
-
ProvideAccountDetailController() controllers.AccountDetailController
|
| 8 |
-
ProvideAuthenticationController() controllers.AuthenticationController
|
| 9 |
-
ProvideEmailVerificationController() controllers.EmailVerificationController
|
| 10 |
-
ProvideEventController() controllers.EventController
|
| 11 |
-
ProvideExamController() controllers.ExamController
|
| 12 |
-
ProvideForgotPasswordController() controllers.ForgotPasswordController
|
| 13 |
-
ProvideOptionController() controllers.OptionController
|
| 14 |
-
ProvideRegionController() controllers.RegionController
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
func (c *controllerProvider)
|
| 68 |
-
return c.
|
| 69 |
-
}
|
| 70 |
-
|
| 71 |
-
func (c *controllerProvider)
|
| 72 |
-
return c.
|
| 73 |
-
}
|
| 74 |
-
|
| 75 |
-
func (c *controllerProvider)
|
| 76 |
-
return c.
|
| 77 |
-
}
|
| 78 |
-
|
| 79 |
-
func (c *controllerProvider)
|
| 80 |
-
return c.
|
| 81 |
-
}
|
| 82 |
-
|
| 83 |
-
func (c *controllerProvider)
|
| 84 |
-
return c.
|
| 85 |
-
}
|
| 86 |
-
|
| 87 |
-
func (c *controllerProvider)
|
| 88 |
-
return c.
|
| 89 |
-
}
|
| 90 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package provider
|
| 2 |
+
|
| 3 |
+
import "abdanhafidz.com/go-boilerplate/controllers"
|
| 4 |
+
|
| 5 |
+
type ControllerProvider interface {
|
| 6 |
+
ProvideAcademyController() controllers.AcademyController
|
| 7 |
+
ProvideAccountDetailController() controllers.AccountDetailController
|
| 8 |
+
ProvideAuthenticationController() controllers.AuthenticationController
|
| 9 |
+
ProvideEmailVerificationController() controllers.EmailVerificationController
|
| 10 |
+
ProvideEventController() controllers.EventController
|
| 11 |
+
ProvideExamController() controllers.ExamController
|
| 12 |
+
ProvideForgotPasswordController() controllers.ForgotPasswordController
|
| 13 |
+
ProvideOptionController() controllers.OptionController
|
| 14 |
+
ProvideRegionController() controllers.RegionController
|
| 15 |
+
|
| 16 |
+
// UPDATE: Menggunakan Pointer (*)
|
| 17 |
+
ProvideUploadController() *controllers.UploadController
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
type controllerProvider struct {
|
| 21 |
+
academyController controllers.AcademyController
|
| 22 |
+
accountDetailController controllers.AccountDetailController
|
| 23 |
+
authenticationController controllers.AuthenticationController
|
| 24 |
+
emailVerificationController controllers.EmailVerificationController
|
| 25 |
+
eventController controllers.EventController
|
| 26 |
+
examController controllers.ExamController
|
| 27 |
+
forgotPasswordController controllers.ForgotPasswordController
|
| 28 |
+
optionController controllers.OptionController
|
| 29 |
+
regionController controllers.RegionController
|
| 30 |
+
|
| 31 |
+
// UPDATE: Menggunakan Pointer (*)
|
| 32 |
+
uploadController *controllers.UploadController
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
func NewControllerProvider(servicesProvider ServicesProvider) ControllerProvider {
|
| 36 |
+
|
| 37 |
+
academyController := controllers.NewAcademyController(servicesProvider.ProvideAcademyService())
|
| 38 |
+
accountDetailController := controllers.NewAccountDetailController(servicesProvider.ProvideAccountService())
|
| 39 |
+
authenticationController := controllers.NewAuthenticationController(servicesProvider.ProvideAccountService(), servicesProvider.ProvideExternalAuthService())
|
| 40 |
+
emailVerificationController := controllers.NewEmailVerificationController(servicesProvider.ProvideEmailVerificationService())
|
| 41 |
+
eventController := controllers.NewEventController(servicesProvider.ProvideEventService())
|
| 42 |
+
examController := controllers.NewExamController(servicesProvider.ProvideExamService())
|
| 43 |
+
forgotPasswordController := controllers.NewForgotPasswordController(servicesProvider.ProvideForgotPasswordService())
|
| 44 |
+
optionController := controllers.NewOptionController(servicesProvider.ProvideOptionService())
|
| 45 |
+
regionController := controllers.NewRegionController(servicesProvider.ProvideRegionService())
|
| 46 |
+
|
| 47 |
+
// UPDATE: Inisialisasi Upload Controller
|
| 48 |
+
// servicesProvider.ProvideUploadService() sekarang sudah return Pointer (*), jadi aman.
|
| 49 |
+
uploadController := controllers.NewUploadController(servicesProvider.ProvideUploadService())
|
| 50 |
+
|
| 51 |
+
return &controllerProvider{
|
| 52 |
+
academyController: academyController,
|
| 53 |
+
accountDetailController: accountDetailController,
|
| 54 |
+
authenticationController: authenticationController,
|
| 55 |
+
emailVerificationController: emailVerificationController,
|
| 56 |
+
eventController: eventController,
|
| 57 |
+
examController: examController,
|
| 58 |
+
forgotPasswordController: forgotPasswordController,
|
| 59 |
+
optionController: optionController,
|
| 60 |
+
regionController: regionController,
|
| 61 |
+
uploadController: uploadController, // Pointer assign ke Pointer
|
| 62 |
+
}
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
// --- Getter Methods ---
|
| 66 |
+
|
| 67 |
+
func (c *controllerProvider) ProvideAcademyController() controllers.AcademyController {
|
| 68 |
+
return c.academyController
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
func (c *controllerProvider) ProvideAccountDetailController() controllers.AccountDetailController {
|
| 72 |
+
return c.accountDetailController
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
func (c *controllerProvider) ProvideAuthenticationController() controllers.AuthenticationController {
|
| 76 |
+
return c.authenticationController
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
func (c *controllerProvider) ProvideEmailVerificationController() controllers.EmailVerificationController {
|
| 80 |
+
return c.emailVerificationController
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
func (c *controllerProvider) ProvideEventController() controllers.EventController {
|
| 84 |
+
return c.eventController
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
func (c *controllerProvider) ProvideExamController() controllers.ExamController {
|
| 88 |
+
return c.examController
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
func (c *controllerProvider) ProvideForgotPasswordController() controllers.ForgotPasswordController {
|
| 92 |
+
return c.forgotPasswordController
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
func (c *controllerProvider) ProvideOptionController() controllers.OptionController {
|
| 96 |
+
return c.optionController
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
func (c *controllerProvider) ProvideRegionController() controllers.RegionController {
|
| 100 |
+
return c.regionController
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
// UPDATE: Return Pointer (*)
|
| 104 |
+
func (c *controllerProvider) ProvideUploadController() *controllers.UploadController {
|
| 105 |
+
return c.uploadController
|
| 106 |
+
}
|
provider/middleware_provider.go
CHANGED
|
@@ -1,30 +1,30 @@
|
|
| 1 |
-
package provider
|
| 2 |
-
|
| 3 |
-
import "abdanhafidz.com/go-boilerplate/middleware"
|
| 4 |
-
|
| 5 |
-
type MiddlewareProvider interface {
|
| 6 |
-
ProvideAuthenticationMiddleware() middleware.AuthenticationMiddleware
|
| 7 |
-
ProvideAuthorizationMiddleware() middleware.AuthorizationMiddleware
|
| 8 |
-
}
|
| 9 |
-
|
| 10 |
-
type middlewareProvider struct {
|
| 11 |
-
authenticationMiddleware middleware.AuthenticationMiddleware
|
| 12 |
-
authorizationMiddleware middleware.AuthorizationMiddleware
|
| 13 |
-
}
|
| 14 |
-
|
| 15 |
-
func NewMiddlewareProvider(servicesProvider ServicesProvider) MiddlewareProvider {
|
| 16 |
-
authenticationMiddleware := middleware.NewAuthenticationMiddleware(servicesProvider.ProvideJWTService())
|
| 17 |
-
authorizationMiddleware := middleware.NewAuthorizationMiddleware(servicesProvider.ProvideEventService())
|
| 18 |
-
return &middlewareProvider{
|
| 19 |
-
authenticationMiddleware: authenticationMiddleware,
|
| 20 |
-
authorizationMiddleware: authorizationMiddleware,
|
| 21 |
-
}
|
| 22 |
-
}
|
| 23 |
-
|
| 24 |
-
func (p *middlewareProvider) ProvideAuthenticationMiddleware() middleware.AuthenticationMiddleware {
|
| 25 |
-
return p.authenticationMiddleware
|
| 26 |
-
}
|
| 27 |
-
|
| 28 |
-
func (p *middlewareProvider) ProvideAuthorizationMiddleware() middleware.AuthorizationMiddleware {
|
| 29 |
-
return p.authorizationMiddleware
|
| 30 |
-
}
|
|
|
|
| 1 |
+
package provider
|
| 2 |
+
|
| 3 |
+
import "abdanhafidz.com/go-boilerplate/middleware"
|
| 4 |
+
|
| 5 |
+
type MiddlewareProvider interface {
|
| 6 |
+
ProvideAuthenticationMiddleware() middleware.AuthenticationMiddleware
|
| 7 |
+
ProvideAuthorizationMiddleware() middleware.AuthorizationMiddleware
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
type middlewareProvider struct {
|
| 11 |
+
authenticationMiddleware middleware.AuthenticationMiddleware
|
| 12 |
+
authorizationMiddleware middleware.AuthorizationMiddleware
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
func NewMiddlewareProvider(servicesProvider ServicesProvider) MiddlewareProvider {
|
| 16 |
+
authenticationMiddleware := middleware.NewAuthenticationMiddleware(servicesProvider.ProvideJWTService())
|
| 17 |
+
authorizationMiddleware := middleware.NewAuthorizationMiddleware(servicesProvider.ProvideEventService())
|
| 18 |
+
return &middlewareProvider{
|
| 19 |
+
authenticationMiddleware: authenticationMiddleware,
|
| 20 |
+
authorizationMiddleware: authorizationMiddleware,
|
| 21 |
+
}
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
func (p *middlewareProvider) ProvideAuthenticationMiddleware() middleware.AuthenticationMiddleware {
|
| 25 |
+
return p.authenticationMiddleware
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
func (p *middlewareProvider) ProvideAuthorizationMiddleware() middleware.AuthorizationMiddleware {
|
| 29 |
+
return p.authorizationMiddleware
|
| 30 |
+
}
|
provider/provider.go
CHANGED
|
@@ -1,87 +1,102 @@
|
|
| 1 |
package provider
|
| 2 |
|
| 3 |
import (
|
| 4 |
-
|
| 5 |
-
|
|
|
|
| 6 |
)
|
| 7 |
|
| 8 |
type AppProvider interface {
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
}
|
|
|
|
| 16 |
type appProvider struct {
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
}
|
| 24 |
|
| 25 |
func NewAppProvider() AppProvider {
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
}
|
|
|
|
| 82 |
func (a *appProvider) ProvideRouter() *gin.Engine {
|
| 83 |
return a.ginRouter
|
| 84 |
}
|
|
|
|
| 85 |
func (a *appProvider) ProvideConfig() ConfigProvider {
|
| 86 |
return a.configProvider
|
| 87 |
}
|
|
@@ -100,4 +115,4 @@ func (a *appProvider) ProvideControllers() ControllerProvider {
|
|
| 100 |
|
| 101 |
func (a *appProvider) ProvideMiddlewares() MiddlewareProvider {
|
| 102 |
return a.middlewareProvider
|
| 103 |
-
}
|
|
|
|
| 1 |
package provider
|
| 2 |
|
| 3 |
import (
|
| 4 |
+
"log"
|
| 5 |
+
entity "abdanhafidz.com/go-boilerplate/models/entity"
|
| 6 |
+
"github.com/gin-gonic/gin"
|
| 7 |
)
|
| 8 |
|
| 9 |
type AppProvider interface {
|
| 10 |
+
ProvideRouter() *gin.Engine
|
| 11 |
+
ProvideConfig() ConfigProvider
|
| 12 |
+
ProvideRepositories() RepositoriesProvider
|
| 13 |
+
ProvideServices() ServicesProvider
|
| 14 |
+
ProvideControllers() ControllerProvider
|
| 15 |
+
ProvideMiddlewares() MiddlewareProvider
|
| 16 |
}
|
| 17 |
+
|
| 18 |
type appProvider struct {
|
| 19 |
+
ginRouter *gin.Engine
|
| 20 |
+
configProvider ConfigProvider
|
| 21 |
+
repositoriesProvider RepositoriesProvider
|
| 22 |
+
servicesProvider ServicesProvider
|
| 23 |
+
controllerProvider ControllerProvider
|
| 24 |
+
middlewareProvider MiddlewareProvider
|
| 25 |
}
|
| 26 |
|
| 27 |
func NewAppProvider() AppProvider {
|
| 28 |
+
ginRouter := gin.Default()
|
| 29 |
+
configProvider := NewConfigProvider()
|
| 30 |
+
repositoriesProvider := NewRepositoriesProvider(configProvider)
|
| 31 |
+
supabaseCfg := configProvider.ProvideSupabaseConfig()
|
| 32 |
+
storageDriver := NewSupabaseStorage(supabaseCfg.URL, supabaseCfg.ServiceKey, supabaseCfg.BucketName)
|
| 33 |
+
servicesProvider := NewServicesProvider(repositoriesProvider, configProvider, storageDriver)
|
| 34 |
+
controllerProvider := NewControllerProvider(servicesProvider)
|
| 35 |
+
middlewareProvider := NewMiddlewareProvider(servicesProvider)
|
| 36 |
+
|
| 37 |
+
// Database Migrations with error handling
|
| 38 |
+
err := configProvider.ProvideDatabaseConfig().AutoMigrateAll(
|
| 39 |
+
// Accounts & Auth
|
| 40 |
+
&entity.Account{},
|
| 41 |
+
&entity.AccountDetail{},
|
| 42 |
+
&entity.EmailVerification{},
|
| 43 |
+
&entity.ExternalAuth{},
|
| 44 |
+
&entity.FCM{},
|
| 45 |
+
&entity.ForgotPassword{},
|
| 46 |
+
|
| 47 |
+
// Events
|
| 48 |
+
&entity.Events{},
|
| 49 |
+
&entity.EventAssign{},
|
| 50 |
+
&entity.Announcement{},
|
| 51 |
+
|
| 52 |
+
// Problemset & Exam
|
| 53 |
+
&entity.ProblemSet{},
|
| 54 |
+
&entity.Questions{},
|
| 55 |
+
&entity.Exam{},
|
| 56 |
+
&entity.ProblemSetExamAssign{},
|
| 57 |
+
&entity.ExamEventAssign{},
|
| 58 |
+
|
| 59 |
+
// Exam Attempt & Result
|
| 60 |
+
&entity.ExamEventAnswer{},
|
| 61 |
+
&entity.ExamEventAttempt{},
|
| 62 |
+
&entity.Result{},
|
| 63 |
+
|
| 64 |
+
// Academy LMS
|
| 65 |
+
&entity.Academy{},
|
| 66 |
+
&entity.AcademyMaterial{},
|
| 67 |
+
&entity.AcademyContent{},
|
| 68 |
+
&entity.AcademyMaterialProgress{},
|
| 69 |
+
&entity.AcademyContentProgress{},
|
| 70 |
+
&entity.AcademyProgress{},
|
| 71 |
+
|
| 72 |
+
// Options & Regions
|
| 73 |
+
&entity.OptionCategory{},
|
| 74 |
+
&entity.OptionValues{},
|
| 75 |
+
&entity.RegionProvince{},
|
| 76 |
+
&entity.RegionCity{},
|
| 77 |
+
|
| 78 |
+
// Files Storage
|
| 79 |
+
&entity.File{},
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
+
if err != nil {
|
| 83 |
+
log.Fatalf("Database migration failed: %v", err)
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
return &appProvider{
|
| 87 |
+
ginRouter: ginRouter,
|
| 88 |
+
configProvider: configProvider,
|
| 89 |
+
repositoriesProvider: repositoriesProvider,
|
| 90 |
+
servicesProvider: servicesProvider,
|
| 91 |
+
controllerProvider: controllerProvider,
|
| 92 |
+
middlewareProvider: middlewareProvider,
|
| 93 |
+
}
|
| 94 |
}
|
| 95 |
+
|
| 96 |
func (a *appProvider) ProvideRouter() *gin.Engine {
|
| 97 |
return a.ginRouter
|
| 98 |
}
|
| 99 |
+
|
| 100 |
func (a *appProvider) ProvideConfig() ConfigProvider {
|
| 101 |
return a.configProvider
|
| 102 |
}
|
|
|
|
| 115 |
|
| 116 |
func (a *appProvider) ProvideMiddlewares() MiddlewareProvider {
|
| 117 |
return a.middlewareProvider
|
| 118 |
+
}
|
provider/repositories_provider.go
CHANGED
|
@@ -1,170 +1,178 @@
|
|
| 1 |
-
package provider
|
| 2 |
-
|
| 3 |
-
import "abdanhafidz.com/go-boilerplate/repositories"
|
| 4 |
-
|
| 5 |
-
type RepositoriesProvider interface {
|
| 6 |
-
ProvideAcademyRepository() repositories.AcademyRepository
|
| 7 |
-
ProvideAccountDetailRepository() repositories.AccountDetailRepository
|
| 8 |
-
ProvideAccountRepository() repositories.AccountRepository
|
| 9 |
-
ProvideEmailVerificationRepository() repositories.EmailVerificationRepository
|
| 10 |
-
ProvideEventAssignRepository() repositories.EventAssignRepository
|
| 11 |
-
ProvideEventsRepository() repositories.EventsRepository
|
| 12 |
-
ProvideExamEventAnswerRepository() repositories.ExamEventAnswerRepository
|
| 13 |
-
ProvideExamEventAssignRepository() repositories.ExamEventAssignRepository
|
| 14 |
-
ProvideExamEventAttemptRepository() repositories.ExamEventAttemptRepository
|
| 15 |
-
ProvideExamRepository() repositories.ExamRepository
|
| 16 |
-
ProvideExternalAuthRepository() repositories.ExternalAuthRepository
|
| 17 |
-
ProvideFCMRepository() repositories.FCMRepository
|
| 18 |
-
ProvideForgotPasswordRepository() repositories.ForgotPasswordRepository
|
| 19 |
-
ProvideOptionRepository() repositories.OptionRepository
|
| 20 |
-
ProvideProblemSetExamAssignRepository() repositories.ProblemSetExamAssignRepository
|
| 21 |
-
ProvideProblemSetRepository() repositories.ProblemSetRepository
|
| 22 |
-
ProvideQuestionsRepository() repositories.QuestionsRepository
|
| 23 |
-
ProvideRegionRepository() repositories.RegionRepository
|
| 24 |
-
ProvideResultRepository() repositories.ResultRepository
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
}
|
| 99 |
-
|
| 100 |
-
func (r *repositoriesProvider)
|
| 101 |
-
return r.
|
| 102 |
-
}
|
| 103 |
-
|
| 104 |
-
func (r *repositoriesProvider)
|
| 105 |
-
return r.
|
| 106 |
-
}
|
| 107 |
-
|
| 108 |
-
func (r *repositoriesProvider)
|
| 109 |
-
return r.
|
| 110 |
-
}
|
| 111 |
-
|
| 112 |
-
func (r *repositoriesProvider)
|
| 113 |
-
return r.
|
| 114 |
-
}
|
| 115 |
-
|
| 116 |
-
func (r *repositoriesProvider)
|
| 117 |
-
return r.
|
| 118 |
-
}
|
| 119 |
-
|
| 120 |
-
func (r *repositoriesProvider)
|
| 121 |
-
return r.
|
| 122 |
-
}
|
| 123 |
-
|
| 124 |
-
func (r *repositoriesProvider)
|
| 125 |
-
return r.
|
| 126 |
-
}
|
| 127 |
-
|
| 128 |
-
func (r *repositoriesProvider)
|
| 129 |
-
return r.
|
| 130 |
-
}
|
| 131 |
-
|
| 132 |
-
func (r *repositoriesProvider)
|
| 133 |
-
return r.
|
| 134 |
-
}
|
| 135 |
-
|
| 136 |
-
func (r *repositoriesProvider)
|
| 137 |
-
return r.
|
| 138 |
-
}
|
| 139 |
-
|
| 140 |
-
func (r *repositoriesProvider)
|
| 141 |
-
return r.
|
| 142 |
-
}
|
| 143 |
-
|
| 144 |
-
func (r *repositoriesProvider)
|
| 145 |
-
return r.
|
| 146 |
-
}
|
| 147 |
-
|
| 148 |
-
func (r *repositoriesProvider)
|
| 149 |
-
return r.
|
| 150 |
-
}
|
| 151 |
-
|
| 152 |
-
func (r *repositoriesProvider)
|
| 153 |
-
return r.
|
| 154 |
-
}
|
| 155 |
-
|
| 156 |
-
func (r *repositoriesProvider)
|
| 157 |
-
return r.
|
| 158 |
-
}
|
| 159 |
-
|
| 160 |
-
func (r *repositoriesProvider)
|
| 161 |
-
return r.
|
| 162 |
-
}
|
| 163 |
-
|
| 164 |
-
func (r *repositoriesProvider)
|
| 165 |
-
return r.
|
| 166 |
-
}
|
| 167 |
-
|
| 168 |
-
func (r *repositoriesProvider)
|
| 169 |
-
return r.
|
| 170 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package provider
|
| 2 |
+
|
| 3 |
+
import "abdanhafidz.com/go-boilerplate/repositories"
|
| 4 |
+
|
| 5 |
+
type RepositoriesProvider interface {
|
| 6 |
+
ProvideAcademyRepository() repositories.AcademyRepository
|
| 7 |
+
ProvideAccountDetailRepository() repositories.AccountDetailRepository
|
| 8 |
+
ProvideAccountRepository() repositories.AccountRepository
|
| 9 |
+
ProvideEmailVerificationRepository() repositories.EmailVerificationRepository
|
| 10 |
+
ProvideEventAssignRepository() repositories.EventAssignRepository
|
| 11 |
+
ProvideEventsRepository() repositories.EventsRepository
|
| 12 |
+
ProvideExamEventAnswerRepository() repositories.ExamEventAnswerRepository
|
| 13 |
+
ProvideExamEventAssignRepository() repositories.ExamEventAssignRepository
|
| 14 |
+
ProvideExamEventAttemptRepository() repositories.ExamEventAttemptRepository
|
| 15 |
+
ProvideExamRepository() repositories.ExamRepository
|
| 16 |
+
ProvideExternalAuthRepository() repositories.ExternalAuthRepository
|
| 17 |
+
ProvideFCMRepository() repositories.FCMRepository
|
| 18 |
+
ProvideForgotPasswordRepository() repositories.ForgotPasswordRepository
|
| 19 |
+
ProvideOptionRepository() repositories.OptionRepository
|
| 20 |
+
ProvideProblemSetExamAssignRepository() repositories.ProblemSetExamAssignRepository
|
| 21 |
+
ProvideProblemSetRepository() repositories.ProblemSetRepository
|
| 22 |
+
ProvideQuestionsRepository() repositories.QuestionsRepository
|
| 23 |
+
ProvideRegionRepository() repositories.RegionRepository
|
| 24 |
+
ProvideResultRepository() repositories.ResultRepository
|
| 25 |
+
ProvideFileRepository() repositories.FileRepository
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
type repositoriesProvider struct {
|
| 29 |
+
academyRepository repositories.AcademyRepository
|
| 30 |
+
accountDetailRepository repositories.AccountDetailRepository
|
| 31 |
+
accountRepository repositories.AccountRepository
|
| 32 |
+
emailVerificationRepository repositories.EmailVerificationRepository
|
| 33 |
+
eventAssignRepository repositories.EventAssignRepository
|
| 34 |
+
eventsRepository repositories.EventsRepository
|
| 35 |
+
examEventAnswerRepository repositories.ExamEventAnswerRepository
|
| 36 |
+
examEventAssignRepository repositories.ExamEventAssignRepository
|
| 37 |
+
examEventAttemptRepository repositories.ExamEventAttemptRepository
|
| 38 |
+
examRepository repositories.ExamRepository
|
| 39 |
+
externalAuthRepository repositories.ExternalAuthRepository
|
| 40 |
+
fCMRepository repositories.FCMRepository
|
| 41 |
+
forgotPasswordRepository repositories.ForgotPasswordRepository
|
| 42 |
+
optionRepository repositories.OptionRepository
|
| 43 |
+
problemSetExamAssignRepository repositories.ProblemSetExamAssignRepository
|
| 44 |
+
problemSetRepository repositories.ProblemSetRepository
|
| 45 |
+
questionsRepository repositories.QuestionsRepository
|
| 46 |
+
regionRepository repositories.RegionRepository
|
| 47 |
+
resultRepository repositories.ResultRepository
|
| 48 |
+
fileRepository repositories.FileRepository // Added field
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
func NewRepositoriesProvider(cfg ConfigProvider) RepositoriesProvider {
|
| 52 |
+
dbConfig := cfg.ProvideDatabaseConfig()
|
| 53 |
+
db := dbConfig.GetInstance()
|
| 54 |
+
|
| 55 |
+
academyRepository := repositories.NewAcademyRepository(db)
|
| 56 |
+
accountDetailRepository := repositories.NewAccountDetailRepository(db)
|
| 57 |
+
accountRepository := repositories.NewAccountRepository(db)
|
| 58 |
+
emailVerificationRepository := repositories.NewEmailVerificationRepository(db)
|
| 59 |
+
eventAssignRepository := repositories.NewEventAssignRepository(db)
|
| 60 |
+
eventsRepository := repositories.NewEventsRepository(db)
|
| 61 |
+
examEventAnswerRepository := repositories.NewExamEventAnswerRepository(db)
|
| 62 |
+
examEventAssignRepository := repositories.NewExamEventAssignRepository(db)
|
| 63 |
+
examEventAttemptRepository := repositories.NewExamEventAttemptRepository(db)
|
| 64 |
+
examRepository := repositories.NewExamRepository(db)
|
| 65 |
+
externalAuthRepository := repositories.NewExternalAuthRepository(db)
|
| 66 |
+
fCMRepository := repositories.NewFCMRepository(db)
|
| 67 |
+
forgotPasswordRepository := repositories.NewForgotPasswordRepository(db)
|
| 68 |
+
optionRepository := repositories.NewOptionRepository(db)
|
| 69 |
+
problemSetExamAssignRepository := repositories.NewProblemSetExamAssignRepository(db)
|
| 70 |
+
problemSetRepository := repositories.NewProblemSetRepository(db)
|
| 71 |
+
questionsRepository := repositories.NewQuestionsRepository(db)
|
| 72 |
+
regionRepository := repositories.NewRegionRepository(db)
|
| 73 |
+
resultRepository := repositories.NewResultRepository(db)
|
| 74 |
+
fileRepository := repositories.NewFileRepository(db) // Init here
|
| 75 |
+
|
| 76 |
+
return &repositoriesProvider{
|
| 77 |
+
academyRepository: academyRepository,
|
| 78 |
+
accountDetailRepository: accountDetailRepository,
|
| 79 |
+
accountRepository: accountRepository,
|
| 80 |
+
emailVerificationRepository: emailVerificationRepository,
|
| 81 |
+
eventAssignRepository: eventAssignRepository,
|
| 82 |
+
eventsRepository: eventsRepository,
|
| 83 |
+
examEventAnswerRepository: examEventAnswerRepository,
|
| 84 |
+
examEventAssignRepository: examEventAssignRepository,
|
| 85 |
+
examEventAttemptRepository: examEventAttemptRepository,
|
| 86 |
+
examRepository: examRepository,
|
| 87 |
+
externalAuthRepository: externalAuthRepository,
|
| 88 |
+
fCMRepository: fCMRepository,
|
| 89 |
+
forgotPasswordRepository: forgotPasswordRepository,
|
| 90 |
+
optionRepository: optionRepository,
|
| 91 |
+
problemSetExamAssignRepository: problemSetExamAssignRepository,
|
| 92 |
+
problemSetRepository: problemSetRepository,
|
| 93 |
+
questionsRepository: questionsRepository,
|
| 94 |
+
regionRepository: regionRepository,
|
| 95 |
+
resultRepository: resultRepository,
|
| 96 |
+
fileRepository: fileRepository, // Assign here
|
| 97 |
+
}
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
func (r *repositoriesProvider) ProvideAcademyRepository() repositories.AcademyRepository {
|
| 101 |
+
return r.academyRepository
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
func (r *repositoriesProvider) ProvideAccountDetailRepository() repositories.AccountDetailRepository {
|
| 105 |
+
return r.accountDetailRepository
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
func (r *repositoriesProvider) ProvideAccountRepository() repositories.AccountRepository {
|
| 109 |
+
return r.accountRepository
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
func (r *repositoriesProvider) ProvideEmailVerificationRepository() repositories.EmailVerificationRepository {
|
| 113 |
+
return r.emailVerificationRepository
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
func (r *repositoriesProvider) ProvideEventAssignRepository() repositories.EventAssignRepository {
|
| 117 |
+
return r.eventAssignRepository
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
func (r *repositoriesProvider) ProvideEventsRepository() repositories.EventsRepository {
|
| 121 |
+
return r.eventsRepository
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
func (r *repositoriesProvider) ProvideExamEventAnswerRepository() repositories.ExamEventAnswerRepository {
|
| 125 |
+
return r.examEventAnswerRepository
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
func (r *repositoriesProvider) ProvideExamEventAssignRepository() repositories.ExamEventAssignRepository {
|
| 129 |
+
return r.examEventAssignRepository
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
func (r *repositoriesProvider) ProvideExamEventAttemptRepository() repositories.ExamEventAttemptRepository {
|
| 133 |
+
return r.examEventAttemptRepository
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
func (r *repositoriesProvider) ProvideExamRepository() repositories.ExamRepository {
|
| 137 |
+
return r.examRepository
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
func (r *repositoriesProvider) ProvideExternalAuthRepository() repositories.ExternalAuthRepository {
|
| 141 |
+
return r.externalAuthRepository
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
func (r *repositoriesProvider) ProvideFCMRepository() repositories.FCMRepository {
|
| 145 |
+
return r.fCMRepository
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
func (r *repositoriesProvider) ProvideForgotPasswordRepository() repositories.ForgotPasswordRepository {
|
| 149 |
+
return r.forgotPasswordRepository
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
func (r *repositoriesProvider) ProvideOptionRepository() repositories.OptionRepository {
|
| 153 |
+
return r.optionRepository
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
func (r *repositoriesProvider) ProvideProblemSetExamAssignRepository() repositories.ProblemSetExamAssignRepository {
|
| 157 |
+
return r.problemSetExamAssignRepository
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
func (r *repositoriesProvider) ProvideProblemSetRepository() repositories.ProblemSetRepository {
|
| 161 |
+
return r.problemSetRepository
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
func (r *repositoriesProvider) ProvideQuestionsRepository() repositories.QuestionsRepository {
|
| 165 |
+
return r.questionsRepository
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
func (r *repositoriesProvider) ProvideRegionRepository() repositories.RegionRepository {
|
| 169 |
+
return r.regionRepository
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
func (r *repositoriesProvider) ProvideResultRepository() repositories.ResultRepository {
|
| 173 |
+
return r.resultRepository
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
func (r *repositoriesProvider) ProvideFileRepository() repositories.FileRepository {
|
| 177 |
+
return r.fileRepository
|
| 178 |
+
}
|
provider/services_provider.go
CHANGED
|
@@ -1,103 +1,121 @@
|
|
| 1 |
-
package provider
|
| 2 |
-
|
| 3 |
-
import
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
func (s *servicesProvider)
|
| 86 |
-
|
| 87 |
-
}
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
func (s *servicesProvider)
|
| 98 |
-
|
| 99 |
-
}
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package provider
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
// Pastikan path ini sesuai dengan go.mod kamu (misal: quzuu-backend-v2/services)
|
| 5 |
+
"abdanhafidz.com/go-boilerplate/services"
|
| 6 |
+
)
|
| 7 |
+
|
| 8 |
+
type ServicesProvider interface {
|
| 9 |
+
ProvideEventService() services.EventService
|
| 10 |
+
ProvideAcademyService() services.AcademyService
|
| 11 |
+
ProvideProblemSetService() services.ProblemSetService
|
| 12 |
+
ProvideJWTService() services.JWTService
|
| 13 |
+
ProvideRegionService() services.RegionService
|
| 14 |
+
ProvideOptionService() services.OptionService
|
| 15 |
+
ProvideExamService() services.ExamService
|
| 16 |
+
ProvideAccountService() services.AccountService
|
| 17 |
+
ProvideForgotPasswordService() services.ForgotPasswordService
|
| 18 |
+
ProvideEmailVerificationService() services.EmailVerificationService
|
| 19 |
+
ProvideExternalAuthService() services.ExternalAuthService
|
| 20 |
+
|
| 21 |
+
// UPDATE: Mengembalikan pointer karena UploadService adalah struct, bukan interface
|
| 22 |
+
ProvideUploadService() *services.UploadService
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
type servicesProvider struct {
|
| 26 |
+
eventService services.EventService
|
| 27 |
+
academyService services.AcademyService
|
| 28 |
+
problemSetService services.ProblemSetService
|
| 29 |
+
jWTService services.JWTService
|
| 30 |
+
regionService services.RegionService
|
| 31 |
+
optionService services.OptionService
|
| 32 |
+
examService services.ExamService
|
| 33 |
+
accountService services.AccountService
|
| 34 |
+
forgotPasswordService services.ForgotPasswordService
|
| 35 |
+
emailVerificationService services.EmailVerificationService
|
| 36 |
+
externalAuthService services.ExternalAuthService
|
| 37 |
+
|
| 38 |
+
// Field untuk menyimpan instance UploadService
|
| 39 |
+
uploadService *services.UploadService
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
func NewServicesProvider(
|
| 43 |
+
repoProvider RepositoriesProvider,
|
| 44 |
+
configProvider ConfigProvider,
|
| 45 |
+
storageProvider services.StorageProvider, // Didapat dari main/wire
|
| 46 |
+
) ServicesProvider {
|
| 47 |
+
|
| 48 |
+
// Inisialisasi service lain...
|
| 49 |
+
eventService := services.NewEventService(repoProvider.ProvideEventsRepository(), repoProvider.ProvideEventAssignRepository())
|
| 50 |
+
academyService := services.NewAcademyService(repoProvider.ProvideAcademyRepository())
|
| 51 |
+
problemSetService := services.NewProblemSetService(repoProvider.ProvideProblemSetRepository(), repoProvider.ProvideQuestionsRepository(), repoProvider.ProvideProblemSetExamAssignRepository())
|
| 52 |
+
jWTService := services.NewJWTService(configProvider.ProvideJWTConfig().GetSecretKey())
|
| 53 |
+
regionService := services.NewRegionService(repoProvider.ProvideRegionRepository())
|
| 54 |
+
optionService := services.NewOptionService(repoProvider.ProvideOptionRepository())
|
| 55 |
+
examService := services.NewExamService(eventService, problemSetService, repoProvider.ProvideProblemSetExamAssignRepository(), repoProvider.ProvideExamRepository(), repoProvider.ProvideExamEventAttemptRepository(), repoProvider.ProvideExamEventAssignRepository(), repoProvider.ProvideExamEventAnswerRepository(), repoProvider.ProvideResultRepository())
|
| 56 |
+
accountService := services.NewAccountService(jWTService, repoProvider.ProvideAccountRepository(), repoProvider.ProvideAccountDetailRepository())
|
| 57 |
+
forgotPasswordService := services.NewForgotPasswordService(jWTService, repoProvider.ProvideAccountRepository(), repoProvider.ProvideForgotPasswordRepository())
|
| 58 |
+
emailVerificationService := services.NewEmailVerificationService(accountService, repoProvider.ProvideEmailVerificationRepository())
|
| 59 |
+
externalAuthService := services.NewExternalAuthService(jWTService, accountService, repoProvider.ProvideExternalAuthRepository())
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
uploadService := services.NewUploadService(
|
| 63 |
+
storageProvider,
|
| 64 |
+
repoProvider.ProvideFileRepository(),
|
| 65 |
+
)
|
| 66 |
+
|
| 67 |
+
return &servicesProvider{
|
| 68 |
+
eventService: eventService,
|
| 69 |
+
academyService: academyService,
|
| 70 |
+
problemSetService: problemSetService,
|
| 71 |
+
jWTService: jWTService,
|
| 72 |
+
regionService: regionService,
|
| 73 |
+
optionService: optionService,
|
| 74 |
+
examService: examService,
|
| 75 |
+
accountService: accountService,
|
| 76 |
+
forgotPasswordService: forgotPasswordService,
|
| 77 |
+
emailVerificationService: emailVerificationService,
|
| 78 |
+
externalAuthService: externalAuthService,
|
| 79 |
+
uploadService: uploadService,
|
| 80 |
+
}
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
// ... (Getter method lainnya tetap sama) ...
|
| 84 |
+
|
| 85 |
+
func (s *servicesProvider) ProvideEventService() services.EventService {
|
| 86 |
+
return s.eventService
|
| 87 |
+
}
|
| 88 |
+
func (s *servicesProvider) ProvideAcademyService() services.AcademyService {
|
| 89 |
+
return s.academyService
|
| 90 |
+
}
|
| 91 |
+
func (s *servicesProvider) ProvideProblemSetService() services.ProblemSetService {
|
| 92 |
+
return s.problemSetService
|
| 93 |
+
}
|
| 94 |
+
func (s *servicesProvider) ProvideJWTService() services.JWTService {
|
| 95 |
+
return s.jWTService
|
| 96 |
+
}
|
| 97 |
+
func (s *servicesProvider) ProvideRegionService() services.RegionService {
|
| 98 |
+
return s.regionService
|
| 99 |
+
}
|
| 100 |
+
func (s *servicesProvider) ProvideOptionService() services.OptionService {
|
| 101 |
+
return s.optionService
|
| 102 |
+
}
|
| 103 |
+
func (s *servicesProvider) ProvideExamService() services.ExamService {
|
| 104 |
+
return s.examService
|
| 105 |
+
}
|
| 106 |
+
func (s *servicesProvider) ProvideAccountService() services.AccountService {
|
| 107 |
+
return s.accountService
|
| 108 |
+
}
|
| 109 |
+
func (s *servicesProvider) ProvideForgotPasswordService() services.ForgotPasswordService {
|
| 110 |
+
return s.forgotPasswordService
|
| 111 |
+
}
|
| 112 |
+
func (s *servicesProvider) ProvideEmailVerificationService() services.EmailVerificationService {
|
| 113 |
+
return s.emailVerificationService
|
| 114 |
+
}
|
| 115 |
+
func (s *servicesProvider) ProvideExternalAuthService() services.ExternalAuthService {
|
| 116 |
+
return s.externalAuthService
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
func (s *servicesProvider) ProvideUploadService() *services.UploadService {
|
| 120 |
+
return s.uploadService
|
| 121 |
+
}
|
provider/storage_interface.go
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package provider
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"context"
|
| 5 |
+
"mime/multipart"
|
| 6 |
+
)
|
| 7 |
+
|
| 8 |
+
// StorageProvider defines the behavior for file operations
|
| 9 |
+
type StorageProvider interface {
|
| 10 |
+
UploadFile(ctx context.Context, file multipart.File, header *multipart.FileHeader) (string, error)
|
| 11 |
+
GetFileURL(path string) (string, error)
|
| 12 |
+
DeleteFile(path string) error
|
| 13 |
+
}
|
provider/supabase_storage.go
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package provider
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"context"
|
| 5 |
+
"fmt"
|
| 6 |
+
"io"
|
| 7 |
+
|
| 8 |
+
// Pastikan import library supabase client yang Anda pakai benar
|
| 9 |
+
storage_go "github.com/supabase-community/storage-go"
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
type SupabaseStorage struct {
|
| 13 |
+
client *storage_go.Client
|
| 14 |
+
bucketName string
|
| 15 |
+
url string
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
func NewSupabaseStorage(url string, key string, bucketName string) *SupabaseStorage {
|
| 19 |
+
client := storage_go.NewClient(url+"/storage/v1", key, nil)
|
| 20 |
+
return &SupabaseStorage{
|
| 21 |
+
client: client,
|
| 22 |
+
bucketName: bucketName,
|
| 23 |
+
url: url,
|
| 24 |
+
}
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
func (s *SupabaseStorage) UploadFile(ctx context.Context, file io.Reader, destinationPath string, contentType string) (string, error) {
|
| 28 |
+
_, err := s.client.UploadFile(s.bucketName, destinationPath, file, storage_go.FileOptions{
|
| 29 |
+
ContentType: &contentType,
|
| 30 |
+
Upsert: new(bool), // Use new(bool) to create a pointer to false
|
| 31 |
+
})
|
| 32 |
+
|
| 33 |
+
if err != nil {
|
| 34 |
+
return "", err
|
| 35 |
+
}
|
| 36 |
+
publicURL := s.client.GetPublicUrl(s.bucketName, destinationPath).SignedURL
|
| 37 |
+
if publicURL == "" {
|
| 38 |
+
publicURL = fmt.Sprintf("%s/storage/v1/object/public/%s/%s", s.url, s.bucketName, destinationPath)
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
return publicURL, nil
|
| 42 |
+
}
|
repositories/academy_repository.go
CHANGED
|
@@ -7,56 +7,58 @@ import (
|
|
| 7 |
entity "abdanhafidz.com/go-boilerplate/models/entity"
|
| 8 |
"github.com/google/uuid"
|
| 9 |
"gorm.io/gorm"
|
|
|
|
| 10 |
)
|
| 11 |
|
| 12 |
type AcademyRepository interface {
|
| 13 |
-
|
|
|
|
| 14 |
GetAcademyByID(ctx context.Context, id uuid.UUID) (entity.Academy, error)
|
| 15 |
GetAcademyBySlug(ctx context.Context, slug string) (entity.Academy, error)
|
| 16 |
GetAcademyWithProgress(ctx context.Context, accountId uuid.UUID, slug string) (entity.Academy, error)
|
| 17 |
-
|
| 18 |
CreateAcademy(ctx context.Context, a entity.Academy) (entity.Academy, error)
|
| 19 |
UpdateAcademy(ctx context.Context, a entity.Academy) (entity.Academy, error)
|
| 20 |
DeleteAcademy(ctx context.Context, id uuid.UUID) error
|
| 21 |
-
|
| 22 |
ListAcademy(ctx context.Context, accountId uuid.UUID) ([]entity.Academy, error)
|
| 23 |
GetAcademyWithMaterials(ctx context.Context, id uuid.UUID) (entity.Academy, []entity.AcademyMaterial, error)
|
| 24 |
CountMaterialsByAcademyID(ctx context.Context, academyId uuid.UUID) (int64, error)
|
| 25 |
|
| 26 |
-
// Material
|
| 27 |
GetMaterialBySlug(ctx context.Context, academy_id uuid.UUID, materialSlug string) (entity.AcademyMaterial, error)
|
| 28 |
GetMaterialByID(ctx context.Context, id uuid.UUID) (entity.AcademyMaterial, error)
|
| 29 |
GetMaterialWithProgress(ctx context.Context, accountId uuid.UUID, academyId uuid.UUID, slug string) (entity.AcademyMaterial, error)
|
| 30 |
-
|
| 31 |
CreateMaterial(ctx context.Context, m entity.AcademyMaterial) (entity.AcademyMaterial, error)
|
| 32 |
UpdateMaterial(ctx context.Context, m entity.AcademyMaterial) (entity.AcademyMaterial, error)
|
| 33 |
DeleteMaterial(ctx context.Context, id uuid.UUID) error
|
| 34 |
-
|
| 35 |
ListMaterials(ctx context.Context, academyId uuid.UUID) ([]entity.AcademyMaterial, error)
|
| 36 |
GetMaterialWithContents(ctx context.Context, id uuid.UUID) (entity.AcademyMaterial, []entity.AcademyContent, error)
|
| 37 |
|
| 38 |
-
// Content
|
| 39 |
GetContentBySlug(ctx context.Context, materialId uuid.UUID, order uint) (entity.AcademyContent, error)
|
| 40 |
GetContentByID(ctx context.Context, id uuid.UUID) (entity.AcademyContent, error)
|
| 41 |
-
GetContentWithProgress(ctx context.Context, accountId uuid.UUID, academyId uuid.UUID,materialId uuid.UUID, order uint) (entity.AcademyContent, error)
|
| 42 |
-
|
| 43 |
CreateContent(ctx context.Context, c entity.AcademyContent) (entity.AcademyContent, error)
|
| 44 |
UpdateContent(ctx context.Context, c entity.AcademyContent) (entity.AcademyContent, error)
|
| 45 |
DeleteContent(ctx context.Context, id uuid.UUID) error
|
| 46 |
-
|
| 47 |
CountContentsByMaterialID(ctx context.Context, materialId uuid.UUID) (int64, error)
|
| 48 |
|
| 49 |
-
// Progress
|
| 50 |
GetAcademyProgress(ctx context.Context, accountId uuid.UUID, academyId uuid.UUID) (entity.AcademyProgress, error)
|
| 51 |
GetMaterialProgress(ctx context.Context, accountId uuid.UUID, academyId uuid.UUID, materialId uuid.UUID) (entity.AcademyMaterialProgress, error)
|
| 52 |
GetContentProgress(ctx context.Context, accountId uuid.UUID, academyId uuid.UUID, materialId uuid.UUID, contentId uuid.UUID) (entity.AcademyContentProgress, error)
|
| 53 |
-
|
| 54 |
UpsertAcademyProgress(ctx context.Context, p entity.AcademyProgress) (entity.AcademyProgress, error)
|
| 55 |
UpsertMaterialProgress(ctx context.Context, p entity.AcademyMaterialProgress) (entity.AcademyMaterialProgress, error)
|
| 56 |
UpsertContentProgress(ctx context.Context, p entity.AcademyContentProgress) (entity.AcademyContentProgress, error)
|
|
|
|
|
|
|
|
|
|
| 57 |
|
| 58 |
CountCompletedContentsByMaterialAndAccount(ctx context.Context, accountId uuid.UUID, materialId uuid.UUID) (int64, error)
|
| 59 |
CountCompletedMaterialsByAcademyAndAccount(ctx context.Context, accountId uuid.UUID, academyId uuid.UUID) (int64, error)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
}
|
| 61 |
|
| 62 |
type academyRepository struct{ db *gorm.DB }
|
|
@@ -65,16 +67,21 @@ func NewAcademyRepository(db *gorm.DB) AcademyRepository {
|
|
| 65 |
return &academyRepository{db: db}
|
| 66 |
}
|
| 67 |
|
| 68 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
func (r *academyRepository) GetAcademyWithMaterials(ctx context.Context, id uuid.UUID) (entity.Academy, []entity.AcademyMaterial, error) {
|
| 70 |
var a entity.Academy
|
| 71 |
-
err := r.db.WithContext(ctx).First(&a, "id = ?", id).Error
|
| 72 |
-
if err != nil {
|
| 73 |
return entity.Academy{}, nil, err
|
| 74 |
}
|
| 75 |
-
|
| 76 |
var m []entity.AcademyMaterial
|
| 77 |
-
|
|
|
|
| 78 |
}
|
| 79 |
|
| 80 |
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) (
|
|
| 88 |
}
|
| 89 |
|
| 90 |
func (r *academyRepository) GetAcademyWithProgress(ctx context.Context, accountId uuid.UUID, slug string) (entity.Academy, error) {
|
| 91 |
-
|
| 92 |
-
var err error
|
| 93 |
-
a, err = r.GetAcademyBySlug(ctx, slug)
|
| 94 |
if err != nil {
|
| 95 |
return a, err
|
| 96 |
}
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
var ap entity.AcademyProgress
|
| 101 |
-
|
| 102 |
-
ap, err = r.GetAcademyProgress(ctx, accountId, academyId)
|
| 103 |
-
if err != nil && err != gorm.ErrRecordNotFound {
|
| 104 |
return a, err
|
| 105 |
}
|
| 106 |
a.AcademyProgresss = ap
|
| 107 |
-
|
| 108 |
return a, nil
|
| 109 |
}
|
| 110 |
|
|
@@ -122,37 +121,51 @@ func (r *academyRepository) DeleteAcademy(ctx context.Context, id uuid.UUID) err
|
|
| 122 |
|
| 123 |
func (r *academyRepository) ListAcademy(ctx context.Context, accountId uuid.UUID) ([]entity.Academy, error) {
|
| 124 |
var list []entity.Academy
|
| 125 |
-
|
| 126 |
if err := r.db.WithContext(ctx).Find(&list).Error; err != nil {
|
| 127 |
return nil, err
|
| 128 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
|
| 130 |
for i := range list {
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
}
|
| 136 |
-
list[i].AcademyProgresss = ap
|
| 137 |
}
|
| 138 |
-
|
| 139 |
return list, nil
|
| 140 |
}
|
| 141 |
|
| 142 |
func (r *academyRepository) CountMaterialsByAcademyID(ctx context.Context, academyId uuid.UUID) (int64, error) {
|
| 143 |
var count int64
|
| 144 |
-
|
| 145 |
-
query := r.db.WithContext(ctx).
|
| 146 |
-
Where("academy_id = ?", academyId)
|
| 147 |
-
|
| 148 |
-
err := query.Model(&entity.AcademyMaterial{}).
|
| 149 |
-
Count(&count).
|
| 150 |
-
Error
|
| 151 |
-
|
| 152 |
return count, err
|
| 153 |
}
|
| 154 |
|
| 155 |
-
// ========== MATERIAL ==========
|
| 156 |
func (r *academyRepository) GetMaterialByID(ctx context.Context, id uuid.UUID) (entity.AcademyMaterial, error) {
|
| 157 |
var m entity.AcademyMaterial
|
| 158 |
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
|
|
| 165 |
|
| 166 |
func (r *academyRepository) GetMaterialWithContents(ctx context.Context, id uuid.UUID) (entity.AcademyMaterial, []entity.AcademyContent, error) {
|
| 167 |
var m entity.AcademyMaterial
|
| 168 |
-
err := r.db.WithContext(ctx).First(&m, "id = ?", id).Error
|
| 169 |
-
if err != nil {
|
| 170 |
return entity.AcademyMaterial{}, nil, err
|
| 171 |
}
|
| 172 |
-
|
| 173 |
var c []entity.AcademyContent
|
| 174 |
-
|
|
|
|
| 175 |
}
|
| 176 |
|
| 177 |
-
func (r *academyRepository) GetMaterialWithProgress(ctx context.Context, accountId uuid.UUID, academyId uuid.UUID, slug string) (entity.AcademyMaterial, error){
|
| 178 |
-
|
| 179 |
-
var err error
|
| 180 |
-
m, err = r.GetMaterialBySlug(ctx, academyId, slug)
|
| 181 |
if err != nil {
|
| 182 |
return m, err
|
| 183 |
}
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
var ap entity.AcademyMaterialProgress
|
| 188 |
-
|
| 189 |
-
ap, err = r.GetMaterialProgress(ctx, accountId,academyId, MaterialId)
|
| 190 |
-
if err != nil && err != gorm.ErrRecordNotFound {
|
| 191 |
return m, err
|
| 192 |
}
|
| 193 |
m.AcademyMaterialProgress = ap
|
| 194 |
-
|
| 195 |
return m, nil
|
| 196 |
}
|
| 197 |
|
|
@@ -201,7 +205,7 @@ func (r *academyRepository) CreateMaterial(ctx context.Context, m entity.Academy
|
|
| 201 |
|
| 202 |
func (r *academyRepository) ListMaterials(ctx context.Context, academyId uuid.UUID) ([]entity.AcademyMaterial, error) {
|
| 203 |
var list []entity.AcademyMaterial
|
| 204 |
-
return list, r.db.WithContext(ctx).Where("academy_id = ?", academyId).Find(&list).Error
|
| 205 |
}
|
| 206 |
|
| 207 |
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
|
|
| 212 |
return r.db.WithContext(ctx).Delete(&entity.AcademyMaterial{}, "id = ?", id).Error
|
| 213 |
}
|
| 214 |
|
| 215 |
-
// ========== CONTENT ==========
|
| 216 |
-
|
| 217 |
func (r *academyRepository) GetContentByID(ctx context.Context, id uuid.UUID) (entity.AcademyContent, error) {
|
| 218 |
var c entity.AcademyContent
|
| 219 |
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
|
|
| 221 |
|
| 222 |
func (r *academyRepository) GetContentBySlug(ctx context.Context, materialId uuid.UUID, order uint) (entity.AcademyContent, error) {
|
| 223 |
var c entity.AcademyContent
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
Where("\"order\" = ?", order).
|
| 227 |
-
Where("material_id = ?", materialId).
|
| 228 |
-
First(&c)
|
| 229 |
-
|
| 230 |
-
return c, result.Error
|
| 231 |
}
|
| 232 |
|
| 233 |
-
func (r *academyRepository) GetContentWithProgress(ctx context.Context, accountId uuid.UUID, academyId uuid.UUID,materialId uuid.UUID, order uint) (entity.AcademyContent, error){
|
| 234 |
-
|
| 235 |
-
var err error
|
| 236 |
-
c, err = r.GetContentBySlug(ctx,materialId,order)
|
| 237 |
if err != nil {
|
| 238 |
return c, err
|
| 239 |
}
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
ap, err = r.GetContentProgress(ctx, accountId, academyId, materialId,c.Id)
|
| 244 |
-
if err != nil && err != gorm.ErrRecordNotFound {
|
| 245 |
return c, err
|
| 246 |
}
|
| 247 |
c.AcademyContentProgress = ap
|
| 248 |
-
|
| 249 |
return c, nil
|
| 250 |
}
|
| 251 |
|
|
@@ -263,89 +254,56 @@ func (r *academyRepository) DeleteContent(ctx context.Context, id uuid.UUID) err
|
|
| 263 |
|
| 264 |
func (r *academyRepository) CountContentsByMaterialID(ctx context.Context, materialId uuid.UUID) (int64, error) {
|
| 265 |
var count int64
|
| 266 |
-
|
| 267 |
-
query := r.db.WithContext(ctx).
|
| 268 |
-
Where("material_id = ?", materialId)
|
| 269 |
-
|
| 270 |
-
err := query.Model(&entity.AcademyContent{}).
|
| 271 |
-
Count(&count).
|
| 272 |
-
Error
|
| 273 |
-
|
| 274 |
return count, err
|
| 275 |
}
|
| 276 |
|
| 277 |
-
// ========== PROGRESS ==========
|
| 278 |
-
|
| 279 |
func (r *academyRepository) GetAcademyProgress(ctx context.Context, accountId uuid.UUID, academyId uuid.UUID) (entity.AcademyProgress, error) {
|
| 280 |
var existing entity.AcademyProgress
|
| 281 |
-
|
| 282 |
-
err := r.db.WithContext(ctx).
|
| 283 |
-
Where("account_id = ? AND academy_id = ?", accountId, academyId).
|
| 284 |
-
First(&existing).Error
|
| 285 |
|
| 286 |
if errors.Is(err, gorm.ErrRecordNotFound) {
|
| 287 |
return entity.AcademyProgress{
|
| 288 |
-
AccountId:
|
| 289 |
-
AcademyId:
|
| 290 |
-
Status:
|
| 291 |
-
Progress: 0,
|
| 292 |
-
TotalCompletedMaterials: 0,
|
| 293 |
}, nil
|
| 294 |
}
|
| 295 |
-
|
| 296 |
-
if err != nil {
|
| 297 |
-
return existing, err
|
| 298 |
-
}
|
| 299 |
-
return existing, nil
|
| 300 |
}
|
| 301 |
|
| 302 |
func (r *academyRepository) UpsertAcademyProgress(ctx context.Context, p entity.AcademyProgress) (entity.AcademyProgress, error) {
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
}
|
| 308 |
-
return p, r.db.WithContext(ctx).Model(&existing).Updates(p).Error
|
| 309 |
}
|
| 310 |
|
| 311 |
func (r *academyRepository) GetMaterialProgress(ctx context.Context, accountId uuid.UUID, academyId uuid.UUID, materialId uuid.UUID) (entity.AcademyMaterialProgress, error) {
|
| 312 |
var existing entity.AcademyMaterialProgress
|
| 313 |
-
|
| 314 |
-
err := r.db.WithContext(ctx).First(&existing, "account_id = ? AND academy_id = ? AND material_id = ?", accountId, academyId, materialId).Error
|
| 315 |
|
| 316 |
if errors.Is(err, gorm.ErrRecordNotFound) {
|
| 317 |
-
|
| 318 |
return entity.AcademyMaterialProgress{
|
| 319 |
-
AccountId:
|
| 320 |
-
AcademyId:
|
| 321 |
-
MaterialId:
|
| 322 |
-
|
| 323 |
-
TotalCompletedContents: 0,
|
| 324 |
-
Status: "NOT_STARTED",
|
| 325 |
}, nil
|
| 326 |
}
|
| 327 |
-
|
| 328 |
-
if err != nil {
|
| 329 |
-
return existing, err
|
| 330 |
-
}
|
| 331 |
-
return existing, nil
|
| 332 |
}
|
| 333 |
|
| 334 |
func (r *academyRepository) UpsertMaterialProgress(ctx context.Context, p entity.AcademyMaterialProgress) (entity.AcademyMaterialProgress, error) {
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
return p, r.db.WithContext(ctx).Create(&p).Error
|
| 340 |
-
}
|
| 341 |
-
|
| 342 |
-
return p, r.db.WithContext(ctx).Model(&existing).Updates(p).Error
|
| 343 |
}
|
| 344 |
|
| 345 |
func (r *academyRepository) GetContentProgress(ctx context.Context, accountId uuid.UUID, academyId uuid.UUID, materialId uuid.UUID, contentId uuid.UUID) (entity.AcademyContentProgress, error) {
|
| 346 |
var existing entity.AcademyContentProgress
|
| 347 |
-
|
| 348 |
-
err := r.db.WithContext(ctx).First(&existing, "account_id = ? AND academy_id = ? AND material_id = ? AND content_id = ?", accountId, academyId, materialId,contentId).Error
|
| 349 |
|
| 350 |
if errors.Is(err, gorm.ErrRecordNotFound) {
|
| 351 |
return entity.AcademyContentProgress{
|
|
@@ -353,45 +311,162 @@ func (r *academyRepository) GetContentProgress(ctx context.Context, accountId uu
|
|
| 353 |
AcademyId: academyId,
|
| 354 |
MaterialId: materialId,
|
| 355 |
ContentId: contentId,
|
| 356 |
-
Status:
|
| 357 |
}, nil
|
| 358 |
}
|
| 359 |
-
|
| 360 |
-
if err != nil {
|
| 361 |
-
return existing, err
|
| 362 |
-
}
|
| 363 |
-
return existing, nil
|
| 364 |
}
|
| 365 |
|
| 366 |
func (r *academyRepository) UpsertContentProgress(ctx context.Context, p entity.AcademyContentProgress) (entity.AcademyContentProgress, error) {
|
| 367 |
-
|
| 368 |
-
|
|
|
|
|
|
|
|
|
|
| 369 |
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
|
| 374 |
-
|
|
|
|
| 375 |
}
|
| 376 |
|
| 377 |
-
|
|
|
|
|
|
|
| 378 |
|
| 379 |
func (r *academyRepository) CountCompletedContentsByMaterialAndAccount(ctx context.Context, accountId uuid.UUID, materialId uuid.UUID) (int64, error) {
|
| 380 |
var count int64
|
| 381 |
-
|
| 382 |
-
Where("account_id = ? AND material_id = ? AND status = ?", accountId, materialId,
|
| 383 |
-
|
| 384 |
-
Count(&count).
|
| 385 |
-
Error
|
| 386 |
return count, err
|
| 387 |
}
|
| 388 |
|
| 389 |
func (r *academyRepository) CountCompletedMaterialsByAcademyAndAccount(ctx context.Context, accountId uuid.UUID, academyId uuid.UUID) (int64, error) {
|
| 390 |
var count int64
|
| 391 |
-
|
| 392 |
-
Where("account_id = ? AND academy_id = ? AND status = ?", accountId, academyId,
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 396 |
return count, err
|
| 397 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
entity "abdanhafidz.com/go-boilerplate/models/entity"
|
| 8 |
"github.com/google/uuid"
|
| 9 |
"gorm.io/gorm"
|
| 10 |
+
"gorm.io/gorm/clause"
|
| 11 |
)
|
| 12 |
|
| 13 |
type AcademyRepository interface {
|
| 14 |
+
Atomic(ctx context.Context, fn func(r AcademyRepository) error) error
|
| 15 |
+
|
| 16 |
GetAcademyByID(ctx context.Context, id uuid.UUID) (entity.Academy, error)
|
| 17 |
GetAcademyBySlug(ctx context.Context, slug string) (entity.Academy, error)
|
| 18 |
GetAcademyWithProgress(ctx context.Context, accountId uuid.UUID, slug string) (entity.Academy, error)
|
|
|
|
| 19 |
CreateAcademy(ctx context.Context, a entity.Academy) (entity.Academy, error)
|
| 20 |
UpdateAcademy(ctx context.Context, a entity.Academy) (entity.Academy, error)
|
| 21 |
DeleteAcademy(ctx context.Context, id uuid.UUID) error
|
|
|
|
| 22 |
ListAcademy(ctx context.Context, accountId uuid.UUID) ([]entity.Academy, error)
|
| 23 |
GetAcademyWithMaterials(ctx context.Context, id uuid.UUID) (entity.Academy, []entity.AcademyMaterial, error)
|
| 24 |
CountMaterialsByAcademyID(ctx context.Context, academyId uuid.UUID) (int64, error)
|
| 25 |
|
|
|
|
| 26 |
GetMaterialBySlug(ctx context.Context, academy_id uuid.UUID, materialSlug string) (entity.AcademyMaterial, error)
|
| 27 |
GetMaterialByID(ctx context.Context, id uuid.UUID) (entity.AcademyMaterial, error)
|
| 28 |
GetMaterialWithProgress(ctx context.Context, accountId uuid.UUID, academyId uuid.UUID, slug string) (entity.AcademyMaterial, error)
|
|
|
|
| 29 |
CreateMaterial(ctx context.Context, m entity.AcademyMaterial) (entity.AcademyMaterial, error)
|
| 30 |
UpdateMaterial(ctx context.Context, m entity.AcademyMaterial) (entity.AcademyMaterial, error)
|
| 31 |
DeleteMaterial(ctx context.Context, id uuid.UUID) error
|
|
|
|
| 32 |
ListMaterials(ctx context.Context, academyId uuid.UUID) ([]entity.AcademyMaterial, error)
|
| 33 |
GetMaterialWithContents(ctx context.Context, id uuid.UUID) (entity.AcademyMaterial, []entity.AcademyContent, error)
|
| 34 |
|
|
|
|
| 35 |
GetContentBySlug(ctx context.Context, materialId uuid.UUID, order uint) (entity.AcademyContent, error)
|
| 36 |
GetContentByID(ctx context.Context, id uuid.UUID) (entity.AcademyContent, error)
|
| 37 |
+
GetContentWithProgress(ctx context.Context, accountId uuid.UUID, academyId uuid.UUID, materialId uuid.UUID, order uint) (entity.AcademyContent, error)
|
|
|
|
| 38 |
CreateContent(ctx context.Context, c entity.AcademyContent) (entity.AcademyContent, error)
|
| 39 |
UpdateContent(ctx context.Context, c entity.AcademyContent) (entity.AcademyContent, error)
|
| 40 |
DeleteContent(ctx context.Context, id uuid.UUID) error
|
|
|
|
| 41 |
CountContentsByMaterialID(ctx context.Context, materialId uuid.UUID) (int64, error)
|
| 42 |
|
|
|
|
| 43 |
GetAcademyProgress(ctx context.Context, accountId uuid.UUID, academyId uuid.UUID) (entity.AcademyProgress, error)
|
| 44 |
GetMaterialProgress(ctx context.Context, accountId uuid.UUID, academyId uuid.UUID, materialId uuid.UUID) (entity.AcademyMaterialProgress, error)
|
| 45 |
GetContentProgress(ctx context.Context, accountId uuid.UUID, academyId uuid.UUID, materialId uuid.UUID, contentId uuid.UUID) (entity.AcademyContentProgress, error)
|
|
|
|
| 46 |
UpsertAcademyProgress(ctx context.Context, p entity.AcademyProgress) (entity.AcademyProgress, error)
|
| 47 |
UpsertMaterialProgress(ctx context.Context, p entity.AcademyMaterialProgress) (entity.AcademyMaterialProgress, error)
|
| 48 |
UpsertContentProgress(ctx context.Context, p entity.AcademyContentProgress) (entity.AcademyContentProgress, error)
|
| 49 |
+
DeleteContentProgressByContentID(ctx context.Context, contentId uuid.UUID) error
|
| 50 |
+
DeleteContentProgressByMaterialID(ctx context.Context, materialId uuid.UUID) error
|
| 51 |
+
DeleteMaterialProgressByMaterialID(ctx context.Context, materialId uuid.UUID) error
|
| 52 |
|
| 53 |
CountCompletedContentsByMaterialAndAccount(ctx context.Context, accountId uuid.UUID, materialId uuid.UUID) (int64, error)
|
| 54 |
CountCompletedMaterialsByAcademyAndAccount(ctx context.Context, accountId uuid.UUID, academyId uuid.UUID) (int64, error)
|
| 55 |
+
CountStartedMaterialsByAcademyAndAccount(ctx context.Context, accountId uuid.UUID, academyId uuid.UUID) (int64, error)
|
| 56 |
+
DecrementMaterialOrdersGreaterThan(ctx context.Context, academyId uuid.UUID, order uint) error
|
| 57 |
+
DecrementContentOrdersGreaterThan(ctx context.Context, materialId uuid.UUID, order uint) error
|
| 58 |
+
GetAccumulatedMaterialProgress(ctx context.Context, accountId uuid.UUID, academyId uuid.UUID) (float64, error)
|
| 59 |
+
|
| 60 |
+
BatchRecalculateMaterialProgress(ctx context.Context, materialId uuid.UUID) error
|
| 61 |
+
BatchRecalculateAcademyProgress(ctx context.Context, academyId uuid.UUID) error
|
| 62 |
}
|
| 63 |
|
| 64 |
type academyRepository struct{ db *gorm.DB }
|
|
|
|
| 67 |
return &academyRepository{db: db}
|
| 68 |
}
|
| 69 |
|
| 70 |
+
func (r *academyRepository) Atomic(ctx context.Context, fn func(r AcademyRepository) error) error {
|
| 71 |
+
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
| 72 |
+
txRepo := NewAcademyRepository(tx)
|
| 73 |
+
return fn(txRepo)
|
| 74 |
+
})
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
func (r *academyRepository) GetAcademyWithMaterials(ctx context.Context, id uuid.UUID) (entity.Academy, []entity.AcademyMaterial, error) {
|
| 78 |
var a entity.Academy
|
| 79 |
+
if err := r.db.WithContext(ctx).First(&a, "id = ?", id).Error; err != nil {
|
|
|
|
| 80 |
return entity.Academy{}, nil, err
|
| 81 |
}
|
|
|
|
| 82 |
var m []entity.AcademyMaterial
|
| 83 |
+
err := r.db.WithContext(ctx).Where("academy_id = ?", id).Order("\"order\" ASC").Find(&m).Error
|
| 84 |
+
return a, m, err
|
| 85 |
}
|
| 86 |
|
| 87 |
func (r *academyRepository) GetAcademyByID(ctx context.Context, id uuid.UUID) (entity.Academy, error) {
|
|
|
|
| 95 |
}
|
| 96 |
|
| 97 |
func (r *academyRepository) GetAcademyWithProgress(ctx context.Context, accountId uuid.UUID, slug string) (entity.Academy, error) {
|
| 98 |
+
a, err := r.GetAcademyBySlug(ctx, slug)
|
|
|
|
|
|
|
| 99 |
if err != nil {
|
| 100 |
return a, err
|
| 101 |
}
|
| 102 |
+
ap, err := r.GetAcademyProgress(ctx, accountId, a.Id)
|
| 103 |
+
if err != nil {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
return a, err
|
| 105 |
}
|
| 106 |
a.AcademyProgresss = ap
|
|
|
|
| 107 |
return a, nil
|
| 108 |
}
|
| 109 |
|
|
|
|
| 121 |
|
| 122 |
func (r *academyRepository) ListAcademy(ctx context.Context, accountId uuid.UUID) ([]entity.Academy, error) {
|
| 123 |
var list []entity.Academy
|
|
|
|
| 124 |
if err := r.db.WithContext(ctx).Find(&list).Error; err != nil {
|
| 125 |
return nil, err
|
| 126 |
}
|
| 127 |
+
if len(list) == 0 {
|
| 128 |
+
return list, nil
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
academyIDs := make([]uuid.UUID, len(list))
|
| 132 |
+
for i, ac := range list {
|
| 133 |
+
academyIDs[i] = ac.Id
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
var progressList []entity.AcademyProgress
|
| 137 |
+
if err := r.db.WithContext(ctx).
|
| 138 |
+
Where("account_id = ?", accountId).
|
| 139 |
+
Where("academy_id IN ?", academyIDs).
|
| 140 |
+
Find(&progressList).Error; err != nil {
|
| 141 |
+
return nil, err
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
progressMap := make(map[uuid.UUID]entity.AcademyProgress)
|
| 145 |
+
for _, p := range progressList {
|
| 146 |
+
progressMap[p.AcademyId] = p
|
| 147 |
+
}
|
| 148 |
|
| 149 |
for i := range list {
|
| 150 |
+
if p, exists := progressMap[list[i].Id]; exists {
|
| 151 |
+
list[i].AcademyProgresss = p
|
| 152 |
+
} else {
|
| 153 |
+
list[i].AcademyProgresss = entity.AcademyProgress{
|
| 154 |
+
AccountId: accountId,
|
| 155 |
+
AcademyId: list[i].Id,
|
| 156 |
+
Status: entity.StatusNotStarted,
|
| 157 |
+
}
|
| 158 |
}
|
|
|
|
| 159 |
}
|
|
|
|
| 160 |
return list, nil
|
| 161 |
}
|
| 162 |
|
| 163 |
func (r *academyRepository) CountMaterialsByAcademyID(ctx context.Context, academyId uuid.UUID) (int64, error) {
|
| 164 |
var count int64
|
| 165 |
+
err := r.db.WithContext(ctx).Model(&entity.AcademyMaterial{}).Where("academy_id = ?", academyId).Count(&count).Error
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
return count, err
|
| 167 |
}
|
| 168 |
|
|
|
|
| 169 |
func (r *academyRepository) GetMaterialByID(ctx context.Context, id uuid.UUID) (entity.AcademyMaterial, error) {
|
| 170 |
var m entity.AcademyMaterial
|
| 171 |
return m, r.db.WithContext(ctx).First(&m, "id = ?", id).Error
|
|
|
|
| 178 |
|
| 179 |
func (r *academyRepository) GetMaterialWithContents(ctx context.Context, id uuid.UUID) (entity.AcademyMaterial, []entity.AcademyContent, error) {
|
| 180 |
var m entity.AcademyMaterial
|
| 181 |
+
if err := r.db.WithContext(ctx).First(&m, "id = ?", id).Error; err != nil {
|
|
|
|
| 182 |
return entity.AcademyMaterial{}, nil, err
|
| 183 |
}
|
|
|
|
| 184 |
var c []entity.AcademyContent
|
| 185 |
+
err := r.db.WithContext(ctx).Where("material_id = ?", id).Order("\"order\" ASC").Find(&c).Error
|
| 186 |
+
return m, c, err
|
| 187 |
}
|
| 188 |
|
| 189 |
+
func (r *academyRepository) GetMaterialWithProgress(ctx context.Context, accountId uuid.UUID, academyId uuid.UUID, slug string) (entity.AcademyMaterial, error) {
|
| 190 |
+
m, err := r.GetMaterialBySlug(ctx, academyId, slug)
|
|
|
|
|
|
|
| 191 |
if err != nil {
|
| 192 |
return m, err
|
| 193 |
}
|
| 194 |
+
ap, err := r.GetMaterialProgress(ctx, accountId, academyId, m.Id)
|
| 195 |
+
if err != nil {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 196 |
return m, err
|
| 197 |
}
|
| 198 |
m.AcademyMaterialProgress = ap
|
|
|
|
| 199 |
return m, nil
|
| 200 |
}
|
| 201 |
|
|
|
|
| 205 |
|
| 206 |
func (r *academyRepository) ListMaterials(ctx context.Context, academyId uuid.UUID) ([]entity.AcademyMaterial, error) {
|
| 207 |
var list []entity.AcademyMaterial
|
| 208 |
+
return list, r.db.WithContext(ctx).Where("academy_id = ?", academyId).Order("\"order\" ASC").Find(&list).Error
|
| 209 |
}
|
| 210 |
|
| 211 |
func (r *academyRepository) UpdateMaterial(ctx context.Context, m entity.AcademyMaterial) (entity.AcademyMaterial, error) {
|
|
|
|
| 216 |
return r.db.WithContext(ctx).Delete(&entity.AcademyMaterial{}, "id = ?", id).Error
|
| 217 |
}
|
| 218 |
|
|
|
|
|
|
|
| 219 |
func (r *academyRepository) GetContentByID(ctx context.Context, id uuid.UUID) (entity.AcademyContent, error) {
|
| 220 |
var c entity.AcademyContent
|
| 221 |
return c, r.db.WithContext(ctx).First(&c, "id = ?", id).Error
|
|
|
|
| 223 |
|
| 224 |
func (r *academyRepository) GetContentBySlug(ctx context.Context, materialId uuid.UUID, order uint) (entity.AcademyContent, error) {
|
| 225 |
var c entity.AcademyContent
|
| 226 |
+
err := r.db.WithContext(ctx).Where("\"order\" = ? AND material_id = ?", order, materialId).First(&c).Error
|
| 227 |
+
return c, err
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
}
|
| 229 |
|
| 230 |
+
func (r *academyRepository) GetContentWithProgress(ctx context.Context, accountId uuid.UUID, academyId uuid.UUID, materialId uuid.UUID, order uint) (entity.AcademyContent, error) {
|
| 231 |
+
c, err := r.GetContentBySlug(ctx, materialId, order)
|
|
|
|
|
|
|
| 232 |
if err != nil {
|
| 233 |
return c, err
|
| 234 |
}
|
| 235 |
+
ap, err := r.GetContentProgress(ctx, accountId, academyId, materialId, c.Id)
|
| 236 |
+
if err != nil {
|
|
|
|
|
|
|
|
|
|
| 237 |
return c, err
|
| 238 |
}
|
| 239 |
c.AcademyContentProgress = ap
|
|
|
|
| 240 |
return c, nil
|
| 241 |
}
|
| 242 |
|
|
|
|
| 254 |
|
| 255 |
func (r *academyRepository) CountContentsByMaterialID(ctx context.Context, materialId uuid.UUID) (int64, error) {
|
| 256 |
var count int64
|
| 257 |
+
err := r.db.WithContext(ctx).Model(&entity.AcademyContent{}).Where("material_id = ?", materialId).Count(&count).Error
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 258 |
return count, err
|
| 259 |
}
|
| 260 |
|
|
|
|
|
|
|
| 261 |
func (r *academyRepository) GetAcademyProgress(ctx context.Context, accountId uuid.UUID, academyId uuid.UUID) (entity.AcademyProgress, error) {
|
| 262 |
var existing entity.AcademyProgress
|
| 263 |
+
err := r.db.WithContext(ctx).Where("account_id = ? AND academy_id = ?", accountId, academyId).First(&existing).Error
|
|
|
|
|
|
|
|
|
|
| 264 |
|
| 265 |
if errors.Is(err, gorm.ErrRecordNotFound) {
|
| 266 |
return entity.AcademyProgress{
|
| 267 |
+
AccountId: accountId,
|
| 268 |
+
AcademyId: academyId,
|
| 269 |
+
Status: entity.StatusNotStarted,
|
|
|
|
|
|
|
| 270 |
}, nil
|
| 271 |
}
|
| 272 |
+
return existing, err
|
|
|
|
|
|
|
|
|
|
|
|
|
| 273 |
}
|
| 274 |
|
| 275 |
func (r *academyRepository) UpsertAcademyProgress(ctx context.Context, p entity.AcademyProgress) (entity.AcademyProgress, error) {
|
| 276 |
+
return p, r.db.WithContext(ctx).Clauses(clause.OnConflict{
|
| 277 |
+
Columns: []clause.Column{{Name: "id"}},
|
| 278 |
+
UpdateAll: true,
|
| 279 |
+
}).Save(&p).Error
|
|
|
|
|
|
|
| 280 |
}
|
| 281 |
|
| 282 |
func (r *academyRepository) GetMaterialProgress(ctx context.Context, accountId uuid.UUID, academyId uuid.UUID, materialId uuid.UUID) (entity.AcademyMaterialProgress, error) {
|
| 283 |
var existing entity.AcademyMaterialProgress
|
| 284 |
+
err := r.db.WithContext(ctx).Where("account_id = ? AND material_id = ?", accountId, materialId).First(&existing).Error
|
|
|
|
| 285 |
|
| 286 |
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
|
|
| 287 |
return entity.AcademyMaterialProgress{
|
| 288 |
+
AccountId: accountId,
|
| 289 |
+
AcademyId: academyId,
|
| 290 |
+
MaterialId: materialId,
|
| 291 |
+
Status: entity.StatusNotStarted,
|
|
|
|
|
|
|
| 292 |
}, nil
|
| 293 |
}
|
| 294 |
+
return existing, err
|
|
|
|
|
|
|
|
|
|
|
|
|
| 295 |
}
|
| 296 |
|
| 297 |
func (r *academyRepository) UpsertMaterialProgress(ctx context.Context, p entity.AcademyMaterialProgress) (entity.AcademyMaterialProgress, error) {
|
| 298 |
+
return p, r.db.WithContext(ctx).Clauses(clause.OnConflict{
|
| 299 |
+
Columns: []clause.Column{{Name: "id"}},
|
| 300 |
+
UpdateAll: true,
|
| 301 |
+
}).Save(&p).Error
|
|
|
|
|
|
|
|
|
|
|
|
|
| 302 |
}
|
| 303 |
|
| 304 |
func (r *academyRepository) GetContentProgress(ctx context.Context, accountId uuid.UUID, academyId uuid.UUID, materialId uuid.UUID, contentId uuid.UUID) (entity.AcademyContentProgress, error) {
|
| 305 |
var existing entity.AcademyContentProgress
|
| 306 |
+
err := r.db.WithContext(ctx).Where("account_id = ? AND content_id = ?", accountId, contentId).First(&existing).Error
|
|
|
|
| 307 |
|
| 308 |
if errors.Is(err, gorm.ErrRecordNotFound) {
|
| 309 |
return entity.AcademyContentProgress{
|
|
|
|
| 311 |
AcademyId: academyId,
|
| 312 |
MaterialId: materialId,
|
| 313 |
ContentId: contentId,
|
| 314 |
+
Status: entity.StatusNotStarted,
|
| 315 |
}, nil
|
| 316 |
}
|
| 317 |
+
return existing, err
|
|
|
|
|
|
|
|
|
|
|
|
|
| 318 |
}
|
| 319 |
|
| 320 |
func (r *academyRepository) UpsertContentProgress(ctx context.Context, p entity.AcademyContentProgress) (entity.AcademyContentProgress, error) {
|
| 321 |
+
return p, r.db.WithContext(ctx).Clauses(clause.OnConflict{
|
| 322 |
+
Columns: []clause.Column{{Name: "id"}},
|
| 323 |
+
UpdateAll: true,
|
| 324 |
+
}).Save(&p).Error
|
| 325 |
+
}
|
| 326 |
|
| 327 |
+
func (r *academyRepository) DeleteContentProgressByContentID(ctx context.Context, contentId uuid.UUID) error {
|
| 328 |
+
return r.db.WithContext(ctx).Where("content_id = ?", contentId).Delete(&entity.AcademyContentProgress{}).Error
|
| 329 |
+
}
|
| 330 |
|
| 331 |
+
func (r *academyRepository) DeleteContentProgressByMaterialID(ctx context.Context, materialId uuid.UUID) error {
|
| 332 |
+
return r.db.WithContext(ctx).Where("material_id = ?", materialId).Delete(&entity.AcademyContentProgress{}).Error
|
| 333 |
}
|
| 334 |
|
| 335 |
+
func (r *academyRepository) DeleteMaterialProgressByMaterialID(ctx context.Context, materialId uuid.UUID) error {
|
| 336 |
+
return r.db.WithContext(ctx).Where("material_id = ?", materialId).Delete(&entity.AcademyMaterialProgress{}).Error
|
| 337 |
+
}
|
| 338 |
|
| 339 |
func (r *academyRepository) CountCompletedContentsByMaterialAndAccount(ctx context.Context, accountId uuid.UUID, materialId uuid.UUID) (int64, error) {
|
| 340 |
var count int64
|
| 341 |
+
err := r.db.WithContext(ctx).Model(&entity.AcademyContentProgress{}).
|
| 342 |
+
Where("account_id = ? AND material_id = ? AND status = ?", accountId, materialId, entity.StatusCompleted).
|
| 343 |
+
Count(&count).Error
|
|
|
|
|
|
|
| 344 |
return count, err
|
| 345 |
}
|
| 346 |
|
| 347 |
func (r *academyRepository) CountCompletedMaterialsByAcademyAndAccount(ctx context.Context, accountId uuid.UUID, academyId uuid.UUID) (int64, error) {
|
| 348 |
var count int64
|
| 349 |
+
err := r.db.WithContext(ctx).Model(&entity.AcademyMaterialProgress{}).
|
| 350 |
+
Where("account_id = ? AND academy_id = ? AND status = ?", accountId, academyId, entity.StatusCompleted).
|
| 351 |
+
Count(&count).Error
|
| 352 |
+
return count, err
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
func (r *academyRepository) CountStartedMaterialsByAcademyAndAccount(ctx context.Context, accountId uuid.UUID, academyId uuid.UUID) (int64, error) {
|
| 356 |
+
var count int64
|
| 357 |
+
err := r.db.WithContext(ctx).Model(&entity.AcademyMaterialProgress{}).
|
| 358 |
+
Where("account_id = ? AND academy_id = ? AND status IN ?", accountId, academyId, []string{entity.StatusInProgress, entity.StatusCompleted}).
|
| 359 |
+
Count(&count).Error
|
| 360 |
return count, err
|
| 361 |
}
|
| 362 |
+
|
| 363 |
+
func (r *academyRepository) DecrementMaterialOrdersGreaterThan(ctx context.Context, academyId uuid.UUID, order uint) error {
|
| 364 |
+
return r.db.WithContext(ctx).Model(&entity.AcademyMaterial{}).
|
| 365 |
+
Where("academy_id = ? AND \"order\" > ?", academyId, order).
|
| 366 |
+
Update("\"order\"", gorm.Expr("\"order\" - 1")).Error
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
func (r *academyRepository) DecrementContentOrdersGreaterThan(ctx context.Context, materialId uuid.UUID, order uint) error {
|
| 370 |
+
return r.db.WithContext(ctx).Model(&entity.AcademyContent{}).
|
| 371 |
+
Where("material_id = ? AND \"order\" > ?", materialId, order).
|
| 372 |
+
Update("\"order\"", gorm.Expr("\"order\" - 1")).Error
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
func (r *academyRepository) GetAccumulatedMaterialProgress(ctx context.Context, accountId uuid.UUID, academyId uuid.UUID) (float64, error) {
|
| 376 |
+
var total float64
|
| 377 |
+
err := r.db.WithContext(ctx).Model(&entity.AcademyMaterialProgress{}).
|
| 378 |
+
Where("account_id = ? AND academy_id = ?", accountId, academyId).
|
| 379 |
+
Select("COALESCE(SUM(progress), 0)").
|
| 380 |
+
Scan(&total).Error
|
| 381 |
+
return total, err
|
| 382 |
+
}
|
| 383 |
+
|
| 384 |
+
func (r *academyRepository) BatchRecalculateMaterialProgress(ctx context.Context, materialId uuid.UUID) error {
|
| 385 |
+
totalContents, err := r.CountContentsByMaterialID(ctx, materialId)
|
| 386 |
+
if err != nil {
|
| 387 |
+
return err
|
| 388 |
+
}
|
| 389 |
+
|
| 390 |
+
if totalContents == 0 {
|
| 391 |
+
return r.db.WithContext(ctx).Model(&entity.AcademyMaterialProgress{}).
|
| 392 |
+
Where("material_id = ?", materialId).
|
| 393 |
+
Updates(map[string]interface{}{
|
| 394 |
+
"progress": 100,
|
| 395 |
+
"status": entity.StatusCompleted,
|
| 396 |
+
"total_completed_contents": 0,
|
| 397 |
+
}).Error
|
| 398 |
+
}
|
| 399 |
+
|
| 400 |
+
return r.db.WithContext(ctx).Exec(`
|
| 401 |
+
UPDATE academy_material_progresses amp
|
| 402 |
+
SET
|
| 403 |
+
total_completed_contents = (
|
| 404 |
+
SELECT COUNT(id) FROM academy_content_progresses acp
|
| 405 |
+
WHERE acp.material_id = amp.material_id AND acp.account_id = amp.account_id AND acp.status = 'COMPLETED'
|
| 406 |
+
),
|
| 407 |
+
progress = (
|
| 408 |
+
SELECT COUNT(id) FROM academy_content_progresses acp
|
| 409 |
+
WHERE acp.material_id = amp.material_id AND acp.account_id = amp.account_id AND acp.status = 'COMPLETED'
|
| 410 |
+
)::float / ? * 100,
|
| 411 |
+
status = CASE
|
| 412 |
+
WHEN (
|
| 413 |
+
SELECT COUNT(id) FROM academy_content_progresses acp
|
| 414 |
+
WHERE acp.material_id = amp.material_id AND acp.account_id = amp.account_id AND acp.status = 'COMPLETED'
|
| 415 |
+
) >= ? THEN 'COMPLETED'
|
| 416 |
+
ELSE 'IN_PROGRESS'
|
| 417 |
+
END,
|
| 418 |
+
completed_at = CASE
|
| 419 |
+
WHEN (
|
| 420 |
+
SELECT COUNT(id) FROM academy_content_progresses acp
|
| 421 |
+
WHERE acp.material_id = amp.material_id AND acp.account_id = amp.account_id AND acp.status = 'COMPLETED'
|
| 422 |
+
) >= ? THEN NOW()
|
| 423 |
+
ELSE NULL
|
| 424 |
+
END
|
| 425 |
+
WHERE amp.material_id = ?
|
| 426 |
+
`, totalContents, totalContents, totalContents, materialId).Error
|
| 427 |
+
}
|
| 428 |
+
|
| 429 |
+
func (r *academyRepository) BatchRecalculateAcademyProgress(ctx context.Context, academyId uuid.UUID) error {
|
| 430 |
+
totalMaterials, err := r.CountMaterialsByAcademyID(ctx, academyId)
|
| 431 |
+
if err != nil {
|
| 432 |
+
return err
|
| 433 |
+
}
|
| 434 |
+
|
| 435 |
+
if totalMaterials == 0 {
|
| 436 |
+
return r.db.WithContext(ctx).Model(&entity.AcademyProgress{}).
|
| 437 |
+
Where("academy_id = ?", academyId).
|
| 438 |
+
Updates(map[string]interface{}{
|
| 439 |
+
"progress": 100,
|
| 440 |
+
"status": entity.StatusCompleted,
|
| 441 |
+
"total_completed_materials": 0,
|
| 442 |
+
}).Error
|
| 443 |
+
}
|
| 444 |
+
|
| 445 |
+
return r.db.WithContext(ctx).Exec(`
|
| 446 |
+
UPDATE academy_progress ap
|
| 447 |
+
SET
|
| 448 |
+
progress = (
|
| 449 |
+
SELECT COALESCE(SUM(amp.progress), 0) FROM academy_material_progresses amp
|
| 450 |
+
WHERE amp.academy_id = ap.academy_id AND amp.account_id = ap.account_id
|
| 451 |
+
)::float / ?,
|
| 452 |
+
total_completed_materials = (
|
| 453 |
+
SELECT COUNT(id) FROM academy_material_progresses amp
|
| 454 |
+
WHERE amp.academy_id = ap.academy_id AND amp.account_id = ap.account_id AND amp.status = 'COMPLETED'
|
| 455 |
+
),
|
| 456 |
+
status = CASE
|
| 457 |
+
WHEN (
|
| 458 |
+
SELECT COUNT(id) FROM academy_material_progresses amp
|
| 459 |
+
WHERE amp.academy_id = ap.academy_id AND amp.account_id = ap.account_id AND amp.status = 'COMPLETED'
|
| 460 |
+
) >= ? THEN 'COMPLETED'
|
| 461 |
+
ELSE 'IN_PROGRESS'
|
| 462 |
+
END,
|
| 463 |
+
completed_at = CASE
|
| 464 |
+
WHEN (
|
| 465 |
+
SELECT COUNT(id) FROM academy_material_progresses amp
|
| 466 |
+
WHERE amp.academy_id = ap.academy_id AND amp.account_id = ap.account_id AND amp.status = 'COMPLETED'
|
| 467 |
+
) >= ? THEN NOW()
|
| 468 |
+
ELSE NULL
|
| 469 |
+
END
|
| 470 |
+
WHERE ap.academy_id = ?
|
| 471 |
+
`, totalMaterials, totalMaterials, totalMaterials, academyId).Error
|
| 472 |
+
}
|
repositories/exam_event_answer_repository.go
CHANGED
|
@@ -1,59 +1,59 @@
|
|
| 1 |
-
package repositories
|
| 2 |
-
|
| 3 |
-
import (
|
| 4 |
-
"context"
|
| 5 |
-
|
| 6 |
-
entity "abdanhafidz.com/go-boilerplate/models/entity"
|
| 7 |
-
"github.com/google/uuid"
|
| 8 |
-
"gorm.io/gorm"
|
| 9 |
-
)
|
| 10 |
-
|
| 11 |
-
type ExamEventAnswerRepository interface {
|
| 12 |
-
Create(ctx context.Context, ans *entity.ExamEventAnswer) error
|
| 13 |
-
Update(ctx context.Context, ans *entity.ExamEventAnswer) error
|
| 14 |
-
GetByAttemptAndQuestion(ctx context.Context, attemptId uuid.UUID, questionId uuid.UUID) (entity.ExamEventAnswer, error)
|
| 15 |
-
ListByAttempt(ctx context.Context, attemptId uuid.UUID) ([]entity.ExamEventAnswer, error)
|
| 16 |
-
DeleteByAttempt(ctx context.Context, attemptId uuid.UUID) error
|
| 17 |
-
|
| 18 |
-
// decomposed result (answer + question)
|
| 19 |
-
}
|
| 20 |
-
|
| 21 |
-
type examEventAnswerRepository struct {
|
| 22 |
-
db *gorm.DB
|
| 23 |
-
}
|
| 24 |
-
|
| 25 |
-
func NewExamEventAnswerRepository(db *gorm.DB) ExamEventAnswerRepository {
|
| 26 |
-
return &examEventAnswerRepository{db: db}
|
| 27 |
-
}
|
| 28 |
-
|
| 29 |
-
func (r *examEventAnswerRepository) Create(ctx context.Context, ans *entity.ExamEventAnswer) error {
|
| 30 |
-
return r.db.WithContext(ctx).Create(ans).Error
|
| 31 |
-
}
|
| 32 |
-
|
| 33 |
-
func (r *examEventAnswerRepository) Update(ctx context.Context, ans *entity.ExamEventAnswer) error {
|
| 34 |
-
return r.db.WithContext(ctx).
|
| 35 |
-
Where("attempt_id = ? AND question_id = ?", ans.AttemptId, ans.QuestionId).
|
| 36 |
-
Updates(ans).Error
|
| 37 |
-
}
|
| 38 |
-
|
| 39 |
-
func (r *examEventAnswerRepository) GetByAttemptAndQuestion(ctx context.Context, attemptId uuid.UUID, questionId uuid.UUID) (entity.ExamEventAnswer, error) {
|
| 40 |
-
var ans entity.ExamEventAnswer
|
| 41 |
-
err := r.db.WithContext(ctx).
|
| 42 |
-
Where("attempt_id = ? AND question_id = ?", attemptId, questionId).
|
| 43 |
-
First(&ans).Error
|
| 44 |
-
return ans, err
|
| 45 |
-
}
|
| 46 |
-
|
| 47 |
-
func (r *examEventAnswerRepository) ListByAttempt(ctx context.Context, attemptId uuid.UUID) ([]entity.ExamEventAnswer, error) {
|
| 48 |
-
var answers []entity.ExamEventAnswer
|
| 49 |
-
err := r.db.WithContext(ctx).
|
| 50 |
-
Where("attempt_id = ?", attemptId).
|
| 51 |
-
Find(&answers).Error
|
| 52 |
-
return answers, err
|
| 53 |
-
}
|
| 54 |
-
|
| 55 |
-
func (r *examEventAnswerRepository) DeleteByAttempt(ctx context.Context, attemptId uuid.UUID) error {
|
| 56 |
-
return r.db.WithContext(ctx).
|
| 57 |
-
Where("attempt_id = ?", attemptId).
|
| 58 |
-
Delete(&entity.ExamEventAnswer{}).Error
|
| 59 |
-
}
|
|
|
|
| 1 |
+
package repositories
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"context"
|
| 5 |
+
|
| 6 |
+
entity "abdanhafidz.com/go-boilerplate/models/entity"
|
| 7 |
+
"github.com/google/uuid"
|
| 8 |
+
"gorm.io/gorm"
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
type ExamEventAnswerRepository interface {
|
| 12 |
+
Create(ctx context.Context, ans *entity.ExamEventAnswer) error
|
| 13 |
+
Update(ctx context.Context, ans *entity.ExamEventAnswer) error
|
| 14 |
+
GetByAttemptAndQuestion(ctx context.Context, attemptId uuid.UUID, questionId uuid.UUID) (entity.ExamEventAnswer, error)
|
| 15 |
+
ListByAttempt(ctx context.Context, attemptId uuid.UUID) ([]entity.ExamEventAnswer, error)
|
| 16 |
+
DeleteByAttempt(ctx context.Context, attemptId uuid.UUID) error
|
| 17 |
+
|
| 18 |
+
// decomposed result (answer + question)
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
type examEventAnswerRepository struct {
|
| 22 |
+
db *gorm.DB
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
func NewExamEventAnswerRepository(db *gorm.DB) ExamEventAnswerRepository {
|
| 26 |
+
return &examEventAnswerRepository{db: db}
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
func (r *examEventAnswerRepository) Create(ctx context.Context, ans *entity.ExamEventAnswer) error {
|
| 30 |
+
return r.db.WithContext(ctx).Create(ans).Error
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
func (r *examEventAnswerRepository) Update(ctx context.Context, ans *entity.ExamEventAnswer) error {
|
| 34 |
+
return r.db.WithContext(ctx).
|
| 35 |
+
Where("attempt_id = ? AND question_id = ?", ans.AttemptId, ans.QuestionId).
|
| 36 |
+
Updates(ans).Error
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
func (r *examEventAnswerRepository) GetByAttemptAndQuestion(ctx context.Context, attemptId uuid.UUID, questionId uuid.UUID) (entity.ExamEventAnswer, error) {
|
| 40 |
+
var ans entity.ExamEventAnswer
|
| 41 |
+
err := r.db.WithContext(ctx).
|
| 42 |
+
Where("attempt_id = ? AND question_id = ?", attemptId, questionId).
|
| 43 |
+
First(&ans).Error
|
| 44 |
+
return ans, err
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
func (r *examEventAnswerRepository) ListByAttempt(ctx context.Context, attemptId uuid.UUID) ([]entity.ExamEventAnswer, error) {
|
| 48 |
+
var answers []entity.ExamEventAnswer
|
| 49 |
+
err := r.db.WithContext(ctx).
|
| 50 |
+
Where("attempt_id = ?", attemptId).
|
| 51 |
+
Find(&answers).Error
|
| 52 |
+
return answers, err
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
func (r *examEventAnswerRepository) DeleteByAttempt(ctx context.Context, attemptId uuid.UUID) error {
|
| 56 |
+
return r.db.WithContext(ctx).
|
| 57 |
+
Where("attempt_id = ?", attemptId).
|
| 58 |
+
Delete(&entity.ExamEventAnswer{}).Error
|
| 59 |
+
}
|
repositories/exam_event_assign_repository.go
CHANGED
|
@@ -1,47 +1,47 @@
|
|
| 1 |
-
package repositories
|
| 2 |
-
|
| 3 |
-
import (
|
| 4 |
-
"context"
|
| 5 |
-
|
| 6 |
-
entity "abdanhafidz.com/go-boilerplate/models/entity"
|
| 7 |
-
"github.com/google/uuid"
|
| 8 |
-
"gorm.io/gorm"
|
| 9 |
-
)
|
| 10 |
-
|
| 11 |
-
type ExamEventAssignRepository interface {
|
| 12 |
-
Create(ctx context.Context, m entity.ExamEventAssign) error
|
| 13 |
-
ListByEvent(ctx context.Context, eventId uuid.UUID) ([]entity.ExamEventAssign, error)
|
| 14 |
-
Delete(ctx context.Context, id uuid.UUID) error
|
| 15 |
-
Check(ctx context.Context, eventId uuid.UUID, examId uuid.UUID) error
|
| 16 |
-
}
|
| 17 |
-
|
| 18 |
-
type examEventAssignRepository struct{ db *gorm.DB }
|
| 19 |
-
|
| 20 |
-
func NewExamEventAssignRepository(db *gorm.DB) ExamEventAssignRepository {
|
| 21 |
-
return &examEventAssignRepository{db}
|
| 22 |
-
}
|
| 23 |
-
|
| 24 |
-
func (r *examEventAssignRepository) Check(ctx context.Context, eventId uuid.UUID, examId uuid.UUID) error {
|
| 25 |
-
return r.db.WithContext(ctx).
|
| 26 |
-
Where("event_id = ?", eventId).
|
| 27 |
-
Where("exam_id = ?", examId).
|
| 28 |
-
First(&entity.ExamEventAssign{}).Error
|
| 29 |
-
}
|
| 30 |
-
func (r *examEventAssignRepository) Create(ctx context.Context, m entity.ExamEventAssign) error {
|
| 31 |
-
return r.db.WithContext(ctx).Create(&m).Error
|
| 32 |
-
}
|
| 33 |
-
|
| 34 |
-
func (r *examEventAssignRepository) ListByEvent(ctx context.Context, eventId uuid.UUID) ([]entity.ExamEventAssign, error) {
|
| 35 |
-
var items []entity.ExamEventAssign
|
| 36 |
-
err := r.db.WithContext(ctx).
|
| 37 |
-
Where("event_id = ?", eventId).
|
| 38 |
-
Preload("Exam").
|
| 39 |
-
Find(&items).Error
|
| 40 |
-
return items, err
|
| 41 |
-
}
|
| 42 |
-
|
| 43 |
-
func (r *examEventAssignRepository) Delete(ctx context.Context, id uuid.UUID) error {
|
| 44 |
-
return r.db.WithContext(ctx).
|
| 45 |
-
Where("id = ?", id).
|
| 46 |
-
Delete(&entity.ExamEventAssign{}).Error
|
| 47 |
-
}
|
|
|
|
| 1 |
+
package repositories
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"context"
|
| 5 |
+
|
| 6 |
+
entity "abdanhafidz.com/go-boilerplate/models/entity"
|
| 7 |
+
"github.com/google/uuid"
|
| 8 |
+
"gorm.io/gorm"
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
type ExamEventAssignRepository interface {
|
| 12 |
+
Create(ctx context.Context, m entity.ExamEventAssign) error
|
| 13 |
+
ListByEvent(ctx context.Context, eventId uuid.UUID) ([]entity.ExamEventAssign, error)
|
| 14 |
+
Delete(ctx context.Context, id uuid.UUID) error
|
| 15 |
+
Check(ctx context.Context, eventId uuid.UUID, examId uuid.UUID) error
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
type examEventAssignRepository struct{ db *gorm.DB }
|
| 19 |
+
|
| 20 |
+
func NewExamEventAssignRepository(db *gorm.DB) ExamEventAssignRepository {
|
| 21 |
+
return &examEventAssignRepository{db}
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
func (r *examEventAssignRepository) Check(ctx context.Context, eventId uuid.UUID, examId uuid.UUID) error {
|
| 25 |
+
return r.db.WithContext(ctx).
|
| 26 |
+
Where("event_id = ?", eventId).
|
| 27 |
+
Where("exam_id = ?", examId).
|
| 28 |
+
First(&entity.ExamEventAssign{}).Error
|
| 29 |
+
}
|
| 30 |
+
func (r *examEventAssignRepository) Create(ctx context.Context, m entity.ExamEventAssign) error {
|
| 31 |
+
return r.db.WithContext(ctx).Create(&m).Error
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
func (r *examEventAssignRepository) ListByEvent(ctx context.Context, eventId uuid.UUID) ([]entity.ExamEventAssign, error) {
|
| 35 |
+
var items []entity.ExamEventAssign
|
| 36 |
+
err := r.db.WithContext(ctx).
|
| 37 |
+
Where("event_id = ?", eventId).
|
| 38 |
+
Preload("Exam").
|
| 39 |
+
Find(&items).Error
|
| 40 |
+
return items, err
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
func (r *examEventAssignRepository) Delete(ctx context.Context, id uuid.UUID) error {
|
| 44 |
+
return r.db.WithContext(ctx).
|
| 45 |
+
Where("id = ?", id).
|
| 46 |
+
Delete(&entity.ExamEventAssign{}).Error
|
| 47 |
+
}
|
repositories/exam_event_attempt_repository.go
CHANGED
|
@@ -1,55 +1,55 @@
|
|
| 1 |
-
package repositories
|
| 2 |
-
|
| 3 |
-
import (
|
| 4 |
-
"context"
|
| 5 |
-
|
| 6 |
-
entity "abdanhafidz.com/go-boilerplate/models/entity"
|
| 7 |
-
"github.com/google/uuid"
|
| 8 |
-
"gorm.io/gorm"
|
| 9 |
-
)
|
| 10 |
-
|
| 11 |
-
type ExamEventAttemptRepository interface {
|
| 12 |
-
Create(ctx context.Context, a *entity.ExamEventAttempt) error
|
| 13 |
-
GetById(ctx context.Context, attemptId uuid.UUID) (entity.ExamEventAttempt, error)
|
| 14 |
-
GetByExamEvent(ctx context.Context, eventId uuid.UUID, examId uuid.UUID, accountId uuid.UUID) (entity.ExamEventAttempt, error)
|
| 15 |
-
Update(ctx context.Context, a *entity.ExamEventAttempt) error
|
| 16 |
-
}
|
| 17 |
-
|
| 18 |
-
type examEventAttemptRepository struct{ db *gorm.DB }
|
| 19 |
-
|
| 20 |
-
func NewExamEventAttemptRepository(db *gorm.DB) ExamEventAttemptRepository {
|
| 21 |
-
return &examEventAttemptRepository{db}
|
| 22 |
-
}
|
| 23 |
-
|
| 24 |
-
func (r *examEventAttemptRepository) Create(ctx context.Context, a *entity.ExamEventAttempt) error {
|
| 25 |
-
return r.db.WithContext(ctx).Create(a).Error
|
| 26 |
-
}
|
| 27 |
-
|
| 28 |
-
func (r *examEventAttemptRepository) GetById(ctx context.Context, attemptId uuid.UUID) (entity.ExamEventAttempt, error) {
|
| 29 |
-
var a entity.ExamEventAttempt
|
| 30 |
-
err := r.db.WithContext(ctx).
|
| 31 |
-
Preload("Answers").
|
| 32 |
-
First(&a, "id = ?", attemptId).Error
|
| 33 |
-
return a, err
|
| 34 |
-
}
|
| 35 |
-
|
| 36 |
-
func (r *examEventAttemptRepository) GetByExamEvent(ctx context.Context, eventId uuid.UUID, examId uuid.UUID, accountId uuid.UUID) (entity.ExamEventAttempt, error) {
|
| 37 |
-
|
| 38 |
-
var attempt entity.ExamEventAttempt
|
| 39 |
-
|
| 40 |
-
err := r.db.WithContext(ctx).
|
| 41 |
-
Preload("Answers").
|
| 42 |
-
Where("event_id = ?", eventId).
|
| 43 |
-
Where("exam_id = ?", examId).
|
| 44 |
-
Where("account_id = ?", accountId).
|
| 45 |
-
First(&attempt).Error
|
| 46 |
-
|
| 47 |
-
return attempt, err
|
| 48 |
-
}
|
| 49 |
-
|
| 50 |
-
func (r *examEventAttemptRepository) Update(ctx context.Context, a *entity.ExamEventAttempt) error {
|
| 51 |
-
return r.db.WithContext(ctx).
|
| 52 |
-
Model(&entity.ExamEventAttempt{}).
|
| 53 |
-
Where("id = ?", a.Id).
|
| 54 |
-
Updates(a).Error
|
| 55 |
-
}
|
|
|
|
| 1 |
+
package repositories
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"context"
|
| 5 |
+
|
| 6 |
+
entity "abdanhafidz.com/go-boilerplate/models/entity"
|
| 7 |
+
"github.com/google/uuid"
|
| 8 |
+
"gorm.io/gorm"
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
type ExamEventAttemptRepository interface {
|
| 12 |
+
Create(ctx context.Context, a *entity.ExamEventAttempt) error
|
| 13 |
+
GetById(ctx context.Context, attemptId uuid.UUID) (entity.ExamEventAttempt, error)
|
| 14 |
+
GetByExamEvent(ctx context.Context, eventId uuid.UUID, examId uuid.UUID, accountId uuid.UUID) (entity.ExamEventAttempt, error)
|
| 15 |
+
Update(ctx context.Context, a *entity.ExamEventAttempt) error
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
type examEventAttemptRepository struct{ db *gorm.DB }
|
| 19 |
+
|
| 20 |
+
func NewExamEventAttemptRepository(db *gorm.DB) ExamEventAttemptRepository {
|
| 21 |
+
return &examEventAttemptRepository{db}
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
func (r *examEventAttemptRepository) Create(ctx context.Context, a *entity.ExamEventAttempt) error {
|
| 25 |
+
return r.db.WithContext(ctx).Create(a).Error
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
func (r *examEventAttemptRepository) GetById(ctx context.Context, attemptId uuid.UUID) (entity.ExamEventAttempt, error) {
|
| 29 |
+
var a entity.ExamEventAttempt
|
| 30 |
+
err := r.db.WithContext(ctx).
|
| 31 |
+
Preload("Answers").
|
| 32 |
+
First(&a, "id = ?", attemptId).Error
|
| 33 |
+
return a, err
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
func (r *examEventAttemptRepository) GetByExamEvent(ctx context.Context, eventId uuid.UUID, examId uuid.UUID, accountId uuid.UUID) (entity.ExamEventAttempt, error) {
|
| 37 |
+
|
| 38 |
+
var attempt entity.ExamEventAttempt
|
| 39 |
+
|
| 40 |
+
err := r.db.WithContext(ctx).
|
| 41 |
+
Preload("Answers").
|
| 42 |
+
Where("event_id = ?", eventId).
|
| 43 |
+
Where("exam_id = ?", examId).
|
| 44 |
+
Where("account_id = ?", accountId).
|
| 45 |
+
First(&attempt).Error
|
| 46 |
+
|
| 47 |
+
return attempt, err
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
func (r *examEventAttemptRepository) Update(ctx context.Context, a *entity.ExamEventAttempt) error {
|
| 51 |
+
return r.db.WithContext(ctx).
|
| 52 |
+
Model(&entity.ExamEventAttempt{}).
|
| 53 |
+
Where("id = ?", a.Id).
|
| 54 |
+
Updates(a).Error
|
| 55 |
+
}
|
repositories/exam_event_repository.go
CHANGED
|
@@ -1,85 +1,85 @@
|
|
| 1 |
-
package repositories
|
| 2 |
-
|
| 3 |
-
import (
|
| 4 |
-
"context"
|
| 5 |
-
|
| 6 |
-
entity "abdanhafidz.com/go-boilerplate/models/entity"
|
| 7 |
-
"github.com/google/uuid"
|
| 8 |
-
"gorm.io/gorm"
|
| 9 |
-
)
|
| 10 |
-
|
| 11 |
-
type ExamRepository interface {
|
| 12 |
-
Create(ctx context.Context, e entity.Exam) error
|
| 13 |
-
Get(ctx context.Context, id uuid.UUID) (entity.Exam, error)
|
| 14 |
-
GetBySlug(ctx context.Context, slug string) (entity.Exam, error)
|
| 15 |
-
Update(ctx context.Context, e entity.Exam) error
|
| 16 |
-
Delete(ctx context.Context, id uuid.UUID) error
|
| 17 |
-
List(ctx context.Context) ([]entity.Exam, error)
|
| 18 |
-
|
| 19 |
-
// Additional business need
|
| 20 |
-
ListByEvent(ctx context.Context, eventId uuid.UUID) ([]entity.Exam, error)
|
| 21 |
-
}
|
| 22 |
-
|
| 23 |
-
type examRepository struct {
|
| 24 |
-
db *gorm.DB
|
| 25 |
-
}
|
| 26 |
-
|
| 27 |
-
func NewExamRepository(db *gorm.DB) ExamRepository {
|
| 28 |
-
return &examRepository{db}
|
| 29 |
-
}
|
| 30 |
-
|
| 31 |
-
// ================= CRUD =================
|
| 32 |
-
|
| 33 |
-
func (r *examRepository) Create(ctx context.Context, e entity.Exam) error {
|
| 34 |
-
return r.db.WithContext(ctx).Create(&e).Error
|
| 35 |
-
}
|
| 36 |
-
|
| 37 |
-
func (r *examRepository) Get(ctx context.Context, id uuid.UUID) (entity.Exam, error) {
|
| 38 |
-
var e entity.Exam
|
| 39 |
-
err := r.db.WithContext(ctx).
|
| 40 |
-
First(&e, "exam_id = ?", id).Error
|
| 41 |
-
return e, err
|
| 42 |
-
}
|
| 43 |
-
|
| 44 |
-
func (r *examRepository) GetBySlug(ctx context.Context, slug string) (entity.Exam, error) {
|
| 45 |
-
var e entity.Exam
|
| 46 |
-
err := r.db.WithContext(ctx).
|
| 47 |
-
Where("slug = ?", slug).
|
| 48 |
-
First(&e).Error
|
| 49 |
-
return e, err
|
| 50 |
-
}
|
| 51 |
-
|
| 52 |
-
func (r *examRepository) Update(ctx context.Context, e entity.Exam) error {
|
| 53 |
-
return r.db.WithContext(ctx).
|
| 54 |
-
Model(&entity.Exam{}).
|
| 55 |
-
Where("exam_id = ?", e.Id).
|
| 56 |
-
Updates(e).Error
|
| 57 |
-
}
|
| 58 |
-
|
| 59 |
-
func (r *examRepository) Delete(ctx context.Context, id uuid.UUID) error {
|
| 60 |
-
return r.db.WithContext(ctx).
|
| 61 |
-
Where("exam_id = ?", id).
|
| 62 |
-
Delete(&entity.Exam{}).Error
|
| 63 |
-
}
|
| 64 |
-
|
| 65 |
-
func (r *examRepository) List(ctx context.Context) ([]entity.Exam, error) {
|
| 66 |
-
var list []entity.Exam
|
| 67 |
-
err := r.db.WithContext(ctx).
|
| 68 |
-
Order("created_at DESC").
|
| 69 |
-
Find(&list).Error
|
| 70 |
-
return list, err
|
| 71 |
-
}
|
| 72 |
-
|
| 73 |
-
// =========== Business Specific ============
|
| 74 |
-
|
| 75 |
-
func (r *examRepository) ListByEvent(ctx context.Context, eventId uuid.UUID) ([]entity.Exam, error) {
|
| 76 |
-
var exams []entity.Exam
|
| 77 |
-
|
| 78 |
-
err := r.db.WithContext(ctx).
|
| 79 |
-
Table("exam").
|
| 80 |
-
Joins(`JOIN exam_event_assign ON exam_event_assign.exam_id = exam.id`).
|
| 81 |
-
Where("exam_event_assign.event_id = ?", eventId).
|
| 82 |
-
Find(&exams).Error
|
| 83 |
-
|
| 84 |
-
return exams, err
|
| 85 |
-
}
|
|
|
|
| 1 |
+
package repositories
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"context"
|
| 5 |
+
|
| 6 |
+
entity "abdanhafidz.com/go-boilerplate/models/entity"
|
| 7 |
+
"github.com/google/uuid"
|
| 8 |
+
"gorm.io/gorm"
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
type ExamRepository interface {
|
| 12 |
+
Create(ctx context.Context, e entity.Exam) error
|
| 13 |
+
Get(ctx context.Context, id uuid.UUID) (entity.Exam, error)
|
| 14 |
+
GetBySlug(ctx context.Context, slug string) (entity.Exam, error)
|
| 15 |
+
Update(ctx context.Context, e entity.Exam) error
|
| 16 |
+
Delete(ctx context.Context, id uuid.UUID) error
|
| 17 |
+
List(ctx context.Context) ([]entity.Exam, error)
|
| 18 |
+
|
| 19 |
+
// Additional business need
|
| 20 |
+
ListByEvent(ctx context.Context, eventId uuid.UUID) ([]entity.Exam, error)
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
type examRepository struct {
|
| 24 |
+
db *gorm.DB
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
func NewExamRepository(db *gorm.DB) ExamRepository {
|
| 28 |
+
return &examRepository{db}
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
// ================= CRUD =================
|
| 32 |
+
|
| 33 |
+
func (r *examRepository) Create(ctx context.Context, e entity.Exam) error {
|
| 34 |
+
return r.db.WithContext(ctx).Create(&e).Error
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
func (r *examRepository) Get(ctx context.Context, id uuid.UUID) (entity.Exam, error) {
|
| 38 |
+
var e entity.Exam
|
| 39 |
+
err := r.db.WithContext(ctx).
|
| 40 |
+
First(&e, "exam_id = ?", id).Error
|
| 41 |
+
return e, err
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
func (r *examRepository) GetBySlug(ctx context.Context, slug string) (entity.Exam, error) {
|
| 45 |
+
var e entity.Exam
|
| 46 |
+
err := r.db.WithContext(ctx).
|
| 47 |
+
Where("slug = ?", slug).
|
| 48 |
+
First(&e).Error
|
| 49 |
+
return e, err
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
func (r *examRepository) Update(ctx context.Context, e entity.Exam) error {
|
| 53 |
+
return r.db.WithContext(ctx).
|
| 54 |
+
Model(&entity.Exam{}).
|
| 55 |
+
Where("exam_id = ?", e.Id).
|
| 56 |
+
Updates(e).Error
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
func (r *examRepository) Delete(ctx context.Context, id uuid.UUID) error {
|
| 60 |
+
return r.db.WithContext(ctx).
|
| 61 |
+
Where("exam_id = ?", id).
|
| 62 |
+
Delete(&entity.Exam{}).Error
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
func (r *examRepository) List(ctx context.Context) ([]entity.Exam, error) {
|
| 66 |
+
var list []entity.Exam
|
| 67 |
+
err := r.db.WithContext(ctx).
|
| 68 |
+
Order("created_at DESC").
|
| 69 |
+
Find(&list).Error
|
| 70 |
+
return list, err
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
// =========== Business Specific ============
|
| 74 |
+
|
| 75 |
+
func (r *examRepository) ListByEvent(ctx context.Context, eventId uuid.UUID) ([]entity.Exam, error) {
|
| 76 |
+
var exams []entity.Exam
|
| 77 |
+
|
| 78 |
+
err := r.db.WithContext(ctx).
|
| 79 |
+
Table("exam").
|
| 80 |
+
Joins(`JOIN exam_event_assign ON exam_event_assign.exam_id = exam.id`).
|
| 81 |
+
Where("exam_event_assign.event_id = ?", eventId).
|
| 82 |
+
Find(&exams).Error
|
| 83 |
+
|
| 84 |
+
return exams, err
|
| 85 |
+
}
|
repositories/file_repository.go
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package repositories
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"context"
|
| 5 |
+
"errors"
|
| 6 |
+
|
| 7 |
+
"github.com/google/uuid"
|
| 8 |
+
"gorm.io/gorm"
|
| 9 |
+
|
| 10 |
+
entity "abdanhafidz.com/go-boilerplate/models/entity"
|
| 11 |
+
http_error "abdanhafidz.com/go-boilerplate/models/error"
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
type FileRepository interface {
|
| 15 |
+
Create(ctx context.Context, file *entity.File) error
|
| 16 |
+
FindByID(ctx context.Context, id uuid.UUID) (*entity.File, error)
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
type fileRepository struct {
|
| 20 |
+
db *gorm.DB
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
func NewFileRepository(db *gorm.DB) FileRepository {
|
| 24 |
+
return &fileRepository{db: db}
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
func (r *fileRepository) Create(ctx context.Context, file *entity.File) error {
|
| 28 |
+
return r.db.WithContext(ctx).Create(file).Error
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
func (r *fileRepository) FindByID(ctx context.Context, id uuid.UUID) (*entity.File, error) {
|
| 32 |
+
var file entity.File
|
| 33 |
+
result := r.db.WithContext(ctx).First(&file, "id = ?", id)
|
| 34 |
+
|
| 35 |
+
if result.Error != nil {
|
| 36 |
+
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
| 37 |
+
return nil, http_error.NOT_FOUND_ERROR
|
| 38 |
+
}
|
| 39 |
+
return nil, result.Error
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
return &file, nil
|
| 43 |
+
}
|
repositories/problem_set_exam_assign_repository.go
CHANGED
|
@@ -1,40 +1,40 @@
|
|
| 1 |
-
package repositories
|
| 2 |
-
|
| 3 |
-
import (
|
| 4 |
-
"context"
|
| 5 |
-
|
| 6 |
-
entity "abdanhafidz.com/go-boilerplate/models/entity"
|
| 7 |
-
"github.com/google/uuid"
|
| 8 |
-
"gorm.io/gorm"
|
| 9 |
-
)
|
| 10 |
-
|
| 11 |
-
type ProblemSetExamAssignRepository interface {
|
| 12 |
-
Create(ctx context.Context, m entity.ProblemSetExamAssign) error
|
| 13 |
-
GetByExam(ctx context.Context, examId uuid.UUID) (entity.ProblemSetExamAssign, error)
|
| 14 |
-
Delete(ctx context.Context, id uuid.UUID) error
|
| 15 |
-
}
|
| 16 |
-
|
| 17 |
-
type problemSetExamAssignRepository struct{ db *gorm.DB }
|
| 18 |
-
|
| 19 |
-
func NewProblemSetExamAssignRepository(db *gorm.DB) ProblemSetExamAssignRepository {
|
| 20 |
-
return &problemSetExamAssignRepository{db}
|
| 21 |
-
}
|
| 22 |
-
|
| 23 |
-
func (r *problemSetExamAssignRepository) Create(ctx context.Context, m entity.ProblemSetExamAssign) error {
|
| 24 |
-
return r.db.WithContext(ctx).Create(&m).Error
|
| 25 |
-
}
|
| 26 |
-
|
| 27 |
-
func (r *problemSetExamAssignRepository) GetByExam(ctx context.Context, examId uuid.UUID) (entity.ProblemSetExamAssign, error) {
|
| 28 |
-
var items entity.ProblemSetExamAssign
|
| 29 |
-
err := r.db.WithContext(ctx).
|
| 30 |
-
Where("exam_id = ?", examId).
|
| 31 |
-
Preload("ProblemSet").
|
| 32 |
-
First(&items).Error
|
| 33 |
-
return items, err
|
| 34 |
-
}
|
| 35 |
-
|
| 36 |
-
func (r *problemSetExamAssignRepository) Delete(ctx context.Context, id uuid.UUID) error {
|
| 37 |
-
return r.db.WithContext(ctx).
|
| 38 |
-
Where("id = ?", id).
|
| 39 |
-
Delete(&entity.ProblemSetExamAssign{}).Error
|
| 40 |
-
}
|
|
|
|
| 1 |
+
package repositories
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"context"
|
| 5 |
+
|
| 6 |
+
entity "abdanhafidz.com/go-boilerplate/models/entity"
|
| 7 |
+
"github.com/google/uuid"
|
| 8 |
+
"gorm.io/gorm"
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
type ProblemSetExamAssignRepository interface {
|
| 12 |
+
Create(ctx context.Context, m entity.ProblemSetExamAssign) error
|
| 13 |
+
GetByExam(ctx context.Context, examId uuid.UUID) (entity.ProblemSetExamAssign, error)
|
| 14 |
+
Delete(ctx context.Context, id uuid.UUID) error
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
type problemSetExamAssignRepository struct{ db *gorm.DB }
|
| 18 |
+
|
| 19 |
+
func NewProblemSetExamAssignRepository(db *gorm.DB) ProblemSetExamAssignRepository {
|
| 20 |
+
return &problemSetExamAssignRepository{db}
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
func (r *problemSetExamAssignRepository) Create(ctx context.Context, m entity.ProblemSetExamAssign) error {
|
| 24 |
+
return r.db.WithContext(ctx).Create(&m).Error
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
func (r *problemSetExamAssignRepository) GetByExam(ctx context.Context, examId uuid.UUID) (entity.ProblemSetExamAssign, error) {
|
| 28 |
+
var items entity.ProblemSetExamAssign
|
| 29 |
+
err := r.db.WithContext(ctx).
|
| 30 |
+
Where("exam_id = ?", examId).
|
| 31 |
+
Preload("ProblemSet").
|
| 32 |
+
First(&items).Error
|
| 33 |
+
return items, err
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
func (r *problemSetExamAssignRepository) Delete(ctx context.Context, id uuid.UUID) error {
|
| 37 |
+
return r.db.WithContext(ctx).
|
| 38 |
+
Where("id = ?", id).
|
| 39 |
+
Delete(&entity.ProblemSetExamAssign{}).Error
|
| 40 |
+
}
|
repositories/problem_set_repository.go
CHANGED
|
@@ -1,57 +1,57 @@
|
|
| 1 |
-
package repositories
|
| 2 |
-
|
| 3 |
-
import (
|
| 4 |
-
"context"
|
| 5 |
-
|
| 6 |
-
entity "abdanhafidz.com/go-boilerplate/models/entity"
|
| 7 |
-
"github.com/google/uuid"
|
| 8 |
-
"gorm.io/gorm"
|
| 9 |
-
)
|
| 10 |
-
|
| 11 |
-
type ProblemSetRepository interface {
|
| 12 |
-
Create(ctx context.Context, ps entity.ProblemSet) error
|
| 13 |
-
Get(ctx context.Context, id uuid.UUID) (entity.ProblemSet, error)
|
| 14 |
-
Update(ctx context.Context, ps entity.ProblemSet) error
|
| 15 |
-
Delete(ctx context.Context, id uuid.UUID) error
|
| 16 |
-
List(ctx context.Context) ([]entity.ProblemSet, error)
|
| 17 |
-
}
|
| 18 |
-
|
| 19 |
-
type problemSetRepository struct {
|
| 20 |
-
db *gorm.DB
|
| 21 |
-
}
|
| 22 |
-
|
| 23 |
-
func NewProblemSetRepository(db *gorm.DB) ProblemSetRepository {
|
| 24 |
-
return &problemSetRepository{db: db}
|
| 25 |
-
}
|
| 26 |
-
|
| 27 |
-
func (r *problemSetRepository) Create(ctx context.Context, ps entity.ProblemSet) error {
|
| 28 |
-
return r.db.WithContext(ctx).Create(&ps).Error
|
| 29 |
-
}
|
| 30 |
-
|
| 31 |
-
func (r *problemSetRepository) Get(ctx context.Context, id uuid.UUID) (entity.ProblemSet, error) {
|
| 32 |
-
var ps entity.ProblemSet
|
| 33 |
-
err := r.db.WithContext(ctx).
|
| 34 |
-
First(&ps, "id = ?", id).Error
|
| 35 |
-
return ps, err
|
| 36 |
-
}
|
| 37 |
-
|
| 38 |
-
func (r *problemSetRepository) List(ctx context.Context) ([]entity.ProblemSet, error) {
|
| 39 |
-
var list []entity.ProblemSet
|
| 40 |
-
err := r.db.WithContext(ctx).
|
| 41 |
-
Order("title").
|
| 42 |
-
Find(&list).Error
|
| 43 |
-
return list, err
|
| 44 |
-
}
|
| 45 |
-
|
| 46 |
-
func (r *problemSetRepository) Update(ctx context.Context, ps entity.ProblemSet) error {
|
| 47 |
-
return r.db.WithContext(ctx).
|
| 48 |
-
Model(&entity.ProblemSet{}).
|
| 49 |
-
Where("id = ?", ps.Id).
|
| 50 |
-
Updates(ps).Error
|
| 51 |
-
}
|
| 52 |
-
|
| 53 |
-
func (r *problemSetRepository) Delete(ctx context.Context, id uuid.UUID) error {
|
| 54 |
-
return r.db.WithContext(ctx).
|
| 55 |
-
Where("id = ?", id).
|
| 56 |
-
Delete(&entity.ProblemSet{}).Error
|
| 57 |
-
}
|
|
|
|
| 1 |
+
package repositories
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"context"
|
| 5 |
+
|
| 6 |
+
entity "abdanhafidz.com/go-boilerplate/models/entity"
|
| 7 |
+
"github.com/google/uuid"
|
| 8 |
+
"gorm.io/gorm"
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
type ProblemSetRepository interface {
|
| 12 |
+
Create(ctx context.Context, ps entity.ProblemSet) error
|
| 13 |
+
Get(ctx context.Context, id uuid.UUID) (entity.ProblemSet, error)
|
| 14 |
+
Update(ctx context.Context, ps entity.ProblemSet) error
|
| 15 |
+
Delete(ctx context.Context, id uuid.UUID) error
|
| 16 |
+
List(ctx context.Context) ([]entity.ProblemSet, error)
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
type problemSetRepository struct {
|
| 20 |
+
db *gorm.DB
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
func NewProblemSetRepository(db *gorm.DB) ProblemSetRepository {
|
| 24 |
+
return &problemSetRepository{db: db}
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
func (r *problemSetRepository) Create(ctx context.Context, ps entity.ProblemSet) error {
|
| 28 |
+
return r.db.WithContext(ctx).Create(&ps).Error
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
func (r *problemSetRepository) Get(ctx context.Context, id uuid.UUID) (entity.ProblemSet, error) {
|
| 32 |
+
var ps entity.ProblemSet
|
| 33 |
+
err := r.db.WithContext(ctx).
|
| 34 |
+
First(&ps, "id = ?", id).Error
|
| 35 |
+
return ps, err
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
func (r *problemSetRepository) List(ctx context.Context) ([]entity.ProblemSet, error) {
|
| 39 |
+
var list []entity.ProblemSet
|
| 40 |
+
err := r.db.WithContext(ctx).
|
| 41 |
+
Order("title").
|
| 42 |
+
Find(&list).Error
|
| 43 |
+
return list, err
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
func (r *problemSetRepository) Update(ctx context.Context, ps entity.ProblemSet) error {
|
| 47 |
+
return r.db.WithContext(ctx).
|
| 48 |
+
Model(&entity.ProblemSet{}).
|
| 49 |
+
Where("id = ?", ps.Id).
|
| 50 |
+
Updates(ps).Error
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
func (r *problemSetRepository) Delete(ctx context.Context, id uuid.UUID) error {
|
| 54 |
+
return r.db.WithContext(ctx).
|
| 55 |
+
Where("id = ?", id).
|
| 56 |
+
Delete(&entity.ProblemSet{}).Error
|
| 57 |
+
}
|
repositories/question_repository.go
CHANGED
|
@@ -1,55 +1,55 @@
|
|
| 1 |
-
package repositories
|
| 2 |
-
|
| 3 |
-
import (
|
| 4 |
-
"context"
|
| 5 |
-
|
| 6 |
-
entity "abdanhafidz.com/go-boilerplate/models/entity"
|
| 7 |
-
"github.com/google/uuid"
|
| 8 |
-
"gorm.io/gorm"
|
| 9 |
-
)
|
| 10 |
-
|
| 11 |
-
type QuestionsRepository interface {
|
| 12 |
-
Create(ctx context.Context, q entity.Questions) error
|
| 13 |
-
Get(ctx context.Context, id uuid.UUID) (entity.Questions, error)
|
| 14 |
-
Update(ctx context.Context, q entity.Questions) error
|
| 15 |
-
Delete(ctx context.Context, id uuid.UUID) error
|
| 16 |
-
ListByProblemSet(ctx context.Context, problemSetId uuid.UUID) ([]entity.Questions, error)
|
| 17 |
-
}
|
| 18 |
-
|
| 19 |
-
type questionsRepository struct{ db *gorm.DB }
|
| 20 |
-
|
| 21 |
-
func NewQuestionsRepository(db *gorm.DB) QuestionsRepository {
|
| 22 |
-
return &questionsRepository{db}
|
| 23 |
-
}
|
| 24 |
-
|
| 25 |
-
func (r *questionsRepository) Create(ctx context.Context, q entity.Questions) error {
|
| 26 |
-
return r.db.WithContext(ctx).Create(&q).Error
|
| 27 |
-
}
|
| 28 |
-
|
| 29 |
-
func (r *questionsRepository) Get(ctx context.Context, id uuid.UUID) (entity.Questions, error) {
|
| 30 |
-
var q entity.Questions
|
| 31 |
-
err := r.db.WithContext(ctx).First(&q, "id = ?", id).Error
|
| 32 |
-
return q, err
|
| 33 |
-
}
|
| 34 |
-
|
| 35 |
-
func (r *questionsRepository) Update(ctx context.Context, q entity.Questions) error {
|
| 36 |
-
return r.db.WithContext(ctx).
|
| 37 |
-
Model(&entity.Questions{}).
|
| 38 |
-
Where("id = ?", q.Id).
|
| 39 |
-
Updates(q).Error
|
| 40 |
-
}
|
| 41 |
-
|
| 42 |
-
func (r *questionsRepository) Delete(ctx context.Context, id uuid.UUID) error {
|
| 43 |
-
return r.db.WithContext(ctx).
|
| 44 |
-
Where("id = ?", id).
|
| 45 |
-
Delete(&entity.Questions{}).Error
|
| 46 |
-
}
|
| 47 |
-
|
| 48 |
-
func (r *questionsRepository) ListByProblemSet(ctx context.Context, problemSetId uuid.UUID) ([]entity.Questions, error) {
|
| 49 |
-
var q []entity.Questions
|
| 50 |
-
err := r.db.WithContext(ctx).
|
| 51 |
-
Where("problem_set_id = ?", problemSetId).
|
| 52 |
-
Order("id").
|
| 53 |
-
Find(&q).Error
|
| 54 |
-
return q, err
|
| 55 |
-
}
|
|
|
|
| 1 |
+
package repositories
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"context"
|
| 5 |
+
|
| 6 |
+
entity "abdanhafidz.com/go-boilerplate/models/entity"
|
| 7 |
+
"github.com/google/uuid"
|
| 8 |
+
"gorm.io/gorm"
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
type QuestionsRepository interface {
|
| 12 |
+
Create(ctx context.Context, q entity.Questions) error
|
| 13 |
+
Get(ctx context.Context, id uuid.UUID) (entity.Questions, error)
|
| 14 |
+
Update(ctx context.Context, q entity.Questions) error
|
| 15 |
+
Delete(ctx context.Context, id uuid.UUID) error
|
| 16 |
+
ListByProblemSet(ctx context.Context, problemSetId uuid.UUID) ([]entity.Questions, error)
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
type questionsRepository struct{ db *gorm.DB }
|
| 20 |
+
|
| 21 |
+
func NewQuestionsRepository(db *gorm.DB) QuestionsRepository {
|
| 22 |
+
return &questionsRepository{db}
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
func (r *questionsRepository) Create(ctx context.Context, q entity.Questions) error {
|
| 26 |
+
return r.db.WithContext(ctx).Create(&q).Error
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
func (r *questionsRepository) Get(ctx context.Context, id uuid.UUID) (entity.Questions, error) {
|
| 30 |
+
var q entity.Questions
|
| 31 |
+
err := r.db.WithContext(ctx).First(&q, "id = ?", id).Error
|
| 32 |
+
return q, err
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
func (r *questionsRepository) Update(ctx context.Context, q entity.Questions) error {
|
| 36 |
+
return r.db.WithContext(ctx).
|
| 37 |
+
Model(&entity.Questions{}).
|
| 38 |
+
Where("id = ?", q.Id).
|
| 39 |
+
Updates(q).Error
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
func (r *questionsRepository) Delete(ctx context.Context, id uuid.UUID) error {
|
| 43 |
+
return r.db.WithContext(ctx).
|
| 44 |
+
Where("id = ?", id).
|
| 45 |
+
Delete(&entity.Questions{}).Error
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
func (r *questionsRepository) ListByProblemSet(ctx context.Context, problemSetId uuid.UUID) ([]entity.Questions, error) {
|
| 49 |
+
var q []entity.Questions
|
| 50 |
+
err := r.db.WithContext(ctx).
|
| 51 |
+
Where("problem_set_id = ?", problemSetId).
|
| 52 |
+
Order("id").
|
| 53 |
+
Find(&q).Error
|
| 54 |
+
return q, err
|
| 55 |
+
}
|
repositories/result_repository.go
CHANGED
|
@@ -1,60 +1,60 @@
|
|
| 1 |
-
package repositories
|
| 2 |
-
|
| 3 |
-
import (
|
| 4 |
-
"context"
|
| 5 |
-
|
| 6 |
-
entity "abdanhafidz.com/go-boilerplate/models/entity"
|
| 7 |
-
"github.com/google/uuid"
|
| 8 |
-
"gorm.io/gorm"
|
| 9 |
-
)
|
| 10 |
-
|
| 11 |
-
type ResultRepository interface {
|
| 12 |
-
Create(ctx context.Context, r *entity.Result) error
|
| 13 |
-
GetById(ctx context.Context, id uuid.UUID) (entity.Result, error)
|
| 14 |
-
Update(ctx context.Context, r *entity.Result) error
|
| 15 |
-
ListByEvent(ctx context.Context, eventId uuid.UUID) ([]entity.Result, error)
|
| 16 |
-
GetByAttemptId(ctx context.Context, attemptId uuid.UUID) (entity.Result, error)
|
| 17 |
-
}
|
| 18 |
-
|
| 19 |
-
type resultRepository struct{ db *gorm.DB }
|
| 20 |
-
|
| 21 |
-
func NewResultRepository(db *gorm.DB) ResultRepository {
|
| 22 |
-
return &resultRepository{db}
|
| 23 |
-
}
|
| 24 |
-
|
| 25 |
-
func (r *resultRepository) Create(ctx context.Context, rs *entity.Result) error {
|
| 26 |
-
return r.db.WithContext(ctx).Create(rs).Error
|
| 27 |
-
}
|
| 28 |
-
func (r *resultRepository) GetByAttemptId(ctx context.Context, attemptId uuid.UUID) (entity.Result, error) {
|
| 29 |
-
var rs entity.Result
|
| 30 |
-
err := r.db.WithContext(ctx).
|
| 31 |
-
Preload("ExamEventAttempt").
|
| 32 |
-
First(&rs, "attempt_id = ?", attemptId).Error
|
| 33 |
-
return rs, err
|
| 34 |
-
}
|
| 35 |
-
func (r *resultRepository) GetById(ctx context.Context, id uuid.UUID) (entity.Result, error) {
|
| 36 |
-
var rs entity.Result
|
| 37 |
-
err := r.db.WithContext(ctx).
|
| 38 |
-
Preload("Account").
|
| 39 |
-
Preload("Event").
|
| 40 |
-
Preload("ProblemSet").
|
| 41 |
-
Preload("ExamEventAttempt").
|
| 42 |
-
First(&rs, "id = ?", id).Error
|
| 43 |
-
return rs, err
|
| 44 |
-
}
|
| 45 |
-
|
| 46 |
-
func (r *resultRepository) Update(ctx context.Context, rs *entity.Result) error {
|
| 47 |
-
return r.db.WithContext(ctx).
|
| 48 |
-
Model(&entity.Result{}).
|
| 49 |
-
Where("id = ?", rs.Id).
|
| 50 |
-
Updates(rs).Error
|
| 51 |
-
}
|
| 52 |
-
|
| 53 |
-
func (r *resultRepository) ListByEvent(ctx context.Context, eventId uuid.UUID) ([]entity.Result, error) {
|
| 54 |
-
var list []entity.Result
|
| 55 |
-
err := r.db.WithContext(ctx).
|
| 56 |
-
Where("event_id = ?", eventId).
|
| 57 |
-
Preload("Account").
|
| 58 |
-
Find(&list).Error
|
| 59 |
-
return list, err
|
| 60 |
-
}
|
|
|
|
| 1 |
+
package repositories
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"context"
|
| 5 |
+
|
| 6 |
+
entity "abdanhafidz.com/go-boilerplate/models/entity"
|
| 7 |
+
"github.com/google/uuid"
|
| 8 |
+
"gorm.io/gorm"
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
type ResultRepository interface {
|
| 12 |
+
Create(ctx context.Context, r *entity.Result) error
|
| 13 |
+
GetById(ctx context.Context, id uuid.UUID) (entity.Result, error)
|
| 14 |
+
Update(ctx context.Context, r *entity.Result) error
|
| 15 |
+
ListByEvent(ctx context.Context, eventId uuid.UUID) ([]entity.Result, error)
|
| 16 |
+
GetByAttemptId(ctx context.Context, attemptId uuid.UUID) (entity.Result, error)
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
type resultRepository struct{ db *gorm.DB }
|
| 20 |
+
|
| 21 |
+
func NewResultRepository(db *gorm.DB) ResultRepository {
|
| 22 |
+
return &resultRepository{db}
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
func (r *resultRepository) Create(ctx context.Context, rs *entity.Result) error {
|
| 26 |
+
return r.db.WithContext(ctx).Create(rs).Error
|
| 27 |
+
}
|
| 28 |
+
func (r *resultRepository) GetByAttemptId(ctx context.Context, attemptId uuid.UUID) (entity.Result, error) {
|
| 29 |
+
var rs entity.Result
|
| 30 |
+
err := r.db.WithContext(ctx).
|
| 31 |
+
Preload("ExamEventAttempt").
|
| 32 |
+
First(&rs, "attempt_id = ?", attemptId).Error
|
| 33 |
+
return rs, err
|
| 34 |
+
}
|
| 35 |
+
func (r *resultRepository) GetById(ctx context.Context, id uuid.UUID) (entity.Result, error) {
|
| 36 |
+
var rs entity.Result
|
| 37 |
+
err := r.db.WithContext(ctx).
|
| 38 |
+
Preload("Account").
|
| 39 |
+
Preload("Event").
|
| 40 |
+
Preload("ProblemSet").
|
| 41 |
+
Preload("ExamEventAttempt").
|
| 42 |
+
First(&rs, "id = ?", id).Error
|
| 43 |
+
return rs, err
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
func (r *resultRepository) Update(ctx context.Context, rs *entity.Result) error {
|
| 47 |
+
return r.db.WithContext(ctx).
|
| 48 |
+
Model(&entity.Result{}).
|
| 49 |
+
Where("id = ?", rs.Id).
|
| 50 |
+
Updates(rs).Error
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
func (r *resultRepository) ListByEvent(ctx context.Context, eventId uuid.UUID) ([]entity.Result, error) {
|
| 54 |
+
var list []entity.Result
|
| 55 |
+
err := r.db.WithContext(ctx).
|
| 56 |
+
Where("event_id = ?", eventId).
|
| 57 |
+
Preload("Account").
|
| 58 |
+
Find(&list).Error
|
| 59 |
+
return list, err
|
| 60 |
+
}
|
router/academy_router.go
CHANGED
|
@@ -1,39 +1,45 @@
|
|
| 1 |
-
package router
|
| 2 |
-
|
| 3 |
-
import (
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
)
|
| 8 |
-
|
| 9 |
-
func AcademyRouter(router *gin.Engine, middleware provider.MiddlewareProvider, controller provider.ControllerProvider) {
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
}
|
|
|
|
| 1 |
+
package router
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"abdanhafidz.com/go-boilerplate/provider"
|
| 5 |
+
"github.com/gin-contrib/gzip"
|
| 6 |
+
"github.com/gin-gonic/gin"
|
| 7 |
+
)
|
| 8 |
+
|
| 9 |
+
func AcademyRouter(router *gin.Engine, middleware provider.MiddlewareProvider, controller provider.ControllerProvider) {
|
| 10 |
+
academyController := controller.ProvideAcademyController()
|
| 11 |
+
authenticationMiddleware := middleware.ProvideAuthenticationMiddleware()
|
| 12 |
+
routerGroup := router.Group("/api/v1/academy")
|
| 13 |
+
|
| 14 |
+
routerGroup.Use(gzip.Gzip(gzip.DefaultCompression))
|
| 15 |
+
|
| 16 |
+
{
|
| 17 |
+
// ================= ADMIN SECTION =================
|
| 18 |
+
adminGroup := routerGroup.Group("/admin", authenticationMiddleware.VerifyAccount)
|
| 19 |
+
{
|
| 20 |
+
// CRUD for Academies
|
| 21 |
+
adminGroup.POST("/", academyController.CreateAcademy)
|
| 22 |
+
adminGroup.GET("/id/:id/detail", academyController.GetAcademyDetail)
|
| 23 |
+
adminGroup.PUT("/id/:id", academyController.UpdateAcademy)
|
| 24 |
+
adminGroup.DELETE("/id/:id", academyController.DeleteAcademy)
|
| 25 |
+
|
| 26 |
+
// Materials
|
| 27 |
+
adminGroup.POST("/materials", academyController.CreateMaterial)
|
| 28 |
+
adminGroup.DELETE("/materials/:id", academyController.DeleteMaterial)
|
| 29 |
+
|
| 30 |
+
// Contents
|
| 31 |
+
adminGroup.POST("/contents", academyController.CreateContent)
|
| 32 |
+
adminGroup.DELETE("/contents/:id", academyController.DeleteContent)
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
// ================= USER SECTION =================
|
| 36 |
+
// Public/Student endpoints (Authenticated)
|
| 37 |
+
routerGroup.GET("/", authenticationMiddleware.VerifyAccount, academyController.ListAcademies)
|
| 38 |
+
routerGroup.GET("/:academy_slug", authenticationMiddleware.VerifyAccount, academyController.GetAcademy)
|
| 39 |
+
routerGroup.GET("/:academy_slug/:material_slug", authenticationMiddleware.VerifyAccount, academyController.GetMaterial)
|
| 40 |
+
routerGroup.GET("/:academy_slug/:material_slug/:order", authenticationMiddleware.VerifyAccount, academyController.GetContent)
|
| 41 |
+
|
| 42 |
+
// Update Progress
|
| 43 |
+
routerGroup.POST("/:academy_slug/:material_slug/:order", authenticationMiddleware.VerifyAccount, academyController.UpdateContentProgress)
|
| 44 |
+
}
|
| 45 |
}
|
router/account_detail_router.go
CHANGED
|
@@ -1,18 +1,18 @@
|
|
| 1 |
-
package router
|
| 2 |
-
|
| 3 |
-
import (
|
| 4 |
-
"abdanhafidz.com/go-boilerplate/provider"
|
| 5 |
-
"github.com/gin-contrib/gzip"
|
| 6 |
-
"github.com/gin-gonic/gin"
|
| 7 |
-
)
|
| 8 |
-
|
| 9 |
-
func AccountDetailRouter(router *gin.Engine, middleware provider.MiddlewareProvider, controller provider.ControllerProvider) {
|
| 10 |
-
routerGroup := router.Group("/api/v1/account")
|
| 11 |
-
accountDetailController := controller.ProvideAccountDetailController()
|
| 12 |
-
authenticationMiddleware := middleware.ProvideAuthenticationMiddleware()
|
| 13 |
-
routerGroup.Use(gzip.Gzip(gzip.DefaultCompression))
|
| 14 |
-
{
|
| 15 |
-
routerGroup.GET("/me", authenticationMiddleware.VerifyAccount, accountDetailController.GetDetail)
|
| 16 |
-
routerGroup.PUT("/me", authenticationMiddleware.VerifyAccount, accountDetailController.UpdateDetail)
|
| 17 |
-
}
|
| 18 |
-
}
|
|
|
|
| 1 |
+
package router
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"abdanhafidz.com/go-boilerplate/provider"
|
| 5 |
+
"github.com/gin-contrib/gzip"
|
| 6 |
+
"github.com/gin-gonic/gin"
|
| 7 |
+
)
|
| 8 |
+
|
| 9 |
+
func AccountDetailRouter(router *gin.Engine, middleware provider.MiddlewareProvider, controller provider.ControllerProvider) {
|
| 10 |
+
routerGroup := router.Group("/api/v1/account")
|
| 11 |
+
accountDetailController := controller.ProvideAccountDetailController()
|
| 12 |
+
authenticationMiddleware := middleware.ProvideAuthenticationMiddleware()
|
| 13 |
+
routerGroup.Use(gzip.Gzip(gzip.DefaultCompression))
|
| 14 |
+
{
|
| 15 |
+
routerGroup.GET("/me", authenticationMiddleware.VerifyAccount, accountDetailController.GetDetail)
|
| 16 |
+
routerGroup.PUT("/me", authenticationMiddleware.VerifyAccount, accountDetailController.UpdateDetail)
|
| 17 |
+
}
|
| 18 |
+
}
|
router/email_router.go
CHANGED
|
@@ -1,17 +1,17 @@
|
|
| 1 |
-
package router
|
| 2 |
-
|
| 3 |
-
import (
|
| 4 |
-
"abdanhafidz.com/go-boilerplate/provider"
|
| 5 |
-
"github.com/gin-contrib/gzip"
|
| 6 |
-
"github.com/gin-gonic/gin"
|
| 7 |
-
)
|
| 8 |
-
|
| 9 |
-
func EmailVerificationRouter(router *gin.Engine, controller provider.ControllerProvider) {
|
| 10 |
-
emailVerificationController := controller.ProvideEmailVerificationController()
|
| 11 |
-
routerGroup := router.Group("/api/v1/email")
|
| 12 |
-
routerGroup.Use(gzip.Gzip(gzip.DefaultCompression))
|
| 13 |
-
{
|
| 14 |
-
routerGroup.POST("/verify", emailVerificationController.Validate)
|
| 15 |
-
routerGroup.POST("/create-verification", emailVerificationController.Create)
|
| 16 |
-
}
|
| 17 |
-
}
|
|
|
|
| 1 |
+
package router
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"abdanhafidz.com/go-boilerplate/provider"
|
| 5 |
+
"github.com/gin-contrib/gzip"
|
| 6 |
+
"github.com/gin-gonic/gin"
|
| 7 |
+
)
|
| 8 |
+
|
| 9 |
+
func EmailVerificationRouter(router *gin.Engine, controller provider.ControllerProvider) {
|
| 10 |
+
emailVerificationController := controller.ProvideEmailVerificationController()
|
| 11 |
+
routerGroup := router.Group("/api/v1/email")
|
| 12 |
+
routerGroup.Use(gzip.Gzip(gzip.DefaultCompression))
|
| 13 |
+
{
|
| 14 |
+
routerGroup.POST("/verify", emailVerificationController.Validate)
|
| 15 |
+
routerGroup.POST("/create-verification", emailVerificationController.Create)
|
| 16 |
+
}
|
| 17 |
+
}
|
router/event_router.go
CHANGED
|
@@ -1,19 +1,19 @@
|
|
| 1 |
-
package router
|
| 2 |
-
|
| 3 |
-
import (
|
| 4 |
-
"abdanhafidz.com/go-boilerplate/provider"
|
| 5 |
-
"github.com/gin-contrib/gzip"
|
| 6 |
-
"github.com/gin-gonic/gin"
|
| 7 |
-
)
|
| 8 |
-
|
| 9 |
-
func EventRouter(router *gin.Engine, middleware provider.MiddlewareProvider, controller provider.ControllerProvider) {
|
| 10 |
-
eventController := controller.ProvideEventController()
|
| 11 |
-
authenticationMiddleware := middleware.ProvideAuthenticationMiddleware()
|
| 12 |
-
routerGroup := router.Group("api/v1/events")
|
| 13 |
-
routerGroup.Use(gzip.Gzip(gzip.DefaultCompression))
|
| 14 |
-
{
|
| 15 |
-
routerGroup.GET("/", authenticationMiddleware.VerifyAccount, eventController.List)
|
| 16 |
-
routerGroup.GET("/:event_slug", authenticationMiddleware.VerifyAccount, eventController.DetailBySlug)
|
| 17 |
-
routerGroup.POST("/register-event", authenticationMiddleware.VerifyAccount, eventController.Join)
|
| 18 |
-
}
|
| 19 |
-
}
|
|
|
|
| 1 |
+
package router
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"abdanhafidz.com/go-boilerplate/provider"
|
| 5 |
+
"github.com/gin-contrib/gzip"
|
| 6 |
+
"github.com/gin-gonic/gin"
|
| 7 |
+
)
|
| 8 |
+
|
| 9 |
+
func EventRouter(router *gin.Engine, middleware provider.MiddlewareProvider, controller provider.ControllerProvider) {
|
| 10 |
+
eventController := controller.ProvideEventController()
|
| 11 |
+
authenticationMiddleware := middleware.ProvideAuthenticationMiddleware()
|
| 12 |
+
routerGroup := router.Group("api/v1/events")
|
| 13 |
+
routerGroup.Use(gzip.Gzip(gzip.DefaultCompression))
|
| 14 |
+
{
|
| 15 |
+
routerGroup.GET("/", authenticationMiddleware.VerifyAccount, eventController.List)
|
| 16 |
+
routerGroup.GET("/:event_slug", authenticationMiddleware.VerifyAccount, eventController.DetailBySlug)
|
| 17 |
+
routerGroup.POST("/register-event", authenticationMiddleware.VerifyAccount, eventController.Join)
|
| 18 |
+
}
|
| 19 |
+
}
|