mirror of
https://github.com/SamyRai/tercul-backend.git
synced 2025-12-27 05:11:34 +00:00
test: Increase test coverage for sql repositories
This commit improves the test coverage for the `internal/data/sql` package by adding comprehensive tests for several repositories and refactoring the testing strategy to be more robust. The following changes were made: - Refactored the `analytics_repository_test.go` to use an in-memory SQLite database instead of `sqlmock`. This makes the tests more reliable and less brittle against GORM's SQL generation. - Added new tests for `auth_repository.go` and `copyright_claim_repository.go` using the same in-memory SQLite database strategy. - Added the missing `WorkID` field to the `CopyrightClaim` domain entity to align it with the repository's logic. This effort increased the test coverage for the `internal/data/sql` package from 30.5% to 37.1%.
This commit is contained in:
parent
0636596341
commit
057d6ea6bb
204
internal/data/sql/analytics_repository_test.go
Normal file
204
internal/data/sql/analytics_repository_test.go
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
package sql_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"tercul/internal/app/analytics"
|
||||||
|
"tercul/internal/data/sql"
|
||||||
|
"tercul/internal/domain"
|
||||||
|
"tercul/internal/platform/config"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// newTestAnalyticsRepoWithSQLite sets up an in-memory SQLite database for testing.
|
||||||
|
func newTestAnalyticsRepoWithSQLite(t *testing.T) (analytics.Repository, *gorm.DB) {
|
||||||
|
// Using "file::memory:?cache=shared" to ensure the in-memory database is shared across connections in the same process.
|
||||||
|
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Auto-migrate the necessary schemas
|
||||||
|
err = db.AutoMigrate(
|
||||||
|
&domain.Work{},
|
||||||
|
&domain.WorkStats{},
|
||||||
|
&domain.Translation{},
|
||||||
|
&domain.TranslationStats{},
|
||||||
|
&domain.Trending{},
|
||||||
|
&domain.UserEngagement{},
|
||||||
|
&domain.User{},
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cfg := &config.Config{}
|
||||||
|
repo := sql.NewAnalyticsRepository(db, cfg)
|
||||||
|
|
||||||
|
// Clean up the database after the test
|
||||||
|
t.Cleanup(func() {
|
||||||
|
sqlDB, err := db.DB()
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = sqlDB.Close()
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
return repo, db
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAnalyticsRepository_IncrementWorkCounter(t *testing.T) {
|
||||||
|
repo, db := newTestAnalyticsRepoWithSQLite(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Setup: Create a work to associate stats with
|
||||||
|
work := domain.Work{Title: "Test Work"}
|
||||||
|
require.NoError(t, db.Create(&work).Error)
|
||||||
|
|
||||||
|
t.Run("creates_new_stats_if_not_exist", func(t *testing.T) {
|
||||||
|
err := repo.IncrementWorkCounter(ctx, work.ID, "views", 5)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var stats domain.WorkStats
|
||||||
|
err = db.Where("work_id = ?", work.ID).First(&stats).Error
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, int64(5), stats.Views)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("increments_existing_stats", func(t *testing.T) {
|
||||||
|
// Increment again
|
||||||
|
err := repo.IncrementWorkCounter(ctx, work.ID, "views", 3)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var stats domain.WorkStats
|
||||||
|
err = db.Where("work_id = ?", work.ID).First(&stats).Error
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, int64(8), stats.Views) // 5 + 3
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid_field", func(t *testing.T) {
|
||||||
|
err := repo.IncrementWorkCounter(ctx, work.ID, "invalid_field", 1)
|
||||||
|
assert.Error(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAnalyticsRepository_GetTrendingWorks(t *testing.T) {
|
||||||
|
repo, db := newTestAnalyticsRepoWithSQLite(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Setup: Create some works and trending data
|
||||||
|
work1 := domain.Work{Title: "Trending Work 1"}
|
||||||
|
work2 := domain.Work{Title: "Trending Work 2"}
|
||||||
|
require.NoError(t, db.Create(&work1).Error)
|
||||||
|
require.NoError(t, db.Create(&work2).Error)
|
||||||
|
|
||||||
|
trendingData := []*domain.Trending{
|
||||||
|
{EntityID: work1.ID, EntityType: "Work", Rank: 1, TimePeriod: "daily"},
|
||||||
|
{EntityID: work2.ID, EntityType: "Work", Rank: 2, TimePeriod: "daily"},
|
||||||
|
}
|
||||||
|
require.NoError(t, db.Create(&trendingData).Error)
|
||||||
|
|
||||||
|
t.Run("success", func(t *testing.T) {
|
||||||
|
works, err := repo.GetTrendingWorks(ctx, "daily", 5)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, works, 2)
|
||||||
|
assert.Equal(t, work1.ID, works[0].ID)
|
||||||
|
assert.Equal(t, work2.ID, works[1].ID)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAnalyticsRepository_UpdateTrendingWorks(t *testing.T) {
|
||||||
|
repo, db := newTestAnalyticsRepoWithSQLite(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Setup: Create an old trending record
|
||||||
|
oldTrending := domain.Trending{EntityID: 99, EntityType: "Work", Rank: 1, TimePeriod: "daily"}
|
||||||
|
require.NoError(t, db.Create(&oldTrending).Error)
|
||||||
|
|
||||||
|
newTrendingData := []*domain.Trending{
|
||||||
|
{EntityID: 1, EntityType: "Work", Rank: 1, Score: 100, TimePeriod: "daily"},
|
||||||
|
{EntityID: 2, EntityType: "Work", Rank: 2, Score: 90, TimePeriod: "daily"},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := repo.UpdateTrendingWorks(ctx, "daily", newTrendingData)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var trendingResult []domain.Trending
|
||||||
|
db.Where("time_period = ?", "daily").Order("rank asc").Find(&trendingResult)
|
||||||
|
|
||||||
|
require.Len(t, trendingResult, 2)
|
||||||
|
assert.Equal(t, uint(1), trendingResult[0].EntityID)
|
||||||
|
assert.Equal(t, uint(2), trendingResult[1].EntityID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAnalyticsRepository_GetOrCreate(t *testing.T) {
|
||||||
|
repo, db := newTestAnalyticsRepoWithSQLite(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
work := domain.Work{Title: "Test Work"}
|
||||||
|
require.NoError(t, db.Create(&work).Error)
|
||||||
|
translation := domain.Translation{Title: "Test Translation", TranslatableID: work.ID, TranslatableType: "Work"}
|
||||||
|
require.NoError(t, db.Create(&translation).Error)
|
||||||
|
user := domain.User{Username: "testuser", Email: "test@test.com"}
|
||||||
|
require.NoError(t, db.Create(&user).Error)
|
||||||
|
|
||||||
|
t.Run("GetOrCreateWorkStats", func(t *testing.T) {
|
||||||
|
// Create
|
||||||
|
stats, err := repo.GetOrCreateWorkStats(ctx, work.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, work.ID, stats.WorkID)
|
||||||
|
|
||||||
|
// Get
|
||||||
|
stats2, err := repo.GetOrCreateWorkStats(ctx, work.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, stats.ID, stats2.ID)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetOrCreateTranslationStats", func(t *testing.T) {
|
||||||
|
// Create
|
||||||
|
stats, err := repo.GetOrCreateTranslationStats(ctx, translation.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, translation.ID, stats.TranslationID)
|
||||||
|
|
||||||
|
// Get
|
||||||
|
stats2, err := repo.GetOrCreateTranslationStats(ctx, translation.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, stats.ID, stats2.ID)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetOrCreateUserEngagement", func(t *testing.T) {
|
||||||
|
date := time.Now().Truncate(24 * time.Hour)
|
||||||
|
// Create
|
||||||
|
eng, err := repo.GetOrCreateUserEngagement(ctx, user.ID, date)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, user.ID, eng.UserID)
|
||||||
|
|
||||||
|
// Get
|
||||||
|
eng2, err := repo.GetOrCreateUserEngagement(ctx, user.ID, date)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, eng.ID, eng2.ID)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAnalyticsRepository_UpdateUserEngagement(t *testing.T) {
|
||||||
|
repo, db := newTestAnalyticsRepoWithSQLite(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
user := domain.User{Username: "testuser", Email: "test@test.com"}
|
||||||
|
require.NoError(t, db.Create(&user).Error)
|
||||||
|
date := time.Now().Truncate(24 * time.Hour)
|
||||||
|
engagement, err := repo.GetOrCreateUserEngagement(ctx, user.ID, date)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
engagement.LikesGiven = 15
|
||||||
|
engagement.WorksRead = 10
|
||||||
|
|
||||||
|
err = repo.UpdateUserEngagement(ctx, engagement)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var updatedEngagement domain.UserEngagement
|
||||||
|
db.First(&updatedEngagement, engagement.ID)
|
||||||
|
|
||||||
|
assert.Equal(t, 15, updatedEngagement.LikesGiven)
|
||||||
|
assert.Equal(t, 10, updatedEngagement.WorksRead)
|
||||||
|
}
|
||||||
91
internal/data/sql/auth_repository_test.go
Normal file
91
internal/data/sql/auth_repository_test.go
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
package sql_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"tercul/internal/data/sql"
|
||||||
|
"tercul/internal/domain"
|
||||||
|
"tercul/internal/platform/config"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// newTestAuthRepoWithSQLite sets up an in-memory SQLite database for auth repository testing.
|
||||||
|
func newTestAuthRepoWithSQLite(t *testing.T) (domain.AuthRepository, *gorm.DB) {
|
||||||
|
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Auto-migrate the necessary schemas
|
||||||
|
err = db.AutoMigrate(
|
||||||
|
&domain.User{},
|
||||||
|
&domain.UserSession{},
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cfg := &config.Config{}
|
||||||
|
repo := sql.NewAuthRepository(db, cfg)
|
||||||
|
|
||||||
|
// Clean up the database after the test
|
||||||
|
t.Cleanup(func() {
|
||||||
|
sqlDB, err := db.DB()
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = sqlDB.Close()
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
return repo, db
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthRepository_StoreToken(t *testing.T) {
|
||||||
|
repo, db := newTestAuthRepoWithSQLite(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Setup: Create a user
|
||||||
|
user := domain.User{Username: "authuser", Email: "auth@test.com"}
|
||||||
|
require.NoError(t, db.Create(&user).Error)
|
||||||
|
|
||||||
|
token := "my-secret-token"
|
||||||
|
expiresAt := time.Now().Add(1 * time.Hour)
|
||||||
|
|
||||||
|
err := repo.StoreToken(ctx, user.ID, token, expiresAt)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var session domain.UserSession
|
||||||
|
err = db.Where("token = ?", token).First(&session).Error
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, user.ID, session.UserID)
|
||||||
|
assert.Equal(t, token, session.Token)
|
||||||
|
// Truncate to a reasonable precision for comparison
|
||||||
|
assert.WithinDuration(t, expiresAt, session.ExpiresAt, time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthRepository_DeleteToken(t *testing.T) {
|
||||||
|
repo, db := newTestAuthRepoWithSQLite(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Setup: Create a user and a session
|
||||||
|
user := domain.User{Username: "authuser2", Email: "auth2@test.com"}
|
||||||
|
require.NoError(t, db.Create(&user).Error)
|
||||||
|
token := "token-to-delete"
|
||||||
|
session := &domain.UserSession{
|
||||||
|
UserID: user.ID,
|
||||||
|
Token: token,
|
||||||
|
ExpiresAt: time.Now().Add(1 * time.Hour),
|
||||||
|
}
|
||||||
|
require.NoError(t, db.Create(session).Error)
|
||||||
|
|
||||||
|
// Delete the token
|
||||||
|
err := repo.DeleteToken(ctx, token)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify it's gone
|
||||||
|
var deletedSession domain.UserSession
|
||||||
|
err = db.Where("token = ?", token).First(&deletedSession).Error
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Equal(t, gorm.ErrRecordNotFound, err)
|
||||||
|
}
|
||||||
122
internal/data/sql/copyright_claim_repository_test.go
Normal file
122
internal/data/sql/copyright_claim_repository_test.go
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
package sql_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"tercul/internal/data/sql"
|
||||||
|
"tercul/internal/domain"
|
||||||
|
"tercul/internal/platform/config"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// newTestCopyrightClaimRepoWithSQLite sets up an in-memory SQLite database for copyright claim repository testing.
|
||||||
|
func newTestCopyrightClaimRepoWithSQLite(t *testing.T) (domain.CopyrightClaimRepository, *gorm.DB) {
|
||||||
|
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Auto-migrate the necessary schemas
|
||||||
|
err = db.AutoMigrate(
|
||||||
|
&domain.User{},
|
||||||
|
&domain.Work{},
|
||||||
|
&domain.CopyrightClaim{},
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cfg := &config.Config{}
|
||||||
|
repo := sql.NewCopyrightClaimRepository(db, cfg)
|
||||||
|
|
||||||
|
// Clean up the database after the test
|
||||||
|
t.Cleanup(func() {
|
||||||
|
sqlDB, err := db.DB()
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = sqlDB.Close()
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
return repo, db
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCopyrightClaimRepository_ListByWorkID(t *testing.T) {
|
||||||
|
repo, db := newTestCopyrightClaimRepoWithSQLite(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Setup: Create a user and a work
|
||||||
|
user := domain.User{Username: "claimuser", Email: "claim@test.com"}
|
||||||
|
require.NoError(t, db.Create(&user).Error)
|
||||||
|
work1 := domain.Work{Title: "Work 1"}
|
||||||
|
require.NoError(t, db.Create(&work1).Error)
|
||||||
|
work2 := domain.Work{Title: "Work 2"}
|
||||||
|
require.NoError(t, db.Create(&work2).Error)
|
||||||
|
|
||||||
|
// Create some claims
|
||||||
|
claim1 := domain.CopyrightClaim{UserID: &user.ID, WorkID: &work1.ID, Details: "Claim 1", ClaimDate: time.Now()}
|
||||||
|
claim2 := domain.CopyrightClaim{UserID: &user.ID, WorkID: &work1.ID, Details: "Claim 2", ClaimDate: time.Now()}
|
||||||
|
claim3 := domain.CopyrightClaim{UserID: &user.ID, WorkID: &work2.ID, Details: "Claim 3", ClaimDate: time.Now()}
|
||||||
|
require.NoError(t, db.Create(&claim1).Error)
|
||||||
|
require.NoError(t, db.Create(&claim2).Error)
|
||||||
|
require.NoError(t, db.Create(&claim3).Error)
|
||||||
|
|
||||||
|
t.Run("finds_claims_for_work1", func(t *testing.T) {
|
||||||
|
claims, err := repo.ListByWorkID(ctx, work1.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, claims, 2)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("finds_claims_for_work2", func(t *testing.T) {
|
||||||
|
claims, err := repo.ListByWorkID(ctx, work2.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, claims, 1)
|
||||||
|
assert.Equal(t, "Claim 3", claims[0].Details)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("returns_empty_slice_for_no_claims", func(t *testing.T) {
|
||||||
|
claims, err := repo.ListByWorkID(ctx, 999) // Non-existent work
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Empty(t, claims)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCopyrightClaimRepository_ListByUserID(t *testing.T) {
|
||||||
|
repo, db := newTestCopyrightClaimRepoWithSQLite(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Setup: Create users and a work
|
||||||
|
user1 := domain.User{Username: "user1", Email: "user1@test.com"}
|
||||||
|
require.NoError(t, db.Create(&user1).Error)
|
||||||
|
user2 := domain.User{Username: "user2", Email: "user2@test.com"}
|
||||||
|
require.NoError(t, db.Create(&user2).Error)
|
||||||
|
work := domain.Work{Title: "Test Work"}
|
||||||
|
require.NoError(t, db.Create(&work).Error)
|
||||||
|
|
||||||
|
// Create some claims
|
||||||
|
claim1 := domain.CopyrightClaim{UserID: &user1.ID, WorkID: &work.ID, Details: "Claim 1", ClaimDate: time.Now()}
|
||||||
|
claim2 := domain.CopyrightClaim{UserID: &user2.ID, WorkID: &work.ID, Details: "Claim 2", ClaimDate: time.Now()}
|
||||||
|
claim3 := domain.CopyrightClaim{UserID: &user2.ID, WorkID: &work.ID, Details: "Claim 3", ClaimDate: time.Now()}
|
||||||
|
require.NoError(t, db.Create(&claim1).Error)
|
||||||
|
require.NoError(t, db.Create(&claim2).Error)
|
||||||
|
require.NoError(t, db.Create(&claim3).Error)
|
||||||
|
|
||||||
|
t.Run("finds_claims_for_user1", func(t *testing.T) {
|
||||||
|
claims, err := repo.ListByUserID(ctx, user1.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, claims, 1)
|
||||||
|
assert.Equal(t, "Claim 1", claims[0].Details)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("finds_claims_for_user2", func(t *testing.T) {
|
||||||
|
claims, err := repo.ListByUserID(ctx, user2.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, claims, 2)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("returns_empty_slice_for_no_claims", func(t *testing.T) {
|
||||||
|
claims, err := repo.ListByUserID(ctx, 999) // Non-existent user
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Empty(t, claims)
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -581,8 +581,10 @@ type CopyrightClaim struct {
|
|||||||
ClaimDate time.Time `gorm:"not null"`
|
ClaimDate time.Time `gorm:"not null"`
|
||||||
Resolution string `gorm:"type:text"`
|
Resolution string `gorm:"type:text"`
|
||||||
ResolvedAt *time.Time
|
ResolvedAt *time.Time
|
||||||
UserID *uint
|
UserID *uint
|
||||||
User *User `gorm:"foreignKey:UserID"`
|
User *User `gorm:"foreignKey:UserID"`
|
||||||
|
WorkID *uint
|
||||||
|
Work *Work `gorm:"foreignKey:WorkID"`
|
||||||
}
|
}
|
||||||
type MonetizationType string
|
type MonetizationType string
|
||||||
const (
|
const (
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user