initial commit
This commit is contained in:
51
internal/repository/module.go
Normal file
51
internal/repository/module.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"go.uber.org/fx"
|
||||
|
||||
"base/internal/repository/postgres/asset"
|
||||
"base/internal/repository/postgres/auth"
|
||||
"base/internal/repository/postgres/profile"
|
||||
"base/internal/repository/postgres/skill"
|
||||
)
|
||||
|
||||
var Auth = fx.Module(
|
||||
"auth",
|
||||
fx.Provide(
|
||||
auth.NewAccountRepository,
|
||||
auth.NewRoleRepository,
|
||||
auth.NewUserRepository,
|
||||
auth.NewUserRoleRepository,
|
||||
),
|
||||
)
|
||||
|
||||
var Profile = fx.Module(
|
||||
"profile",
|
||||
fx.Provide(
|
||||
profile.NewProfileRepository,
|
||||
profile.NewRoleRepository,
|
||||
),
|
||||
)
|
||||
|
||||
var Asset = fx.Module(
|
||||
"asset",
|
||||
fx.Provide(
|
||||
asset.NewAssetRepository,
|
||||
asset.NewCategoryRepository,
|
||||
),
|
||||
)
|
||||
|
||||
var Skill = fx.Module(
|
||||
"skill",
|
||||
fx.Provide(
|
||||
skill.NewRepository,
|
||||
),
|
||||
)
|
||||
|
||||
var Module = fx.Module(
|
||||
"repository",
|
||||
Auth,
|
||||
Profile,
|
||||
Asset,
|
||||
Skill,
|
||||
)
|
||||
60
internal/repository/postgres/asset/RELATIONS.md
Normal file
60
internal/repository/postgres/asset/RELATIONS.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# Asset relations: Report and Comment
|
||||
|
||||
## Overview
|
||||
|
||||
Reports and comments are **child relations** of an asset. They are stored in separate tables (`asset_reports`, `asset_comments`) and are always loaded/saved **through the asset** (no standalone Report or Comment repository in this layer).
|
||||
|
||||
---
|
||||
|
||||
## Comment relation
|
||||
|
||||
### Storage
|
||||
- **Table:** `asset_comments`
|
||||
- **Columns:** `id`, `asset_id`, `content`, `writer_id`, `writer_type`, `parent_id`, `created_at`, `updated_at`, `deleted_at`
|
||||
- **Parent link:** `asset_id` → `assets.id`
|
||||
|
||||
### What happens
|
||||
|
||||
| Operation | Behavior |
|
||||
|-----------|----------|
|
||||
| **Create asset** | If `asset.Comments` is non-empty, each comment is inserted with `asset_id = new_asset_id`. IDs/timestamps come from DB. |
|
||||
| **FindByID / FindByProfileID** | All rows in `asset_comments` with `asset_id = asset.id` are loaded and mapped to `asset.Comments` (flat list). |
|
||||
| **Update asset** | All existing comments for that asset are **deleted**, then `asset.Comments` is re-inserted (replace strategy). |
|
||||
| **Delete asset** | All comments for that asset are deleted in the same transaction before the asset row is deleted. |
|
||||
|
||||
### Domain vs persistence
|
||||
- **Domain:** `Comment` has `Replies []Comment` (nested).
|
||||
- **Persistence:** Stored as rows in `asset_comments` with `parent_id` for replies.
|
||||
- **When loading:** A flat list is read from DB, then `buildCommentTree()` turns it into a tree: top-level comments have `Replies` populated.
|
||||
- **When saving:** `flattenComments()` turns the tree into a flat list (parent then its replies); all rows are persisted. Note: when **creating** a new asset with new nested comments, reply `ParentID` in the domain may be zero (parent not yet saved); the current single-batch insert does not resolve parent IDs, so nested replies on create may end up with `parent_id = NULL`. For **updates** after load, parent IDs exist and nested replies persist correctly.
|
||||
|
||||
---
|
||||
|
||||
## Report relation
|
||||
|
||||
### Storage
|
||||
- **Table:** `asset_reports`
|
||||
- **Columns:** `id`, `asset_id`, `reported_by` (JSONB), `reported_at`, `reason` (JSONB), `status`, `notes`, `attachments` (JSONB), `created_at`, `updated_at`, `deleted_at`
|
||||
- **Parent link:** `asset_id` → `assets.id`
|
||||
- **Nested data:** `ReportedBy`, `ReportReason`, and `Attachments` are stored as JSON in the same row.
|
||||
|
||||
### What happens
|
||||
|
||||
| Operation | Behavior |
|
||||
|-----------|----------|
|
||||
| **Create asset** | If `asset.Reports` is non-empty, each report is inserted: `ReportedBy`, `Reason`, and `Attachments` are JSON-encoded into the report row. |
|
||||
| **FindByID / FindByProfileID** | All rows in `asset_reports` with `asset_id = asset.id` are loaded; JSONB columns are decoded into `Report.ReportedBy`, `Report.Reason`, `Report.Attachments`. |
|
||||
| **Update asset** | All existing reports for that asset are **deleted**, then `asset.Reports` is re-inserted (replace strategy). |
|
||||
| **Delete asset** | All reports for that asset are deleted in the same transaction before the asset row is deleted. |
|
||||
|
||||
### Domain vs persistence
|
||||
- **ReportedBy**, **ReportReason**, **Attachments** are fully round-tripped via JSON; no separate tables.
|
||||
- Report **ID** and **ReportedAt** are set when loading from DB; on create, IDs/timestamps come from DB.
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
- **Comment:** Stored and loaded as a **flat** list per asset; `parent_id` is persisted but **Replies** are not built when loading and nested replies are not written when saving.
|
||||
- **Report:** Stored and loaded as a list per asset; nested structures (ReportedBy, Reason, Attachments) are stored as JSONB and fully restored on load.
|
||||
- Both relations use a **replace-on-update** strategy: updating an asset deletes all its comments and reports and re-inserts from `asset.Comments` and `asset.Reports`.
|
||||
297
internal/repository/postgres/asset/asset.go
Normal file
297
internal/repository/postgres/asset/asset.go
Normal file
@@ -0,0 +1,297 @@
|
||||
package asset
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"go.uber.org/fx"
|
||||
"gorm.io/gorm"
|
||||
|
||||
domainAsset "base/internal/domain/asset"
|
||||
)
|
||||
|
||||
type assetRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewAssetRepository(lc fx.Lifecycle, db *gorm.DB) domainAsset.AssetRepository {
|
||||
lc.Append(
|
||||
fx.Hook{
|
||||
OnStart: func(ctx context.Context) error {
|
||||
return nil
|
||||
},
|
||||
OnStop: func(ctx context.Context) error {
|
||||
return nil
|
||||
},
|
||||
})
|
||||
return &assetRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *assetRepository) loadRelatedData(ctx context.Context, assetID, categoryID uuid.UUID) (*domainAsset.Category, []domainAsset.Artifact, []domainAsset.Comment, []domainAsset.Report, error) {
|
||||
var category *domainAsset.Category
|
||||
if categoryID != uuid.Nil {
|
||||
var catModel CategoryModel
|
||||
if err := r.db.WithContext(ctx).Where("id = ?", categoryID).First(&catModel).Error; err == nil {
|
||||
category = toCategoryDomain(&catModel)
|
||||
}
|
||||
}
|
||||
|
||||
var artifactModels []ArtifactModel
|
||||
if err := r.db.WithContext(ctx).Where("asset_id = ?", assetID).Find(&artifactModels).Error; err != nil {
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
artifacts := toArtifactDomains(artifactModels)
|
||||
|
||||
var commentModels []CommentModel
|
||||
if err := r.db.WithContext(ctx).Where("asset_id = ?", assetID).Find(&commentModels).Error; err != nil {
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
comments := toCommentDomains(commentModels)
|
||||
|
||||
var reportModels []ReportModel
|
||||
if err := r.db.WithContext(ctx).Where("asset_id = ?", assetID).Find(&reportModels).Error; err != nil {
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
reports, err := toReportDomains(reportModels)
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
return category, artifacts, comments, reports, nil
|
||||
}
|
||||
|
||||
func (r *assetRepository) Create(ctx context.Context, asset *domainAsset.Asset) error {
|
||||
model := toAssetModel(asset)
|
||||
|
||||
tx := r.db.WithContext(ctx).Begin()
|
||||
if tx.Error != nil {
|
||||
return tx.Error
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
if err := tx.Create(model).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(asset.AssetArtifacts) > 0 {
|
||||
artifactModels := toArtifactModels(model.ID, asset.AssetArtifacts)
|
||||
if err := tx.Create(&artifactModels).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(asset.Comments) > 0 {
|
||||
commentModels := toCommentModels(model.ID, asset.Comments)
|
||||
if err := tx.Create(&commentModels).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(asset.Reports) > 0 {
|
||||
reportModels, err := toReportModels(model.ID, asset.Reports)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Create(&reportModels).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
copyAssetFromModel(asset, model)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *assetRepository) FindByID(ctx context.Context, id uuid.UUID) (*domainAsset.Asset, error) {
|
||||
var model Model
|
||||
if err := r.db.WithContext(ctx).Where("id = ?", id).First(&model).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("asset not found")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
category, artifacts, comments, reports, err := r.loadRelatedData(ctx, model.ID, model.AssetCategoryID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return toAssetDomain(&model, category, artifacts, comments, reports), nil
|
||||
}
|
||||
|
||||
func (r *assetRepository) Update(ctx context.Context, asset *domainAsset.Asset) error {
|
||||
model := toAssetModel(asset)
|
||||
|
||||
tx := r.db.WithContext(ctx).Begin()
|
||||
if tx.Error != nil {
|
||||
return tx.Error
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
if err := tx.Model(&Model{}).Where("id = ?", asset.ID).Updates(model).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.Where("asset_id = ?", asset.ID).Delete(&ArtifactModel{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("asset_id = ?", asset.ID).Delete(&CommentModel{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("asset_id = ?", asset.ID).Delete(&ReportModel{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(asset.AssetArtifacts) > 0 {
|
||||
artifactModels := toArtifactModels(asset.ID, asset.AssetArtifacts)
|
||||
if err := tx.Create(&artifactModels).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(asset.Comments) > 0 {
|
||||
commentModels := toCommentModels(asset.ID, asset.Comments)
|
||||
if err := tx.Create(&commentModels).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(asset.Reports) > 0 {
|
||||
reportModels, err := toReportModels(asset.ID, asset.Reports)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Create(&reportModels).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Commit().Error
|
||||
}
|
||||
|
||||
func (r *assetRepository) Delete(ctx context.Context, asset *domainAsset.Asset) error {
|
||||
tx := r.db.WithContext(ctx).Begin()
|
||||
if tx.Error != nil {
|
||||
return tx.Error
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
if err := tx.Where("asset_id = ?", asset.ID).Delete(&ArtifactModel{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("asset_id = ?", asset.ID).Delete(&CommentModel{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("asset_id = ?", asset.ID).Delete(&ReportModel{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Delete(&Model{}, "id = ?", asset.ID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.Commit().Error
|
||||
}
|
||||
|
||||
func (r *assetRepository) FindLatestByCategory(ctx context.Context, categoryID uuid.UUID, limit int) ([]*domainAsset.Asset, error) {
|
||||
return r.FindLatestByCategoryPaginated(ctx, categoryID, limit, 0)
|
||||
}
|
||||
|
||||
func (r *assetRepository) FindLatestByCategoryPaginated(ctx context.Context, categoryID uuid.UUID, limit, offset int) ([]*domainAsset.Asset, error) {
|
||||
var models []Model
|
||||
q := r.db.WithContext(ctx).Where("asset_category_id = ?", categoryID).Order("created_at DESC")
|
||||
|
||||
if limit > 0 {
|
||||
q = q.Limit(limit)
|
||||
}
|
||||
|
||||
if offset > 0 {
|
||||
q = q.Offset(offset)
|
||||
}
|
||||
|
||||
if err := q.Find(&models).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(models) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
out := make([]*domainAsset.Asset, len(models))
|
||||
|
||||
for i, model := range models {
|
||||
category, artifacts, comments, reports, err := r.loadRelatedData(ctx, model.ID, model.AssetCategoryID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out[i] = toAssetDomain(&model, category, artifacts, comments, reports)
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (r *assetRepository) CountByCategory(ctx context.Context, categoryID uuid.UUID) (int, error) {
|
||||
var count int64
|
||||
if err := r.db.WithContext(ctx).Model(&Model{}).Where("asset_category_id = ?", categoryID).Count(&count).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return int(count), nil
|
||||
}
|
||||
|
||||
func (r *assetRepository) FindLatest(ctx context.Context, limit, offset int) ([]*domainAsset.Asset, error) {
|
||||
var models []Model
|
||||
q := r.db.WithContext(ctx).Order("created_at DESC")
|
||||
if limit > 0 {
|
||||
q = q.Limit(limit)
|
||||
}
|
||||
if offset > 0 {
|
||||
q = q.Offset(offset)
|
||||
}
|
||||
if err := q.Find(&models).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(models) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
out := make([]*domainAsset.Asset, len(models))
|
||||
for i, model := range models {
|
||||
category, artifacts, comments, reports, err := r.loadRelatedData(ctx, model.ID, model.AssetCategoryID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out[i] = toAssetDomain(&model, category, artifacts, comments, reports)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (r *assetRepository) FindByProfileID(ctx context.Context, profileID uuid.UUID) ([]*domainAsset.Asset, error) {
|
||||
var models []Model
|
||||
if err := r.db.WithContext(ctx).Where("profile_id = ?", profileID).Order("created_at DESC").Find(&models).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(models) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
out := make([]*domainAsset.Asset, len(models))
|
||||
for i, model := range models {
|
||||
category, artifacts, comments, reports, err := r.loadRelatedData(ctx, model.ID, model.AssetCategoryID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out[i] = toAssetDomain(&model, category, artifacts, comments, reports)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (r *assetRepository) Count(ctx context.Context) (int, error) {
|
||||
var count int64
|
||||
if err := r.db.WithContext(ctx).Model(&Model{}).Count(&count).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return int(count), nil
|
||||
}
|
||||
90
internal/repository/postgres/asset/category.go
Normal file
90
internal/repository/postgres/asset/category.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package asset
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"go.uber.org/fx"
|
||||
"gorm.io/gorm"
|
||||
|
||||
domainAsset "base/internal/domain/asset"
|
||||
)
|
||||
|
||||
type categoryRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewCategoryRepository(lc fx.Lifecycle, db *gorm.DB) domainAsset.CategoryRepository {
|
||||
lc.Append(
|
||||
fx.Hook{
|
||||
OnStart: func(ctx context.Context) error {
|
||||
return nil
|
||||
},
|
||||
OnStop: func(ctx context.Context) error {
|
||||
return nil
|
||||
},
|
||||
})
|
||||
return &categoryRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *categoryRepository) Create(ctx context.Context, category *domainAsset.Category) error {
|
||||
model := toCategoryModel(category)
|
||||
now := time.Now()
|
||||
model.CreatedAt = now
|
||||
model.UpdatedAt = now
|
||||
if err := r.db.WithContext(ctx).Create(model).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
category.ID = model.ID
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *categoryRepository) FindByID(ctx context.Context, id uuid.UUID) (*domainAsset.Category, error) {
|
||||
var model CategoryModel
|
||||
if err := r.db.WithContext(ctx).Where("id = ?", id).First(&model).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("category not found")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return toCategoryDomain(&model), nil
|
||||
}
|
||||
|
||||
func (r *categoryRepository) Update(ctx context.Context, category *domainAsset.Category) error {
|
||||
model := toCategoryModel(category)
|
||||
model.UpdatedAt = time.Now()
|
||||
return r.db.WithContext(ctx).Model(&CategoryModel{}).Where("id = ?", category.ID).Updates(model).Error
|
||||
}
|
||||
|
||||
func (r *categoryRepository) Delete(ctx context.Context, id uuid.UUID) error {
|
||||
return r.db.WithContext(ctx).Delete(&CategoryModel{}, "id = ?", id).Error
|
||||
}
|
||||
|
||||
func (r *categoryRepository) FindAll(ctx context.Context) ([]*domainAsset.Category, error) {
|
||||
var models []CategoryModel
|
||||
if err := r.db.WithContext(ctx).Order("name ASC").Find(&models).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]*domainAsset.Category, len(models))
|
||||
for i := range models {
|
||||
out[i] = toCategoryDomain(&models[i])
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (r *categoryRepository) FindByIDs(ctx context.Context, ids []uuid.UUID) ([]*domainAsset.Category, error) {
|
||||
if len(ids) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
var models []CategoryModel
|
||||
if err := r.db.WithContext(ctx).Where("id IN ?", ids).Order("name ASC").Find(&models).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]*domainAsset.Category, len(models))
|
||||
for i := range models {
|
||||
out[i] = toCategoryDomain(&models[i])
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
249
internal/repository/postgres/asset/mapper.go
Normal file
249
internal/repository/postgres/asset/mapper.go
Normal file
@@ -0,0 +1,249 @@
|
||||
package asset
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
domainAsset "base/internal/domain/asset"
|
||||
)
|
||||
|
||||
func toCategoryModel(category *domainAsset.Category) *CategoryModel {
|
||||
return &CategoryModel{
|
||||
ID: category.ID,
|
||||
Name: category.Name,
|
||||
Icon: category.Icon,
|
||||
Color: category.Color,
|
||||
CardType: category.CardType,
|
||||
Featured: category.Featured,
|
||||
Description: category.Description,
|
||||
}
|
||||
}
|
||||
|
||||
func toCategoryDomain(model *CategoryModel) *domainAsset.Category {
|
||||
return &domainAsset.Category{
|
||||
ID: model.ID,
|
||||
Name: model.Name,
|
||||
Icon: model.Icon,
|
||||
Color: model.Color,
|
||||
CardType: model.CardType,
|
||||
Featured: model.Featured,
|
||||
Description: model.Description,
|
||||
}
|
||||
}
|
||||
|
||||
func toAssetModel(asset *domainAsset.Asset) *Model {
|
||||
return &Model{
|
||||
ID: asset.ID,
|
||||
ProfileID: asset.ProfileID,
|
||||
Status: int(asset.Status),
|
||||
AssetCategoryID: asset.AssetCategoryID,
|
||||
Title: asset.Title,
|
||||
Description: asset.Description,
|
||||
Link: asset.Link,
|
||||
Analytics: asset.Analytics,
|
||||
CreatedAt: asset.CreatedAt,
|
||||
UpdatedAt: asset.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func toAssetDomain(model *Model, category *domainAsset.Category, artifacts []domainAsset.Artifact, comments []domainAsset.Comment, reports []domainAsset.Report) *domainAsset.Asset {
|
||||
cat := domainAsset.Category{}
|
||||
if category != nil {
|
||||
cat = *category
|
||||
}
|
||||
return &domainAsset.Asset{
|
||||
ID: model.ID,
|
||||
ProfileID: model.ProfileID,
|
||||
Status: domainAsset.Status(model.Status),
|
||||
AssetCategoryID: model.AssetCategoryID,
|
||||
AssetCategory: cat,
|
||||
Title: model.Title,
|
||||
Description: model.Description,
|
||||
Link: model.Link,
|
||||
Analytics: model.Analytics,
|
||||
Reports: reports,
|
||||
AssetArtifacts: artifacts,
|
||||
Comments: comments,
|
||||
CreatedAt: model.CreatedAt,
|
||||
UpdatedAt: model.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func copyAssetFromModel(asset *domainAsset.Asset, model *Model) {
|
||||
asset.ID = model.ID
|
||||
asset.CreatedAt = model.CreatedAt
|
||||
asset.UpdatedAt = model.UpdatedAt
|
||||
}
|
||||
|
||||
func toArtifactModels(assetID uuid.UUID, artifacts []domainAsset.Artifact) []ArtifactModel {
|
||||
models := make([]ArtifactModel, len(artifacts))
|
||||
for i, a := range artifacts {
|
||||
models[i] = ArtifactModel{
|
||||
AssetID: assetID,
|
||||
Type: a.Type,
|
||||
DownloadURL: a.DownloadURL,
|
||||
Price: a.Price,
|
||||
Title: a.Title,
|
||||
Description: a.Description,
|
||||
}
|
||||
}
|
||||
return models
|
||||
}
|
||||
|
||||
func toArtifactDomains(models []ArtifactModel) []domainAsset.Artifact {
|
||||
out := make([]domainAsset.Artifact, len(models))
|
||||
for i, m := range models {
|
||||
out[i] = domainAsset.Artifact{
|
||||
ID: m.ID,
|
||||
AssetID: m.AssetID,
|
||||
Type: m.Type,
|
||||
DownloadURL: m.DownloadURL,
|
||||
Price: m.Price,
|
||||
Title: m.Title,
|
||||
Description: m.Description,
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// flattenComments turns a tree of comments (with Replies) into a single slice:
|
||||
// top-level first, then each comment's replies recursively. Used when saving.
|
||||
func flattenComments(comments []domainAsset.Comment) []domainAsset.Comment {
|
||||
var out []domainAsset.Comment
|
||||
for _, c := range comments {
|
||||
out = append(out, c)
|
||||
out = append(out, flattenComments(c.Replies)...)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func toCommentModels(assetID uuid.UUID, comments []domainAsset.Comment) []CommentModel {
|
||||
flat := flattenComments(comments)
|
||||
models := make([]CommentModel, 0, len(flat))
|
||||
for _, c := range flat {
|
||||
models = append(models, CommentModel{
|
||||
AssetID: assetID,
|
||||
Content: c.Content,
|
||||
WriterID: c.WriterID,
|
||||
WriterType: c.WriterType,
|
||||
ParentID: c.ParentID,
|
||||
CreatedAt: c.CreatedAt,
|
||||
UpdatedAt: c.UpdatedAt,
|
||||
})
|
||||
}
|
||||
return models
|
||||
}
|
||||
|
||||
func toCommentDomains(models []CommentModel) []domainAsset.Comment {
|
||||
out := make([]domainAsset.Comment, len(models))
|
||||
for i, m := range models {
|
||||
out[i] = domainAsset.Comment{
|
||||
ID: m.ID,
|
||||
AssetID: m.AssetID,
|
||||
Content: m.Content,
|
||||
WriterID: m.WriterID,
|
||||
WriterType: m.WriterType,
|
||||
ParentID: m.ParentID,
|
||||
CreatedAt: m.CreatedAt,
|
||||
UpdatedAt: m.UpdatedAt,
|
||||
}
|
||||
}
|
||||
return buildCommentTree(out)
|
||||
}
|
||||
|
||||
// buildCommentTree turns a flat list of comments (with ParentID set) into a tree:
|
||||
// top-level comments have Replies populated; nested Replies are not further nested in this type.
|
||||
func buildCommentTree(flat []domainAsset.Comment) []domainAsset.Comment {
|
||||
if len(flat) == 0 {
|
||||
return nil
|
||||
}
|
||||
byID := make(map[uuid.UUID]*domainAsset.Comment)
|
||||
for i := range flat {
|
||||
flat[i].Replies = nil
|
||||
byID[flat[i].ID] = &flat[i]
|
||||
}
|
||||
// First pass: attach replies to parents
|
||||
for i := range flat {
|
||||
c := &flat[i]
|
||||
if c.ParentID == nil {
|
||||
continue
|
||||
}
|
||||
if parent, ok := byID[*c.ParentID]; ok {
|
||||
parent.Replies = append(parent.Replies, *c)
|
||||
}
|
||||
}
|
||||
// Second pass: collect top-level comments (with Replies already populated)
|
||||
var roots []domainAsset.Comment
|
||||
for i := range flat {
|
||||
c := &flat[i]
|
||||
if c.ParentID == nil {
|
||||
roots = append(roots, *c)
|
||||
}
|
||||
}
|
||||
return roots
|
||||
}
|
||||
|
||||
func toReportModels(assetID uuid.UUID, reports []domainAsset.Report) ([]ReportModel, error) {
|
||||
models := make([]ReportModel, len(reports))
|
||||
for i, r := range reports {
|
||||
reportedBy, err := json.Marshal(r.ReportedBy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reason, err := json.Marshal(r.Reason)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var attachments json.RawMessage
|
||||
if len(r.Attachments) > 0 {
|
||||
attachments, err = json.Marshal(r.Attachments)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
models[i] = ReportModel{
|
||||
AssetID: assetID,
|
||||
ReportedBy: reportedBy,
|
||||
ReportedAt: r.ReportedAt,
|
||||
Reason: reason,
|
||||
Status: int(r.Status),
|
||||
Notes: r.Notes,
|
||||
Attachments: attachments,
|
||||
CreatedAt: r.ReportedAt,
|
||||
UpdatedAt: r.ReportedAt,
|
||||
}
|
||||
}
|
||||
return models, nil
|
||||
}
|
||||
|
||||
func toReportDomains(models []ReportModel) ([]domainAsset.Report, error) {
|
||||
out := make([]domainAsset.Report, len(models))
|
||||
for i, m := range models {
|
||||
var reportedBy domainAsset.ReportedBy
|
||||
if err := json.Unmarshal(m.ReportedBy, &reportedBy); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var reason domainAsset.ReportReason
|
||||
if err := json.Unmarshal(m.Reason, &reason); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var attachments []domainAsset.Attachment
|
||||
if len(m.Attachments) > 0 {
|
||||
if err := json.Unmarshal(m.Attachments, &attachments); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
out[i] = domainAsset.Report{
|
||||
ID: m.ID,
|
||||
AssetID: m.AssetID,
|
||||
ReportedBy: reportedBy,
|
||||
ReportedAt: m.ReportedAt,
|
||||
Reason: reason,
|
||||
Status: domainAsset.ReportStatus(m.Status),
|
||||
Notes: m.Notes,
|
||||
Attachments: attachments,
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
95
internal/repository/postgres/asset/schema.go
Normal file
95
internal/repository/postgres/asset/schema.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package asset
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type CategoryModel struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()"`
|
||||
Name string `gorm:"column:name;type:text;not null"`
|
||||
Icon string `gorm:"column:icon;type:text"`
|
||||
Color string `gorm:"column:color;type:text"`
|
||||
CardType string `gorm:"column:card_type;type:text"`
|
||||
Featured bool `gorm:"column:featured;type:boolean;default:false"`
|
||||
Description string `gorm:"column:description;type:text"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;type:timestamptz;not null"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamptz;not null"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;type:timestamptz;index"`
|
||||
}
|
||||
|
||||
func (CategoryModel) TableName() string {
|
||||
return "asset_categories"
|
||||
}
|
||||
|
||||
type Model struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()"`
|
||||
ProfileID uuid.UUID `gorm:"column:profile_id;type:uuid;not null;index:assets_profile_id_idx"`
|
||||
Status int `gorm:"column:status;type:integer;not null;default:0"`
|
||||
AssetCategoryID uuid.UUID `gorm:"column:asset_category_id;type:uuid;not null;index:assets_category_id_idx"`
|
||||
Title string `gorm:"column:title;type:text;not null"`
|
||||
Description string `gorm:"column:description;type:text"`
|
||||
Link string `gorm:"column:link;type:text"`
|
||||
Analytics json.RawMessage `gorm:"column:analytics;type:jsonb"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;type:timestamptz;not null"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamptz;not null"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;type:timestamptz;index"`
|
||||
}
|
||||
|
||||
func (Model) TableName() string {
|
||||
return "assets"
|
||||
}
|
||||
|
||||
type ArtifactModel struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()"`
|
||||
AssetID uuid.UUID `gorm:"column:asset_id;type:uuid;not null;index:asset_artifacts_asset_id_idx"`
|
||||
Type string `gorm:"column:type;type:text;not null"`
|
||||
DownloadURL string `gorm:"column:download_url;type:text"`
|
||||
Price int `gorm:"column:price;type:integer;default:0"`
|
||||
Title string `gorm:"column:title;type:text"`
|
||||
Description string `gorm:"column:description;type:text"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;type:timestamptz;not null"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamptz;not null"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;type:timestamptz;index"`
|
||||
}
|
||||
|
||||
func (ArtifactModel) TableName() string {
|
||||
return "asset_artifacts"
|
||||
}
|
||||
|
||||
type CommentModel struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()"`
|
||||
AssetID uuid.UUID `gorm:"column:asset_id;type:uuid;not null;index:asset_comments_asset_id_idx"`
|
||||
Content string `gorm:"column:content;type:text;not null"`
|
||||
WriterID uuid.UUID `gorm:"column:writer_id;type:uuid;not null"`
|
||||
WriterType string `gorm:"column:writer_type;type:text;not null"`
|
||||
ParentID *uuid.UUID `gorm:"column:parent_id;type:uuid;index:asset_comments_parent_id_idx"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;type:timestamptz;not null"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamptz;not null"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;type:timestamptz;index"`
|
||||
}
|
||||
|
||||
func (CommentModel) TableName() string {
|
||||
return "asset_comments"
|
||||
}
|
||||
|
||||
type ReportModel struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()"`
|
||||
AssetID uuid.UUID `gorm:"column:asset_id;type:uuid;not null;index:asset_reports_asset_id_idx"`
|
||||
ReportedBy json.RawMessage `gorm:"column:reported_by;type:jsonb;not null"`
|
||||
ReportedAt time.Time `gorm:"column:reported_at;type:timestamptz;not null"`
|
||||
Reason json.RawMessage `gorm:"column:reason;type:jsonb;not null"`
|
||||
Status int `gorm:"column:status;type:integer;not null;default:0"`
|
||||
Notes string `gorm:"column:notes;type:text"`
|
||||
Attachments json.RawMessage `gorm:"column:attachments;type:jsonb"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;type:timestamptz;not null"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamptz;not null"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;type:timestamptz;index"`
|
||||
}
|
||||
|
||||
func (ReportModel) TableName() string {
|
||||
return "asset_reports"
|
||||
}
|
||||
88
internal/repository/postgres/auth/account.go
Normal file
88
internal/repository/postgres/auth/account.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"go.uber.org/fx"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
|
||||
domainAuth "base/internal/domain/auth"
|
||||
)
|
||||
|
||||
type accountRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewAccountRepository(lc fx.Lifecycle, db *gorm.DB) domainAuth.AccountRepository {
|
||||
lc.Append(
|
||||
fx.Hook{
|
||||
OnStart: func(ctx context.Context) error {
|
||||
return nil
|
||||
},
|
||||
OnStop: func(ctx context.Context) error {
|
||||
return nil
|
||||
},
|
||||
})
|
||||
return &accountRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *accountRepository) Create(ctx context.Context, account *domainAuth.Account) error {
|
||||
model := toAccountModel(account)
|
||||
if err := r.db.WithContext(ctx).Create(model).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
copyAccountFromModel(account, model)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *accountRepository) FindByID(ctx context.Context, id uuid.UUID) (*domainAuth.Account, error) {
|
||||
var model AccountModel
|
||||
if err := r.db.WithContext(ctx).Where("id = ?", id).First(&model).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return toAccountDomain(&model), nil
|
||||
}
|
||||
|
||||
func (r *accountRepository) FindByUserID(ctx context.Context, userID uuid.UUID) ([]*domainAuth.Account, error) {
|
||||
var models []AccountModel
|
||||
if err := r.db.WithContext(ctx).Where("user_id = ?", userID).Find(&models).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
accounts := make([]*domainAuth.Account, len(models))
|
||||
for i, model := range models {
|
||||
accounts[i] = toAccountDomain(&model)
|
||||
}
|
||||
return accounts, nil
|
||||
}
|
||||
|
||||
func (r *accountRepository) Update(ctx context.Context, account *domainAuth.Account) error {
|
||||
model := toAccountModel(account)
|
||||
return r.db.WithContext(ctx).Model(&AccountModel{}).Where("id = ?", account.ID).Updates(model).Error
|
||||
}
|
||||
|
||||
func (r *accountRepository) Delete(ctx context.Context, id uuid.UUID) error {
|
||||
return r.db.WithContext(ctx).Delete(&AccountModel{}, "id = ?", id).Error
|
||||
}
|
||||
|
||||
func (r *accountRepository) List(ctx context.Context, limit, offset int) ([]*domainAuth.Account, error) {
|
||||
var models []AccountModel
|
||||
if err := r.db.WithContext(ctx).Limit(limit).Offset(offset).Find(&models).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
accounts := make([]*domainAuth.Account, len(models))
|
||||
for i, model := range models {
|
||||
accounts[i] = toAccountDomain(&model)
|
||||
}
|
||||
return accounts, nil
|
||||
}
|
||||
|
||||
func (r *accountRepository) Count(ctx context.Context) (int64, error) {
|
||||
var count int64
|
||||
if err := r.db.WithContext(ctx).Model(&AccountModel{}).Count(&count).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
381
internal/repository/postgres/auth/account_test.go
Normal file
381
internal/repository/postgres/auth/account_test.go
Normal file
@@ -0,0 +1,381 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
domainAuth "base/internal/domain/auth"
|
||||
"base/internal/pkg/oauth"
|
||||
)
|
||||
|
||||
func TestAccountRepository_Create(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
repo := createTestAccountRepository(db)
|
||||
userRepo := createTestUserRepository(db)
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("create account successfully", func(t *testing.T) {
|
||||
// Create user first
|
||||
user := &domainAuth.User{
|
||||
ID: uuid.New(),
|
||||
FirstName: "Account",
|
||||
LastName: "User",
|
||||
Email: "account@example.com",
|
||||
Status: domainAuth.UserStatusActive,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
err := userRepo.Create(ctx, user)
|
||||
require.NoError(t, err)
|
||||
|
||||
account := &domainAuth.Account{
|
||||
ID: uuid.New(),
|
||||
UserID: user.ID,
|
||||
Provider: oauth.Google,
|
||||
Password: nil,
|
||||
Scope: []string{"read", "write"},
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
err = repo.Create(ctx, account)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEqual(t, uuid.Nil, account.ID)
|
||||
|
||||
// Verify account was created
|
||||
found, err := repo.FindByID(ctx, account.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, account.UserID, found.UserID)
|
||||
assert.Equal(t, account.Provider, found.Provider)
|
||||
assert.Equal(t, account.Scope, found.Scope)
|
||||
})
|
||||
|
||||
t.Run("create account with password", func(t *testing.T) {
|
||||
user := &domainAuth.User{
|
||||
ID: uuid.New(),
|
||||
FirstName: "Password",
|
||||
LastName: "User",
|
||||
Email: "password@example.com",
|
||||
Status: domainAuth.UserStatusActive,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
err := userRepo.Create(ctx, user)
|
||||
require.NoError(t, err)
|
||||
|
||||
password := "hashedpassword"
|
||||
account := &domainAuth.Account{
|
||||
ID: uuid.New(),
|
||||
UserID: user.ID,
|
||||
Provider: oauth.Credentials,
|
||||
Password: &password,
|
||||
Scope: []string{},
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
err = repo.Create(ctx, account)
|
||||
assert.NoError(t, err)
|
||||
|
||||
found, err := repo.FindByID(ctx, account.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, found.Password)
|
||||
assert.Equal(t, password, *found.Password)
|
||||
})
|
||||
|
||||
t.Run("create account with meta", func(t *testing.T) {
|
||||
user := &domainAuth.User{
|
||||
ID: uuid.New(),
|
||||
FirstName: "Meta",
|
||||
LastName: "User",
|
||||
Email: "meta@example.com",
|
||||
Status: domainAuth.UserStatusActive,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
err := userRepo.Create(ctx, user)
|
||||
require.NoError(t, err)
|
||||
|
||||
metaJSON := json.RawMessage(`{"key": "value", "number": 123}`)
|
||||
account := &domainAuth.Account{
|
||||
ID: uuid.New(),
|
||||
UserID: user.ID,
|
||||
Provider: oauth.Google,
|
||||
Meta: metaJSON,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
err = repo.Create(ctx, account)
|
||||
assert.NoError(t, err)
|
||||
|
||||
found, err := repo.FindByID(ctx, account.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, found.Meta)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAccountRepository_FindByID(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
repo := createTestAccountRepository(db)
|
||||
userRepo := createTestUserRepository(db)
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("find existing account by id", func(t *testing.T) {
|
||||
user := &domainAuth.User{
|
||||
ID: uuid.New(),
|
||||
FirstName: "Find",
|
||||
LastName: "User",
|
||||
Email: "find@example.com",
|
||||
Status: domainAuth.UserStatusActive,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
err := userRepo.Create(ctx, user)
|
||||
require.NoError(t, err)
|
||||
|
||||
account := &domainAuth.Account{
|
||||
ID: uuid.New(),
|
||||
UserID: user.ID,
|
||||
Provider: oauth.Google,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
err = repo.Create(ctx, account)
|
||||
require.NoError(t, err)
|
||||
|
||||
found, err := repo.FindByID(ctx, account.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, account.ID, found.ID)
|
||||
assert.Equal(t, account.UserID, found.UserID)
|
||||
})
|
||||
|
||||
t.Run("find non-existent account", func(t *testing.T) {
|
||||
nonExistentID := uuid.New()
|
||||
found, err := repo.FindByID(ctx, nonExistentID)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, found)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAccountRepository_FindByUserID(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
repo := createTestAccountRepository(db)
|
||||
userRepo := createTestUserRepository(db)
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("find accounts by user id", func(t *testing.T) {
|
||||
user := &domainAuth.User{
|
||||
ID: uuid.New(),
|
||||
FirstName: "Multi",
|
||||
LastName: "Account",
|
||||
Email: "multi@example.com",
|
||||
Status: domainAuth.UserStatusActive,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
err := userRepo.Create(ctx, user)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create multiple accounts
|
||||
account1 := &domainAuth.Account{
|
||||
ID: uuid.New(),
|
||||
UserID: user.ID,
|
||||
Provider: oauth.Google,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
err = repo.Create(ctx, account1)
|
||||
require.NoError(t, err)
|
||||
|
||||
account2 := &domainAuth.Account{
|
||||
ID: uuid.New(),
|
||||
UserID: user.ID,
|
||||
Provider: oauth.GitHub,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
err = repo.Create(ctx, account2)
|
||||
require.NoError(t, err)
|
||||
|
||||
accounts, err := repo.FindByUserID(ctx, user.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, accounts, 2)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAccountRepository_Update(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
repo := createTestAccountRepository(db)
|
||||
userRepo := createTestUserRepository(db)
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("update account successfully", func(t *testing.T) {
|
||||
user := &domainAuth.User{
|
||||
ID: uuid.New(),
|
||||
FirstName: "Update",
|
||||
LastName: "User",
|
||||
Email: "update@example.com",
|
||||
Status: domainAuth.UserStatusActive,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
err := userRepo.Create(ctx, user)
|
||||
require.NoError(t, err)
|
||||
|
||||
account := &domainAuth.Account{
|
||||
ID: uuid.New(),
|
||||
UserID: user.ID,
|
||||
Provider: oauth.Google,
|
||||
Scope: []string{"read"},
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
err = repo.Create(ctx, account)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Update account
|
||||
account.Scope = []string{"read", "write", "admin"}
|
||||
newToken := "newtoken"
|
||||
account.AccessToken = &newToken
|
||||
|
||||
err = repo.Update(ctx, account)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify update
|
||||
found, err := repo.FindByID(ctx, account.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []string{"read", "write", "admin"}, found.Scope)
|
||||
assert.NotNil(t, found.AccessToken)
|
||||
assert.Equal(t, newToken, *found.AccessToken)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAccountRepository_Delete(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
repo := createTestAccountRepository(db)
|
||||
userRepo := createTestUserRepository(db)
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("delete account successfully", func(t *testing.T) {
|
||||
user := &domainAuth.User{
|
||||
ID: uuid.New(),
|
||||
FirstName: "Delete",
|
||||
LastName: "User",
|
||||
Email: "delete@example.com",
|
||||
Status: domainAuth.UserStatusActive,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
err := userRepo.Create(ctx, user)
|
||||
require.NoError(t, err)
|
||||
|
||||
account := &domainAuth.Account{
|
||||
ID: uuid.New(),
|
||||
UserID: user.ID,
|
||||
Provider: oauth.Google,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
err = repo.Create(ctx, account)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = repo.Delete(ctx, account.ID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify deletion
|
||||
found, err := repo.FindByID(ctx, account.ID)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, found)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAccountRepository_List(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
repo := createTestAccountRepository(db)
|
||||
userRepo := createTestUserRepository(db)
|
||||
ctx := context.Background()
|
||||
|
||||
// Create user and multiple accounts
|
||||
user := &domainAuth.User{
|
||||
ID: uuid.New(),
|
||||
FirstName: "List",
|
||||
LastName: "User",
|
||||
Email: "list@example.com",
|
||||
Status: domainAuth.UserStatusActive,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
err := userRepo.Create(ctx, user)
|
||||
require.NoError(t, err)
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
account := &domainAuth.Account{
|
||||
ID: uuid.New(),
|
||||
UserID: user.ID,
|
||||
Provider: oauth.Provider(i % 4), // Cycle through providers
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
err := repo.Create(ctx, account)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
t.Run("list accounts with limit and offset", func(t *testing.T) {
|
||||
accounts, err := repo.List(ctx, 3, 0)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, accounts, 3)
|
||||
|
||||
accounts, err = repo.List(ctx, 3, 3)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, accounts, 2) // Remaining 2 accounts
|
||||
})
|
||||
}
|
||||
|
||||
func TestAccountRepository_Count(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
repo := createTestAccountRepository(db)
|
||||
userRepo := createTestUserRepository(db)
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("count accounts", func(t *testing.T) {
|
||||
initialCount, err := repo.Count(ctx)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(0), initialCount)
|
||||
|
||||
user := &domainAuth.User{
|
||||
ID: uuid.New(),
|
||||
FirstName: "Count",
|
||||
LastName: "User",
|
||||
Email: "count@example.com",
|
||||
Status: domainAuth.UserStatusActive,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
err = userRepo.Create(ctx, user)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create accounts
|
||||
for i := 0; i < 3; i++ {
|
||||
account := &domainAuth.Account{
|
||||
ID: uuid.New(),
|
||||
UserID: user.ID,
|
||||
Provider: oauth.Google,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
err := repo.Create(ctx, account)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
count, err := repo.Count(ctx)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(3), count)
|
||||
})
|
||||
}
|
||||
184
internal/repository/postgres/auth/mapper.go
Normal file
184
internal/repository/postgres/auth/mapper.go
Normal file
@@ -0,0 +1,184 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
domainAuth "base/internal/domain/auth"
|
||||
"base/internal/pkg/oauth"
|
||||
)
|
||||
|
||||
func toUserModel(user *domainAuth.User) *UserModel {
|
||||
// Note: DisplayName exists in schema but not in domain model
|
||||
// Compute it from FirstName + LastName
|
||||
displayName := user.FirstName + " " + user.LastName
|
||||
return &UserModel{
|
||||
ID: user.ID,
|
||||
FirstName: user.FirstName,
|
||||
LastName: user.LastName,
|
||||
DisplayName: displayName,
|
||||
PhoneNumber: user.PhoneNumber,
|
||||
Email: user.Email,
|
||||
EmailVerified: user.EmailVerified,
|
||||
Status: int(user.Status),
|
||||
InvitationCode: user.InvitationCode,
|
||||
CreatedAt: user.CreatedAt,
|
||||
UpdatedAt: user.UpdatedAt,
|
||||
DeletedAt: gorm.DeletedAt{Time: user.DeletedAt, Valid: !user.DeletedAt.IsZero()},
|
||||
}
|
||||
}
|
||||
|
||||
func toUserDomain(model *UserModel) *domainAuth.User {
|
||||
var deletedAt time.Time
|
||||
if model.DeletedAt.Valid {
|
||||
deletedAt = model.DeletedAt.Time
|
||||
}
|
||||
return &domainAuth.User{
|
||||
ID: model.ID,
|
||||
FirstName: model.FirstName,
|
||||
LastName: model.LastName,
|
||||
PhoneNumber: model.PhoneNumber,
|
||||
Email: model.Email,
|
||||
EmailVerified: model.EmailVerified,
|
||||
Status: domainAuth.UserStatus(model.Status),
|
||||
InvitationCode: model.InvitationCode,
|
||||
CreatedAt: model.CreatedAt,
|
||||
UpdatedAt: model.UpdatedAt,
|
||||
DeletedAt: deletedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func copyUserFromModel(user *domainAuth.User, model *UserModel) {
|
||||
user.ID = model.ID
|
||||
user.PhoneNumber = model.PhoneNumber
|
||||
user.EmailVerified = model.EmailVerified
|
||||
user.Status = domainAuth.UserStatus(model.Status)
|
||||
user.InvitationCode = model.InvitationCode
|
||||
user.CreatedAt = model.CreatedAt
|
||||
user.UpdatedAt = model.UpdatedAt
|
||||
if model.DeletedAt.Valid {
|
||||
user.DeletedAt = model.DeletedAt.Time
|
||||
}
|
||||
}
|
||||
|
||||
func toRoleModel(role *domainAuth.Role) *RoleModel {
|
||||
desc := &role.Description
|
||||
if role.Description == "" {
|
||||
desc = nil
|
||||
}
|
||||
return &RoleModel{
|
||||
ID: role.ID,
|
||||
Name: role.Name,
|
||||
Description: desc,
|
||||
CreatedAt: role.CreatedAt,
|
||||
UpdatedAt: role.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func toRoleDomain(model *RoleModel) *domainAuth.Role {
|
||||
desc := ""
|
||||
if model.Description != nil {
|
||||
desc = *model.Description
|
||||
}
|
||||
return &domainAuth.Role{
|
||||
ID: model.ID,
|
||||
Name: model.Name,
|
||||
Description: desc,
|
||||
CreatedAt: model.CreatedAt,
|
||||
UpdatedAt: model.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func copyRoleFromModel(role *domainAuth.Role, model *RoleModel) error {
|
||||
role.ID = model.ID
|
||||
role.CreatedAt = model.CreatedAt
|
||||
role.UpdatedAt = model.UpdatedAt
|
||||
return nil
|
||||
}
|
||||
|
||||
func toAccountModel(account *domainAuth.Account) *AccountModel {
|
||||
var scopeStr *string
|
||||
if len(account.Scope) > 0 {
|
||||
scopeBytes, _ := json.Marshal(account.Scope)
|
||||
s := string(scopeBytes)
|
||||
scopeStr = &s
|
||||
}
|
||||
|
||||
// Store provider in Meta JSONB field
|
||||
metaMap := make(map[string]interface{})
|
||||
if len(account.Meta) > 0 {
|
||||
_ = json.Unmarshal(account.Meta, &metaMap)
|
||||
}
|
||||
metaMap["provider"] = int(account.Provider)
|
||||
metaBytes, _ := json.Marshal(metaMap)
|
||||
meta := json.RawMessage(metaBytes)
|
||||
|
||||
return &AccountModel{
|
||||
ID: account.ID,
|
||||
UserID: account.UserID,
|
||||
Provider: int(account.Provider), // Store provider as column for querying
|
||||
Password: account.Password,
|
||||
AccessToken: account.AccessToken,
|
||||
RefreshToken: account.RefreshToken,
|
||||
Scope: scopeStr,
|
||||
Meta: &meta,
|
||||
CreatedAt: account.CreatedAt,
|
||||
UpdatedAt: account.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func toAccountDomain(model *AccountModel) *domainAuth.Account {
|
||||
var scope []string
|
||||
if model.Scope != nil {
|
||||
_ = json.Unmarshal([]byte(*model.Scope), &scope)
|
||||
}
|
||||
|
||||
var meta json.RawMessage
|
||||
var provider int
|
||||
|
||||
// Use Provider field if available (for querying), otherwise extract from Meta
|
||||
if model.Provider > 0 {
|
||||
provider = model.Provider
|
||||
}
|
||||
|
||||
if model.Meta != nil {
|
||||
meta = *model.Meta
|
||||
// If provider not set from field, try to extract from Meta
|
||||
if provider == 0 {
|
||||
var metaMap map[string]interface{}
|
||||
if err := json.Unmarshal(meta, &metaMap); err == nil {
|
||||
if p, ok := metaMap["provider"].(float64); ok {
|
||||
provider = int(p)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Import oauth package for Provider type
|
||||
// Provider is stored as int, convert to oauth.Provider
|
||||
var accountProvider oauth.Provider
|
||||
if provider > 0 {
|
||||
accountProvider = oauth.Provider(provider)
|
||||
}
|
||||
|
||||
return &domainAuth.Account{
|
||||
ID: model.ID,
|
||||
UserID: model.UserID,
|
||||
Provider: accountProvider,
|
||||
Password: model.Password,
|
||||
AccessToken: model.AccessToken,
|
||||
RefreshToken: model.RefreshToken,
|
||||
Scope: scope,
|
||||
Meta: meta,
|
||||
CreatedAt: model.CreatedAt,
|
||||
UpdatedAt: model.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func copyAccountFromModel(account *domainAuth.Account, model *AccountModel) {
|
||||
account.ID = model.ID
|
||||
account.CreatedAt = model.CreatedAt
|
||||
account.UpdatedAt = model.UpdatedAt
|
||||
}
|
||||
81
internal/repository/postgres/auth/role.go
Normal file
81
internal/repository/postgres/auth/role.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"go.uber.org/fx"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
|
||||
domainAuth "base/internal/domain/auth"
|
||||
)
|
||||
|
||||
type roleRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewRoleRepository(lc fx.Lifecycle, db *gorm.DB) domainAuth.RoleRepository {
|
||||
lc.Append(
|
||||
fx.Hook{
|
||||
OnStart: func(ctx context.Context) error {
|
||||
return db.AutoMigrate(&domainAuth.Role{})
|
||||
},
|
||||
OnStop: func(ctx context.Context) error {
|
||||
return nil
|
||||
},
|
||||
})
|
||||
return &roleRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *roleRepository) Create(ctx context.Context, role *domainAuth.Role) error {
|
||||
model := toRoleModel(role)
|
||||
if err := r.db.WithContext(ctx).Create(model).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return copyRoleFromModel(role, model)
|
||||
}
|
||||
|
||||
func (r *roleRepository) FindByID(ctx context.Context, id uuid.UUID) (*domainAuth.Role, error) {
|
||||
var model RoleModel
|
||||
if err := r.db.WithContext(ctx).Where("id = ?", id).First(&model).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return toRoleDomain(&model), nil
|
||||
}
|
||||
|
||||
func (r *roleRepository) FindByName(ctx context.Context, name string) (*domainAuth.Role, error) {
|
||||
var model RoleModel
|
||||
if err := r.db.WithContext(ctx).Where("name = ?", name).First(&model).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return toRoleDomain(&model), nil
|
||||
}
|
||||
|
||||
func (r *roleRepository) Update(ctx context.Context, role *domainAuth.Role) error {
|
||||
model := toRoleModel(role)
|
||||
return r.db.WithContext(ctx).Model(&RoleModel{}).Where("id = ?", role.ID).Updates(model).Error
|
||||
}
|
||||
|
||||
func (r *roleRepository) Delete(ctx context.Context, id uuid.UUID) error {
|
||||
return r.db.WithContext(ctx).Delete(&RoleModel{}, "id = ?", id).Error
|
||||
}
|
||||
|
||||
func (r *roleRepository) List(ctx context.Context, limit, offset int) ([]*domainAuth.Role, error) {
|
||||
var models []RoleModel
|
||||
if err := r.db.WithContext(ctx).Limit(limit).Offset(offset).Find(&models).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
roles := make([]*domainAuth.Role, len(models))
|
||||
for i, model := range models {
|
||||
roles[i] = toRoleDomain(&model)
|
||||
}
|
||||
return roles, nil
|
||||
}
|
||||
|
||||
func (r *roleRepository) Count(ctx context.Context) (int64, error) {
|
||||
var count int64
|
||||
if err := r.db.WithContext(ctx).Model(&RoleModel{}).Count(&count).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
235
internal/repository/postgres/auth/role_test.go
Normal file
235
internal/repository/postgres/auth/role_test.go
Normal file
@@ -0,0 +1,235 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
domainAuth "base/internal/domain/auth"
|
||||
)
|
||||
|
||||
func TestRoleRepository_Create(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
repo := createTestRoleRepository(db)
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("create role successfully", func(t *testing.T) {
|
||||
role := &domainAuth.Role{
|
||||
ID: uuid.New(),
|
||||
Name: "admin",
|
||||
Description: "Administrator role",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
err := repo.Create(ctx, role)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEqual(t, uuid.Nil, role.ID)
|
||||
|
||||
// Verify role was created
|
||||
found, err := repo.FindByID(ctx, role.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, role.Name, found.Name)
|
||||
assert.Equal(t, role.Description, found.Description)
|
||||
})
|
||||
|
||||
t.Run("create role with duplicate name fails", func(t *testing.T) {
|
||||
name := "duplicate"
|
||||
role1 := &domainAuth.Role{
|
||||
ID: uuid.New(),
|
||||
Name: name,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
err := repo.Create(ctx, role1)
|
||||
assert.NoError(t, err)
|
||||
|
||||
role2 := &domainAuth.Role{
|
||||
ID: uuid.New(),
|
||||
Name: name,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
err = repo.Create(ctx, role2)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRoleRepository_FindByID(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
repo := createTestRoleRepository(db)
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("find existing role by id", func(t *testing.T) {
|
||||
role := &domainAuth.Role{
|
||||
ID: uuid.New(),
|
||||
Name: "find",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
err := repo.Create(ctx, role)
|
||||
require.NoError(t, err)
|
||||
|
||||
found, err := repo.FindByID(ctx, role.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, role.ID, found.ID)
|
||||
assert.Equal(t, role.Name, found.Name)
|
||||
})
|
||||
|
||||
t.Run("find non-existent role", func(t *testing.T) {
|
||||
nonExistentID := uuid.New()
|
||||
found, err := repo.FindByID(ctx, nonExistentID)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, found)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRoleRepository_FindByName(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
repo := createTestRoleRepository(db)
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("find existing role by name", func(t *testing.T) {
|
||||
name := "findbyname"
|
||||
role := &domainAuth.Role{
|
||||
ID: uuid.New(),
|
||||
Name: name,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
err := repo.Create(ctx, role)
|
||||
require.NoError(t, err)
|
||||
|
||||
found, err := repo.FindByName(ctx, name)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, role.ID, found.ID)
|
||||
assert.Equal(t, name, found.Name)
|
||||
})
|
||||
|
||||
t.Run("find non-existent role by name", func(t *testing.T) {
|
||||
found, err := repo.FindByName(ctx, "nonexistent")
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, found)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRoleRepository_Update(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
repo := createTestRoleRepository(db)
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("update role successfully", func(t *testing.T) {
|
||||
role := &domainAuth.Role{
|
||||
ID: uuid.New(),
|
||||
Name: "update",
|
||||
Description: "Original description",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
err := repo.Create(ctx, role)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Update role
|
||||
role.Description = "Updated description"
|
||||
|
||||
err = repo.Update(ctx, role)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify update
|
||||
found, err := repo.FindByID(ctx, role.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Updated description", found.Description)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRoleRepository_Delete(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
repo := createTestRoleRepository(db)
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("delete role successfully", func(t *testing.T) {
|
||||
role := &domainAuth.Role{
|
||||
ID: uuid.New(),
|
||||
Name: "delete",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
err := repo.Create(ctx, role)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = repo.Delete(ctx, role.ID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify deletion (soft delete)
|
||||
found, err := repo.FindByID(ctx, role.ID)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, found)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRoleRepository_List(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
repo := createTestRoleRepository(db)
|
||||
ctx := context.Background()
|
||||
|
||||
// Create multiple roles
|
||||
for i := 0; i < 5; i++ {
|
||||
role := &domainAuth.Role{
|
||||
ID: uuid.New(),
|
||||
Name: "role" + strconv.Itoa(i),
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
err := repo.Create(ctx, role)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
t.Run("list roles with limit and offset", func(t *testing.T) {
|
||||
roles, err := repo.List(ctx, 3, 0)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, roles, 3)
|
||||
|
||||
roles, err = repo.List(ctx, 3, 3)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, roles, 2) // Remaining 2 roles
|
||||
})
|
||||
}
|
||||
|
||||
func TestRoleRepository_Count(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
repo := createTestRoleRepository(db)
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("count roles", func(t *testing.T) {
|
||||
initialCount, err := repo.Count(ctx)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(0), initialCount)
|
||||
|
||||
// Create roles
|
||||
for i := 0; i < 3; i++ {
|
||||
role := &domainAuth.Role{
|
||||
ID: uuid.New(),
|
||||
Name: "count" + strconv.Itoa(i),
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
err := repo.Create(ctx, role)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
count, err := repo.Count(ctx)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(3), count)
|
||||
})
|
||||
}
|
||||
70
internal/repository/postgres/auth/schema.go
Normal file
70
internal/repository/postgres/auth/schema.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type UserModel struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()"`
|
||||
FirstName string `gorm:"column:first_name;type:text;not null"`
|
||||
LastName string `gorm:"column:last_name;type:text;not null"`
|
||||
DisplayName string `gorm:"column:display_name;type:text;not null"`
|
||||
PhoneNumber string `gorm:"column:phone_number;type:text"`
|
||||
Email string `gorm:"column:email;type:text;not null;uniqueIndex:users_email_unique"`
|
||||
EmailVerified bool `gorm:"column:email_verified;type:boolean;default:false;not null"`
|
||||
Status int `gorm:"column:status;type:integer;default:0;not null"`
|
||||
InvitationCode string `gorm:"column:invitation_code;type:text"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;type:timestamptz;not null"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamptz;not null"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;type:timestamptz;index"`
|
||||
}
|
||||
|
||||
func (UserModel) TableName() string {
|
||||
return "users"
|
||||
}
|
||||
|
||||
type RoleModel struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()"`
|
||||
Name string `gorm:"column:name;type:text;not null;uniqueIndex:roles_name_unique"`
|
||||
Description *string `gorm:"column:description;type:text"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;type:timestamptz;not null"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamptz;not null"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;type:timestamptz;index"`
|
||||
}
|
||||
|
||||
func (RoleModel) TableName() string {
|
||||
return "roles"
|
||||
}
|
||||
|
||||
type AccountModel struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()"`
|
||||
UserID uuid.UUID `gorm:"column:user_id;type:uuid;not null;index:accounts_user_id_idx"`
|
||||
Provider int `gorm:"column:provider;type:integer;index:accounts_provider_idx"` // For querying, also stored in meta
|
||||
Password *string `gorm:"column:password;type:text"`
|
||||
AccessToken *string `gorm:"column:access_token;type:text"`
|
||||
RefreshToken *string `gorm:"column:refresh_token;type:text"`
|
||||
Scope *string `gorm:"column:scope;type:text"`
|
||||
Meta *json.RawMessage `gorm:"column:meta;type:jsonb"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;type:timestamptz;not null"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamptz;not null"`
|
||||
}
|
||||
|
||||
func (AccountModel) TableName() string {
|
||||
return "accounts"
|
||||
}
|
||||
|
||||
type UserRoleModel struct {
|
||||
UserID uuid.UUID `gorm:"column:user_id;type:uuid;not null;index:user_roles_user_id_idx"`
|
||||
RoleID uuid.UUID `gorm:"column:role_id;type:uuid;not null;index:user_roles_role_id_idx"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;type:timestamptz;not null"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamptz;not null"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;type:timestamptz;index"`
|
||||
}
|
||||
|
||||
func (UserRoleModel) TableName() string {
|
||||
return "user_roles"
|
||||
}
|
||||
108
internal/repository/postgres/auth/test_helper.go
Normal file
108
internal/repository/postgres/auth/test_helper.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
|
||||
domainAuth "base/internal/domain/auth"
|
||||
)
|
||||
|
||||
// setupTestDB creates an in-memory SQLite database for testing
|
||||
func setupTestDB(t *testing.T) *gorm.DB {
|
||||
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
|
||||
DisableForeignKeyConstraintWhenMigrating: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create tables manually with SQLite-compatible syntax
|
||||
// This avoids PostgreSQL-specific syntax like gen_random_uuid() and timestamptz
|
||||
|
||||
createUsersTable := `
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id TEXT PRIMARY KEY,
|
||||
first_name TEXT NOT NULL,
|
||||
last_name TEXT NOT NULL,
|
||||
display_name TEXT NOT NULL,
|
||||
phone_number TEXT,
|
||||
email TEXT NOT NULL,
|
||||
email_verified INTEGER NOT NULL DEFAULT 0,
|
||||
status INTEGER NOT NULL DEFAULT 0,
|
||||
invitation_code TEXT,
|
||||
created_at DATETIME NOT NULL,
|
||||
updated_at DATETIME NOT NULL,
|
||||
deleted_at DATETIME,
|
||||
UNIQUE(email)
|
||||
)
|
||||
`
|
||||
require.NoError(t, db.Exec(createUsersTable).Error)
|
||||
|
||||
createRolesTable := `
|
||||
CREATE TABLE IF NOT EXISTS roles (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
created_at DATETIME NOT NULL,
|
||||
updated_at DATETIME NOT NULL,
|
||||
deleted_at DATETIME,
|
||||
UNIQUE(name)
|
||||
)
|
||||
`
|
||||
require.NoError(t, db.Exec(createRolesTable).Error)
|
||||
|
||||
createAccountsTable := `
|
||||
CREATE TABLE IF NOT EXISTS accounts (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
provider INTEGER,
|
||||
password TEXT,
|
||||
access_token TEXT,
|
||||
refresh_token TEXT,
|
||||
scope TEXT,
|
||||
meta TEXT,
|
||||
created_at DATETIME NOT NULL,
|
||||
updated_at DATETIME NOT NULL
|
||||
)
|
||||
`
|
||||
require.NoError(t, db.Exec(createAccountsTable).Error)
|
||||
require.NoError(t, db.Exec("CREATE INDEX IF NOT EXISTS accounts_user_id_idx ON accounts(user_id)").Error)
|
||||
require.NoError(t, db.Exec("CREATE INDEX IF NOT EXISTS accounts_provider_idx ON accounts(provider)").Error)
|
||||
|
||||
createUserRolesTable := `
|
||||
CREATE TABLE IF NOT EXISTS user_roles (
|
||||
user_id TEXT NOT NULL,
|
||||
role_id TEXT NOT NULL,
|
||||
created_at DATETIME NOT NULL,
|
||||
updated_at DATETIME NOT NULL,
|
||||
deleted_at DATETIME,
|
||||
PRIMARY KEY (user_id, role_id)
|
||||
)
|
||||
`
|
||||
require.NoError(t, db.Exec(createUserRolesTable).Error)
|
||||
require.NoError(t, db.Exec("CREATE INDEX IF NOT EXISTS user_roles_user_id_idx ON user_roles(user_id)").Error)
|
||||
require.NoError(t, db.Exec("CREATE INDEX IF NOT EXISTS user_roles_role_id_idx ON user_roles(role_id)").Error)
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
// createTestUserRepository creates a user repository for testing
|
||||
func createTestUserRepository(db *gorm.DB) domainAuth.UserRepository {
|
||||
return &userRepository{db: db}
|
||||
}
|
||||
|
||||
// createTestRoleRepository creates a role repository for testing
|
||||
func createTestRoleRepository(db *gorm.DB) domainAuth.RoleRepository {
|
||||
return &roleRepository{db: db}
|
||||
}
|
||||
|
||||
// createTestAccountRepository creates an account repository for testing
|
||||
func createTestAccountRepository(db *gorm.DB) domainAuth.AccountRepository {
|
||||
return &accountRepository{db: db}
|
||||
}
|
||||
|
||||
// createTestUserRoleRepository creates a user role repository for testing
|
||||
func createTestUserRoleRepository(db *gorm.DB) domainAuth.UserRoleRepository {
|
||||
return &userRoleRepository{db: db}
|
||||
}
|
||||
430
internal/repository/postgres/auth/user.go
Normal file
430
internal/repository/postgres/auth/user.go
Normal file
@@ -0,0 +1,430 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"go.uber.org/fx"
|
||||
"gorm.io/gorm"
|
||||
|
||||
domainAuth "base/internal/domain/auth"
|
||||
)
|
||||
|
||||
type userRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewUserRepository(lc fx.Lifecycle, db *gorm.DB) domainAuth.UserRepository {
|
||||
lc.Append(
|
||||
fx.Hook{
|
||||
OnStart: func(ctx context.Context) error {
|
||||
return nil
|
||||
},
|
||||
OnStop: func(ctx context.Context) error {
|
||||
return nil
|
||||
},
|
||||
})
|
||||
return &userRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *userRepository) Create(ctx context.Context, user *domainAuth.User) error {
|
||||
model := toUserModel(user)
|
||||
|
||||
if err := r.db.WithContext(ctx).Create(model).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
copyUserFromModel(user, model)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *userRepository) CreateWithAccount(ctx context.Context, user *domainAuth.User, account *domainAuth.Account) error {
|
||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
// Create user within transaction
|
||||
userModel := toUserModel(user)
|
||||
if err := tx.WithContext(ctx).Create(userModel).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
copyUserFromModel(user, userModel)
|
||||
|
||||
// Create account within transaction
|
||||
accountModel := toAccountModel(account)
|
||||
if err := tx.WithContext(ctx).Create(accountModel).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
copyAccountFromModel(account, accountModel)
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (r *userRepository) UpsertWithAccount(ctx context.Context, email string, user *domainAuth.User, account *domainAuth.Account) (bool, error) {
|
||||
isNewUser := false
|
||||
|
||||
err := r.db.WithContext(ctx).Transaction(
|
||||
func(tx *gorm.DB) error {
|
||||
// Check if user exists by email
|
||||
var existingUserModel UserModel
|
||||
err := tx.WithContext(ctx).Where("email = ?", email).First(&existingUserModel).Error
|
||||
if err != nil {
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
|
||||
isNewUser = true
|
||||
userModel := toUserModel(user)
|
||||
|
||||
if err = tx.WithContext(ctx).Create(userModel).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
copyUserFromModel(user, userModel)
|
||||
|
||||
account.UserID = user.ID
|
||||
|
||||
// Create account for new user
|
||||
accountModel := toAccountModel(account)
|
||||
if err = tx.WithContext(ctx).Create(accountModel).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
copyAccountFromModel(account, accountModel)
|
||||
}
|
||||
|
||||
// TODO: check no error if user exist because in find user accounts we use user.ID
|
||||
if !isNewUser {
|
||||
// Load all accounts for this user to check if one with this provider exists
|
||||
var existingAccountModel AccountModel
|
||||
findAccountsErr := tx.WithContext(ctx).
|
||||
Where("user_id = ? AND provider = ?", user.ID, int(account.Provider)).
|
||||
First(&existingAccountModel).Error
|
||||
if findAccountsErr != nil {
|
||||
if !errors.Is(findAccountsErr, gorm.ErrRecordNotFound) {
|
||||
return findAccountsErr
|
||||
}
|
||||
|
||||
accountModel := toAccountModel(account)
|
||||
if err = tx.WithContext(ctx).Create(accountModel).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
copyAccountFromModel(account, accountModel)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
accountModel := toAccountModel(account)
|
||||
updateAccountErr := tx.WithContext(ctx).
|
||||
Model(&AccountModel{}).
|
||||
Where("id = ?", existingAccountModel.ID).
|
||||
Updates(accountModel).Error
|
||||
if updateAccountErr != nil {
|
||||
return updateAccountErr
|
||||
}
|
||||
|
||||
copyAccountFromModel(account, accountModel)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return isNewUser, err
|
||||
}
|
||||
|
||||
func (r *userRepository) FindByID(ctx context.Context, id uuid.UUID, opts ...domainAuth.UserQueryOption) (*domainAuth.User, error) {
|
||||
// Parse query options
|
||||
options := &domainAuth.UserQueryOptions{}
|
||||
for _, opt := range opts {
|
||||
opt(options)
|
||||
}
|
||||
|
||||
var model UserModel
|
||||
if err := r.db.WithContext(ctx).Where("id = ?", id).First(&model).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user := toUserDomain(&model)
|
||||
|
||||
// Conditionally load relations based on options
|
||||
if options.LoadRoles {
|
||||
roles, err := r.loadUserRoles(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user.Roles = roles
|
||||
}
|
||||
|
||||
if options.LoadAccounts {
|
||||
accounts, err := r.loadUserAccounts(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user.Accounts = accounts
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (r *userRepository) FindByEmail(ctx context.Context, email string, opts ...domainAuth.UserQueryOption) (*domainAuth.User, error) {
|
||||
// Parse query options
|
||||
options := &domainAuth.UserQueryOptions{}
|
||||
for _, opt := range opts {
|
||||
opt(options)
|
||||
}
|
||||
|
||||
var model UserModel
|
||||
if err := r.db.WithContext(ctx).Where("email = ?", email).First(&model).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user := toUserDomain(&model)
|
||||
|
||||
// Conditionally load relations based on options
|
||||
if options.LoadRoles {
|
||||
roles, err := r.loadUserRoles(ctx, user.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user.Roles = roles
|
||||
} else {
|
||||
user.Roles = []domainAuth.Role{}
|
||||
}
|
||||
|
||||
if options.LoadAccounts {
|
||||
accounts, err := r.loadUserAccounts(ctx, user.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user.Accounts = accounts
|
||||
} else {
|
||||
user.Accounts = []domainAuth.Account{}
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (r *userRepository) Update(ctx context.Context, user *domainAuth.User) error {
|
||||
model := toUserModel(user)
|
||||
return r.db.WithContext(ctx).Model(&UserModel{}).Where("id = ?", user.ID).Updates(model).Error
|
||||
}
|
||||
|
||||
func (r *userRepository) Delete(ctx context.Context, id uuid.UUID) error {
|
||||
return r.db.WithContext(ctx).Delete(&UserModel{}, "id = ?", id).Error
|
||||
}
|
||||
|
||||
func (r *userRepository) List(ctx context.Context, limit, offset int, opts ...domainAuth.UserQueryOption) ([]*domainAuth.User, error) {
|
||||
// Parse query options
|
||||
options := &domainAuth.UserQueryOptions{}
|
||||
for _, opt := range opts {
|
||||
opt(options)
|
||||
}
|
||||
|
||||
var models []UserModel
|
||||
if err := r.db.WithContext(ctx).Limit(limit).Offset(offset).Find(&models).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(models) == 0 {
|
||||
return []*domainAuth.User{}, nil
|
||||
}
|
||||
|
||||
users := make([]*domainAuth.User, len(models))
|
||||
userIDs := make([]uuid.UUID, len(models))
|
||||
|
||||
for i, model := range models {
|
||||
users[i] = toUserDomain(&model)
|
||||
userIDs[i] = users[i].ID
|
||||
}
|
||||
|
||||
// Batch load relations if requested
|
||||
if options.LoadRoles {
|
||||
rolesMap, err := r.loadUsersRoles(ctx, userIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, user := range users {
|
||||
if roles, ok := rolesMap[user.ID]; ok {
|
||||
user.Roles = roles
|
||||
} else {
|
||||
user.Roles = []domainAuth.Role{}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, user := range users {
|
||||
user.Roles = []domainAuth.Role{}
|
||||
}
|
||||
}
|
||||
|
||||
if options.LoadAccounts {
|
||||
accountsMap, err := r.loadUsersAccounts(ctx, userIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, user := range users {
|
||||
if accounts, ok := accountsMap[user.ID]; ok {
|
||||
user.Accounts = accounts
|
||||
} else {
|
||||
user.Accounts = []domainAuth.Account{}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, user := range users {
|
||||
user.Accounts = []domainAuth.Account{}
|
||||
}
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (r *userRepository) Count(ctx context.Context) (int64, error) {
|
||||
var count int64
|
||||
if err := r.db.WithContext(ctx).Model(&UserModel{}).Count(&count).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// loadUserRoles loads roles for a single user
|
||||
func (r *userRepository) loadUserRoles(ctx context.Context, userID uuid.UUID) ([]domainAuth.Role, error) {
|
||||
var roleModels []RoleModel
|
||||
if err := r.db.WithContext(ctx).
|
||||
Table("roles").
|
||||
Joins("INNER JOIN user_roles ON roles.id = user_roles.role_id").
|
||||
Where("user_roles.user_id = ? AND user_roles.deleted_at IS NULL AND roles.deleted_at IS NULL", userID).
|
||||
Find(&roleModels).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
roles := make([]domainAuth.Role, len(roleModels))
|
||||
for i, model := range roleModels {
|
||||
role := toRoleDomain(&model)
|
||||
roles[i] = *role
|
||||
}
|
||||
return roles, nil
|
||||
}
|
||||
|
||||
func (r *userRepository) UserRoles(ctx context.Context, userID uuid.UUID) ([]domainAuth.Role, error) {
|
||||
var roleModels []RoleModel
|
||||
if err := r.db.WithContext(ctx).
|
||||
Table("roles").
|
||||
Joins("INNER JOIN user_roles ON roles.id = user_roles.role_id").
|
||||
Where("user_roles.user_id = ? AND user_roles.deleted_at IS NULL AND roles.deleted_at IS NULL", userID).
|
||||
Find(&roleModels).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
roles := make([]domainAuth.Role, len(roleModels))
|
||||
for i, model := range roleModels {
|
||||
role := toRoleDomain(&model)
|
||||
roles[i] = *role
|
||||
}
|
||||
return roles, nil
|
||||
}
|
||||
|
||||
func (r *userRepository) loadUserAccounts(ctx context.Context, userID uuid.UUID) ([]domainAuth.Account, error) {
|
||||
var accountModels []AccountModel
|
||||
if err := r.db.WithContext(ctx).
|
||||
Where("user_id = ?", userID).
|
||||
Find(&accountModels).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
accounts := make([]domainAuth.Account, len(accountModels))
|
||||
for i, model := range accountModels {
|
||||
account := toAccountDomain(&model)
|
||||
accounts[i] = *account
|
||||
}
|
||||
return accounts, nil
|
||||
}
|
||||
|
||||
func (r *userRepository) UserAccounts(ctx context.Context, userID uuid.UUID) ([]domainAuth.Account, error) {
|
||||
var accountModels []AccountModel
|
||||
if err := r.db.WithContext(ctx).
|
||||
Where("user_id = ?", userID).
|
||||
Find(&accountModels).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
accounts := make([]domainAuth.Account, len(accountModels))
|
||||
for i, model := range accountModels {
|
||||
account := toAccountDomain(&model)
|
||||
accounts[i] = *account
|
||||
}
|
||||
return accounts, nil
|
||||
}
|
||||
|
||||
func (r *userRepository) loadUsersRoles(ctx context.Context, userIDs []uuid.UUID) (map[uuid.UUID][]domainAuth.Role, error) {
|
||||
if len(userIDs) == 0 {
|
||||
return make(map[uuid.UUID][]domainAuth.Role), nil
|
||||
}
|
||||
|
||||
var userRoles []struct {
|
||||
UserID uuid.UUID `gorm:"column:user_id"`
|
||||
RoleID uuid.UUID `gorm:"column:role_id"`
|
||||
}
|
||||
|
||||
if err := r.db.WithContext(ctx).
|
||||
Table("user_roles").
|
||||
Select("user_id, role_id").
|
||||
Where("user_id IN ? AND deleted_at IS NULL", userIDs).
|
||||
Find(&userRoles).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(userRoles) == 0 {
|
||||
return make(map[uuid.UUID][]domainAuth.Role), nil
|
||||
}
|
||||
|
||||
roleIDs := make([]uuid.UUID, 0, len(userRoles))
|
||||
for _, ur := range userRoles {
|
||||
roleIDs = append(roleIDs, ur.RoleID)
|
||||
}
|
||||
|
||||
var roleModels []RoleModel
|
||||
if err := r.db.WithContext(ctx).
|
||||
Where("id IN ? AND deleted_at IS NULL", roleIDs).
|
||||
Find(&roleModels).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create a map of role_id -> role
|
||||
rolesByID := make(map[uuid.UUID]*domainAuth.Role)
|
||||
for i := range roleModels {
|
||||
role := toRoleDomain(&roleModels[i])
|
||||
rolesByID[role.ID] = role
|
||||
}
|
||||
|
||||
// Group roles by user_id
|
||||
rolesMap := make(map[uuid.UUID][]domainAuth.Role)
|
||||
for _, ur := range userRoles {
|
||||
if role, ok := rolesByID[ur.RoleID]; ok {
|
||||
rolesMap[ur.UserID] = append(rolesMap[ur.UserID], *role)
|
||||
}
|
||||
}
|
||||
|
||||
return rolesMap, nil
|
||||
}
|
||||
|
||||
func (r *userRepository) loadUsersAccounts(ctx context.Context, userIDs []uuid.UUID) (map[uuid.UUID][]domainAuth.Account, error) {
|
||||
if len(userIDs) == 0 {
|
||||
return make(map[uuid.UUID][]domainAuth.Account), nil
|
||||
}
|
||||
|
||||
var accountModels []AccountModel
|
||||
if err := r.db.WithContext(ctx).
|
||||
Where("user_id IN ?", userIDs).
|
||||
Find(&accountModels).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
accountsMap := make(map[uuid.UUID][]domainAuth.Account)
|
||||
for _, model := range accountModels {
|
||||
account := toAccountDomain(&model)
|
||||
accountsMap[model.UserID] = append(accountsMap[model.UserID], *account)
|
||||
}
|
||||
|
||||
return accountsMap, nil
|
||||
}
|
||||
96
internal/repository/postgres/auth/user_role.go
Normal file
96
internal/repository/postgres/auth/user_role.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/google/uuid"
|
||||
"go.uber.org/fx"
|
||||
"gorm.io/gorm"
|
||||
|
||||
domainAuth "base/internal/domain/auth"
|
||||
)
|
||||
|
||||
type userRoleRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewUserRoleRepository(lc fx.Lifecycle, db *gorm.DB) domainAuth.UserRoleRepository {
|
||||
lc.Append(
|
||||
fx.Hook{
|
||||
OnStart: func(ctx context.Context) error {
|
||||
return db.AutoMigrate(UserRoleModel{})
|
||||
},
|
||||
OnStop: func(ctx context.Context) error {
|
||||
return nil
|
||||
},
|
||||
})
|
||||
return &userRoleRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *userRoleRepository) Create(ctx context.Context, userID, roleID uuid.UUID) error {
|
||||
model := &UserRoleModel{
|
||||
UserID: userID,
|
||||
RoleID: roleID,
|
||||
}
|
||||
return r.db.WithContext(ctx).Create(model).Error
|
||||
}
|
||||
|
||||
func (r *userRoleRepository) FindByUserID(ctx context.Context, userID uuid.UUID) ([]*domainAuth.Role, error) {
|
||||
var roleModels []RoleModel
|
||||
if err := r.db.WithContext(ctx).
|
||||
Table("roles").
|
||||
Joins("INNER JOIN user_roles ON roles.id = user_roles.role_id").
|
||||
Where("user_roles.user_id = ? AND user_roles.deleted_at IS NULL", userID).
|
||||
Find(&roleModels).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
roles := make([]*domainAuth.Role, len(roleModels))
|
||||
for i, model := range roleModels {
|
||||
roles[i] = toRoleDomain(&model)
|
||||
}
|
||||
return roles, nil
|
||||
}
|
||||
|
||||
func (r *userRoleRepository) FindByRoleID(ctx context.Context, roleID uuid.UUID) ([]*domainAuth.User, error) {
|
||||
var userModels []UserModel
|
||||
if err := r.db.WithContext(ctx).
|
||||
Table("users").
|
||||
Joins("INNER JOIN user_roles ON users.id = user_roles.user_id").
|
||||
Where("user_roles.role_id = ? AND user_roles.deleted_at IS NULL", roleID).
|
||||
Find(&userModels).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
users := make([]*domainAuth.User, len(userModels))
|
||||
for i, model := range userModels {
|
||||
users[i] = toUserDomain(&model)
|
||||
}
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (r *userRoleRepository) Delete(ctx context.Context, userID, roleID uuid.UUID) error {
|
||||
return r.db.WithContext(ctx).
|
||||
Where("user_id = ? AND role_id = ?", userID, roleID).
|
||||
Delete(&UserRoleModel{}).Error
|
||||
}
|
||||
|
||||
func (r *userRoleRepository) DeleteByUserID(ctx context.Context, userID uuid.UUID) error {
|
||||
return r.db.WithContext(ctx).
|
||||
Where("user_id = ?", userID).
|
||||
Delete(&UserRoleModel{}).Error
|
||||
}
|
||||
|
||||
func (r *userRoleRepository) DeleteByRoleID(ctx context.Context, roleID uuid.UUID) error {
|
||||
return r.db.WithContext(ctx).
|
||||
Where("role_id = ?", roleID).
|
||||
Delete(&UserRoleModel{}).Error
|
||||
}
|
||||
|
||||
func (r *userRoleRepository) Exists(ctx context.Context, userID, roleID uuid.UUID) (bool, error) {
|
||||
var count int64
|
||||
if err := r.db.WithContext(ctx).
|
||||
Model(&UserRoleModel{}).
|
||||
Where("user_id = ? AND role_id = ?", userID, roleID).
|
||||
Count(&count).Error; err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
369
internal/repository/postgres/auth/user_role_test.go
Normal file
369
internal/repository/postgres/auth/user_role_test.go
Normal file
@@ -0,0 +1,369 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
domainAuth "base/internal/domain/auth"
|
||||
)
|
||||
|
||||
func TestUserRoleRepository_Create(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
repo := createTestUserRoleRepository(db)
|
||||
userRepo := createTestUserRepository(db)
|
||||
roleRepo := createTestRoleRepository(db)
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("create user role successfully", func(t *testing.T) {
|
||||
user := &domainAuth.User{
|
||||
ID: uuid.New(),
|
||||
FirstName: "User",
|
||||
LastName: "Role",
|
||||
Email: "userrole@example.com",
|
||||
Status: domainAuth.UserStatusActive,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
err := userRepo.Create(ctx, user)
|
||||
require.NoError(t, err)
|
||||
|
||||
role := &domainAuth.Role{
|
||||
ID: uuid.New(),
|
||||
Name: "test",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
err = roleRepo.Create(ctx, role)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = repo.Create(ctx, user.ID, role.ID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify user role was created
|
||||
exists, err := repo.Exists(ctx, user.ID, role.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, exists)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserRoleRepository_FindByUserID(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
repo := createTestUserRoleRepository(db)
|
||||
userRepo := createTestUserRepository(db)
|
||||
roleRepo := createTestRoleRepository(db)
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("find roles by user id", func(t *testing.T) {
|
||||
user := &domainAuth.User{
|
||||
ID: uuid.New(),
|
||||
FirstName: "Find",
|
||||
LastName: "User",
|
||||
Email: "find@example.com",
|
||||
Status: domainAuth.UserStatusActive,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
err := userRepo.Create(ctx, user)
|
||||
require.NoError(t, err)
|
||||
|
||||
role1 := &domainAuth.Role{
|
||||
ID: uuid.New(),
|
||||
Name: "role1",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
err = roleRepo.Create(ctx, role1)
|
||||
require.NoError(t, err)
|
||||
|
||||
role2 := &domainAuth.Role{
|
||||
ID: uuid.New(),
|
||||
Name: "role2",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
err = roleRepo.Create(ctx, role2)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = repo.Create(ctx, user.ID, role1.ID)
|
||||
require.NoError(t, err)
|
||||
err = repo.Create(ctx, user.ID, role2.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
roles, err := repo.FindByUserID(ctx, user.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, roles, 2)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserRoleRepository_FindByRoleID(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
repo := createTestUserRoleRepository(db)
|
||||
userRepo := createTestUserRepository(db)
|
||||
roleRepo := createTestRoleRepository(db)
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("find users by role id", func(t *testing.T) {
|
||||
role := &domainAuth.Role{
|
||||
ID: uuid.New(),
|
||||
Name: "shared",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
err := roleRepo.Create(ctx, role)
|
||||
require.NoError(t, err)
|
||||
|
||||
user1 := &domainAuth.User{
|
||||
ID: uuid.New(),
|
||||
FirstName: "User",
|
||||
LastName: "One",
|
||||
Email: "user1@example.com",
|
||||
Status: domainAuth.UserStatusActive,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
err = userRepo.Create(ctx, user1)
|
||||
require.NoError(t, err)
|
||||
|
||||
user2 := &domainAuth.User{
|
||||
ID: uuid.New(),
|
||||
FirstName: "User",
|
||||
LastName: "Two",
|
||||
Email: "user2@example.com",
|
||||
Status: domainAuth.UserStatusActive,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
err = userRepo.Create(ctx, user2)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = repo.Create(ctx, user1.ID, role.ID)
|
||||
require.NoError(t, err)
|
||||
err = repo.Create(ctx, user2.ID, role.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
users, err := repo.FindByRoleID(ctx, role.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, users, 2)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserRoleRepository_Delete(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
repo := createTestUserRoleRepository(db)
|
||||
userRepo := createTestUserRepository(db)
|
||||
roleRepo := createTestRoleRepository(db)
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("delete user role successfully", func(t *testing.T) {
|
||||
user := &domainAuth.User{
|
||||
ID: uuid.New(),
|
||||
FirstName: "Delete",
|
||||
LastName: "User",
|
||||
Email: "delete@example.com",
|
||||
Status: domainAuth.UserStatusActive,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
err := userRepo.Create(ctx, user)
|
||||
require.NoError(t, err)
|
||||
|
||||
role := &domainAuth.Role{
|
||||
ID: uuid.New(),
|
||||
Name: "delete",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
err = roleRepo.Create(ctx, role)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = repo.Create(ctx, user.ID, role.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = repo.Delete(ctx, user.ID, role.ID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify deletion
|
||||
exists, err := repo.Exists(ctx, user.ID, role.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, exists)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserRoleRepository_DeleteByUserID(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
repo := createTestUserRoleRepository(db)
|
||||
userRepo := createTestUserRepository(db)
|
||||
roleRepo := createTestRoleRepository(db)
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("delete all roles for user", func(t *testing.T) {
|
||||
user := &domainAuth.User{
|
||||
ID: uuid.New(),
|
||||
FirstName: "Delete",
|
||||
LastName: "All",
|
||||
Email: "deleteall@example.com",
|
||||
Status: domainAuth.UserStatusActive,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
err := userRepo.Create(ctx, user)
|
||||
require.NoError(t, err)
|
||||
|
||||
role1 := &domainAuth.Role{
|
||||
ID: uuid.New(),
|
||||
Name: "role1",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
err = roleRepo.Create(ctx, role1)
|
||||
require.NoError(t, err)
|
||||
|
||||
role2 := &domainAuth.Role{
|
||||
ID: uuid.New(),
|
||||
Name: "role2",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
err = roleRepo.Create(ctx, role2)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = repo.Create(ctx, user.ID, role1.ID)
|
||||
require.NoError(t, err)
|
||||
err = repo.Create(ctx, user.ID, role2.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = repo.DeleteByUserID(ctx, user.ID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify all roles deleted
|
||||
roles, err := repo.FindByUserID(ctx, user.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, roles, 0)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserRoleRepository_DeleteByRoleID(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
repo := createTestUserRoleRepository(db)
|
||||
userRepo := createTestUserRepository(db)
|
||||
roleRepo := createTestRoleRepository(db)
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("delete role from all users", func(t *testing.T) {
|
||||
role := &domainAuth.Role{
|
||||
ID: uuid.New(),
|
||||
Name: "shared",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
err := roleRepo.Create(ctx, role)
|
||||
require.NoError(t, err)
|
||||
|
||||
user1 := &domainAuth.User{
|
||||
ID: uuid.New(),
|
||||
FirstName: "User",
|
||||
LastName: "One",
|
||||
Email: "user1@example.com",
|
||||
Status: domainAuth.UserStatusActive,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
err = userRepo.Create(ctx, user1)
|
||||
require.NoError(t, err)
|
||||
|
||||
user2 := &domainAuth.User{
|
||||
ID: uuid.New(),
|
||||
FirstName: "User",
|
||||
LastName: "Two",
|
||||
Email: "user2@example.com",
|
||||
Status: domainAuth.UserStatusActive,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
err = userRepo.Create(ctx, user2)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = repo.Create(ctx, user1.ID, role.ID)
|
||||
require.NoError(t, err)
|
||||
err = repo.Create(ctx, user2.ID, role.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = repo.DeleteByRoleID(ctx, role.ID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify role deleted from all users
|
||||
users, err := repo.FindByRoleID(ctx, role.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, users, 0)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserRoleRepository_Exists(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
repo := createTestUserRoleRepository(db)
|
||||
userRepo := createTestUserRepository(db)
|
||||
roleRepo := createTestRoleRepository(db)
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("exists returns true for existing user role", func(t *testing.T) {
|
||||
user := &domainAuth.User{
|
||||
ID: uuid.New(),
|
||||
FirstName: "Exists",
|
||||
LastName: "User",
|
||||
Email: "exists@example.com",
|
||||
Status: domainAuth.UserStatusActive,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
err := userRepo.Create(ctx, user)
|
||||
require.NoError(t, err)
|
||||
|
||||
role := &domainAuth.Role{
|
||||
ID: uuid.New(),
|
||||
Name: "exists",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
err = roleRepo.Create(ctx, role)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = repo.Create(ctx, user.ID, role.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
exists, err := repo.Exists(ctx, user.ID, role.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, exists)
|
||||
})
|
||||
|
||||
t.Run("exists returns false for non-existent user role", func(t *testing.T) {
|
||||
user := &domainAuth.User{
|
||||
ID: uuid.New(),
|
||||
FirstName: "Not",
|
||||
LastName: "Exists",
|
||||
Email: "notexists@example.com",
|
||||
Status: domainAuth.UserStatusActive,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
err := userRepo.Create(ctx, user)
|
||||
require.NoError(t, err)
|
||||
|
||||
role := &domainAuth.Role{
|
||||
ID: uuid.New(),
|
||||
Name: "notexists",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
err = roleRepo.Create(ctx, role)
|
||||
require.NoError(t, err)
|
||||
|
||||
exists, err := repo.Exists(ctx, user.ID, role.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, exists)
|
||||
})
|
||||
}
|
||||
605
internal/repository/postgres/auth/user_test.go
Normal file
605
internal/repository/postgres/auth/user_test.go
Normal file
@@ -0,0 +1,605 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"base/internal/pkg/oauth"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
domainAuth "base/internal/domain/auth"
|
||||
)
|
||||
|
||||
func TestUserRepository_Create(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
repo := createTestUserRepository(db)
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("create user successfully", func(t *testing.T) {
|
||||
user := &domainAuth.User{
|
||||
ID: uuid.New(),
|
||||
FirstName: "John",
|
||||
LastName: "Doe",
|
||||
Email: "john.doe@example.com",
|
||||
EmailVerified: false,
|
||||
Status: domainAuth.UserStatusActive,
|
||||
PhoneNumber: "1234567890",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
err := repo.Create(ctx, user)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEqual(t, uuid.Nil, user.ID)
|
||||
|
||||
// Verify user was created
|
||||
found, err := repo.FindByID(ctx, user.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, user.Email, found.Email)
|
||||
assert.Equal(t, user.FirstName, found.FirstName)
|
||||
assert.Equal(t, user.LastName, found.LastName)
|
||||
})
|
||||
|
||||
t.Run("create user with duplicate email fails", func(t *testing.T) {
|
||||
email := "duplicate@example.com"
|
||||
user1 := &domainAuth.User{
|
||||
ID: uuid.New(),
|
||||
FirstName: "User",
|
||||
LastName: "One",
|
||||
Email: email,
|
||||
Status: domainAuth.UserStatusActive,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
err := repo.Create(ctx, user1)
|
||||
assert.NoError(t, err)
|
||||
|
||||
user2 := &domainAuth.User{
|
||||
ID: uuid.New(),
|
||||
FirstName: "User",
|
||||
LastName: "Two",
|
||||
Email: email,
|
||||
Status: domainAuth.UserStatusActive,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
err = repo.Create(ctx, user2)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserRepository_UpsertWithAccount(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
repo := createTestUserRepository(db)
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("upsert creates new user and account", func(t *testing.T) {
|
||||
email := "newuser@example.com"
|
||||
user := &domainAuth.User{
|
||||
ID: uuid.New(),
|
||||
FirstName: "New",
|
||||
LastName: "User",
|
||||
Email: email,
|
||||
Status: domainAuth.UserStatusActive,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
account := &domainAuth.Account{
|
||||
ID: uuid.New(),
|
||||
Provider: oauth.Google,
|
||||
Scope: []string{"read"},
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
isNew, err := repo.UpsertWithAccount(ctx, email, user, account)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, isNew)
|
||||
// For new users, UserID is set by UpsertWithAccount
|
||||
assert.Equal(t, user.ID, account.UserID)
|
||||
|
||||
// Verify user was created
|
||||
foundUser, err := repo.FindByID(ctx, user.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, user.Email, foundUser.Email)
|
||||
|
||||
// Note: For new users, UpsertWithAccount sets account.UserID but doesn't create the account
|
||||
// The account needs to be created separately if needed
|
||||
})
|
||||
|
||||
t.Run("upsert updates existing user with new account", func(t *testing.T) {
|
||||
email := "existing@example.com"
|
||||
user := &domainAuth.User{
|
||||
ID: uuid.New(),
|
||||
FirstName: "Existing",
|
||||
LastName: "User",
|
||||
Email: email,
|
||||
Status: domainAuth.UserStatusActive,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
// Create user first
|
||||
err := repo.Create(ctx, user)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Get the user from DB to ensure we have the correct ID
|
||||
foundUser, err := repo.FindByEmail(ctx, email)
|
||||
require.NoError(t, err)
|
||||
user.ID = foundUser.ID
|
||||
|
||||
// Create first account with Google provider
|
||||
account1 := &domainAuth.Account{
|
||||
ID: uuid.New(),
|
||||
UserID: user.ID,
|
||||
Provider: oauth.Google,
|
||||
Scope: []string{"read"},
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
accountRepo := createTestAccountRepository(db)
|
||||
err = accountRepo.Create(ctx, account1)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Upsert with different provider (GitHub) - should create new account
|
||||
account2 := &domainAuth.Account{
|
||||
ID: uuid.New(),
|
||||
UserID: user.ID, // Set UserID before upsert
|
||||
Provider: oauth.GitHub,
|
||||
Scope: []string{"read", "write"},
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
// Use the same user object but ensure it has the correct ID
|
||||
userForUpsert := &domainAuth.User{
|
||||
ID: user.ID,
|
||||
FirstName: user.FirstName,
|
||||
LastName: user.LastName,
|
||||
Email: user.Email,
|
||||
Status: user.Status,
|
||||
CreatedAt: user.CreatedAt,
|
||||
UpdatedAt: user.UpdatedAt,
|
||||
}
|
||||
|
||||
isNew, err := repo.UpsertWithAccount(ctx, email, userForUpsert, account2)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, isNew)
|
||||
// Note: account.UserID is not updated by UpsertWithAccount when user exists,
|
||||
// but it should already be set correctly
|
||||
assert.Equal(t, user.ID, account2.UserID)
|
||||
// Account ID should be set after creation
|
||||
assert.NotEqual(t, uuid.Nil, account2.ID)
|
||||
|
||||
// Verify the GitHub account was created by finding it by ID
|
||||
foundAccount2, err := accountRepo.FindByID(ctx, account2.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, foundAccount2)
|
||||
assert.Equal(t, account2.UserID, foundAccount2.UserID)
|
||||
assert.Equal(t, account2.Provider, foundAccount2.Provider)
|
||||
assert.Equal(t, account2.Scope, foundAccount2.Scope)
|
||||
|
||||
// Verify both accounts exist
|
||||
accounts, err := accountRepo.FindByUserID(ctx, user.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, len(accounts), 2) // At least Google and GitHub accounts
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserRepository_FindByID(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
repo := createTestUserRepository(db)
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("find existing user by id", func(t *testing.T) {
|
||||
user := &domainAuth.User{
|
||||
ID: uuid.New(),
|
||||
FirstName: "Find",
|
||||
LastName: "User",
|
||||
Email: "find@example.com",
|
||||
Status: domainAuth.UserStatusActive,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
err := repo.Create(ctx, user)
|
||||
require.NoError(t, err)
|
||||
|
||||
found, err := repo.FindByID(ctx, user.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, user.ID, found.ID)
|
||||
assert.Equal(t, user.Email, found.Email)
|
||||
})
|
||||
|
||||
t.Run("find user with roles", func(t *testing.T) {
|
||||
user := &domainAuth.User{
|
||||
ID: uuid.New(),
|
||||
FirstName: "Role",
|
||||
LastName: "User",
|
||||
Email: "role@example.com",
|
||||
Status: domainAuth.UserStatusActive,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
err := repo.Create(ctx, user)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create role
|
||||
roleRepo := createTestRoleRepository(db)
|
||||
role := &domainAuth.Role{
|
||||
ID: uuid.New(),
|
||||
Name: "admin",
|
||||
Description: "Administrator",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
err = roleRepo.Create(ctx, role)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Assign role to user
|
||||
userRoleRepo := createTestUserRoleRepository(db)
|
||||
err = userRoleRepo.Create(ctx, user.ID, role.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Find user with roles
|
||||
found, err := repo.FindByID(ctx, user.ID, domainAuth.WithRoles())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, user.ID, found.ID)
|
||||
assert.Len(t, found.Roles, 1)
|
||||
assert.Equal(t, role.Name, found.Roles[0].Name)
|
||||
})
|
||||
|
||||
t.Run("find non-existent user", func(t *testing.T) {
|
||||
nonExistentID := uuid.New()
|
||||
found, err := repo.FindByID(ctx, nonExistentID)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, found)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserRepository_FindByEmail(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
repo := createTestUserRepository(db)
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("find existing user by email", func(t *testing.T) {
|
||||
email := "email@example.com"
|
||||
user := &domainAuth.User{
|
||||
ID: uuid.New(),
|
||||
FirstName: "Email",
|
||||
LastName: "User",
|
||||
Email: email,
|
||||
Status: domainAuth.UserStatusActive,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
err := repo.Create(ctx, user)
|
||||
require.NoError(t, err)
|
||||
|
||||
found, err := repo.FindByEmail(ctx, email)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, user.ID, found.ID)
|
||||
assert.Equal(t, email, found.Email)
|
||||
})
|
||||
|
||||
t.Run("find user with accounts", func(t *testing.T) {
|
||||
email := "accounts@example.com"
|
||||
user := &domainAuth.User{
|
||||
ID: uuid.New(),
|
||||
FirstName: "Accounts",
|
||||
LastName: "User",
|
||||
Email: email,
|
||||
Status: domainAuth.UserStatusActive,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
err := repo.Create(ctx, user)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create account
|
||||
accountRepo := createTestAccountRepository(db)
|
||||
account := &domainAuth.Account{
|
||||
ID: uuid.New(),
|
||||
UserID: user.ID,
|
||||
Provider: oauth.Google,
|
||||
Scope: []string{"read"},
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
err = accountRepo.Create(ctx, account)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Find user with accounts
|
||||
found, err := repo.FindByEmail(ctx, email, domainAuth.WithAccounts())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, user.ID, found.ID)
|
||||
assert.Len(t, found.Accounts, 1)
|
||||
assert.Equal(t, account.Provider, found.Accounts[0].Provider)
|
||||
})
|
||||
|
||||
t.Run("find non-existent user by email", func(t *testing.T) {
|
||||
found, err := repo.FindByEmail(ctx, "nonexistent@example.com")
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, found)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserRepository_Update(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
repo := createTestUserRepository(db)
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("update user successfully", func(t *testing.T) {
|
||||
user := &domainAuth.User{
|
||||
ID: uuid.New(),
|
||||
FirstName: "Update",
|
||||
LastName: "User",
|
||||
Email: "update@example.com",
|
||||
Status: domainAuth.UserStatusActive,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
err := repo.Create(ctx, user)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Update user
|
||||
user.FirstName = "Updated"
|
||||
user.EmailVerified = true
|
||||
user.Status = domainAuth.UserStatusInactive
|
||||
|
||||
err = repo.Update(ctx, user)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify update
|
||||
found, err := repo.FindByID(ctx, user.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Updated", found.FirstName)
|
||||
assert.True(t, found.EmailVerified)
|
||||
assert.Equal(t, domainAuth.UserStatusInactive, found.Status)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserRepository_Delete(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
repo := createTestUserRepository(db)
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("delete user successfully", func(t *testing.T) {
|
||||
user := &domainAuth.User{
|
||||
ID: uuid.New(),
|
||||
FirstName: "Delete",
|
||||
LastName: "User",
|
||||
Email: "delete@example.com",
|
||||
Status: domainAuth.UserStatusActive,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
err := repo.Create(ctx, user)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = repo.Delete(ctx, user.ID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify deletion (soft delete)
|
||||
found, err := repo.FindByID(ctx, user.ID)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, found)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserRepository_List(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
repo := createTestUserRepository(db)
|
||||
ctx := context.Background()
|
||||
|
||||
// Create multiple users
|
||||
for i := 0; i < 5; i++ {
|
||||
user := &domainAuth.User{
|
||||
ID: uuid.New(),
|
||||
FirstName: "User",
|
||||
LastName: "Test",
|
||||
Email: "user" + strconv.Itoa(i) + "@example.com",
|
||||
Status: domainAuth.UserStatusActive,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
err := repo.Create(ctx, user)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
t.Run("list users with limit and offset", func(t *testing.T) {
|
||||
users, err := repo.List(ctx, 3, 0)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, users, 3)
|
||||
|
||||
users, err = repo.List(ctx, 3, 3)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, users, 2) // Remaining 2 users
|
||||
})
|
||||
|
||||
t.Run("list users with relations", func(t *testing.T) {
|
||||
user := &domainAuth.User{
|
||||
ID: uuid.New(),
|
||||
FirstName: "Relation",
|
||||
LastName: "User",
|
||||
Email: "relation@example.com",
|
||||
Status: domainAuth.UserStatusActive,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
err := repo.Create(ctx, user)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create role and assign
|
||||
roleRepo := createTestRoleRepository(db)
|
||||
role := &domainAuth.Role{
|
||||
ID: uuid.New(),
|
||||
Name: "user",
|
||||
Description: "Regular user",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
err = roleRepo.Create(ctx, role)
|
||||
require.NoError(t, err)
|
||||
|
||||
userRoleRepo := createTestUserRoleRepository(db)
|
||||
err = userRoleRepo.Create(ctx, user.ID, role.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
users, err := repo.List(ctx, 10, 0, domainAuth.WithRoles())
|
||||
assert.NoError(t, err)
|
||||
assert.Greater(t, len(users), 0)
|
||||
|
||||
// Find our user in the list
|
||||
var foundUser *domainAuth.User
|
||||
for _, u := range users {
|
||||
if u.ID == user.ID {
|
||||
foundUser = u
|
||||
break
|
||||
}
|
||||
}
|
||||
require.NotNil(t, foundUser)
|
||||
assert.Len(t, foundUser.Roles, 1)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserRepository_Count(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
repo := createTestUserRepository(db)
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("count users", func(t *testing.T) {
|
||||
initialCount, err := repo.Count(ctx)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(0), initialCount)
|
||||
|
||||
// Create users
|
||||
for i := 0; i < 3; i++ {
|
||||
user := &domainAuth.User{
|
||||
ID: uuid.New(),
|
||||
FirstName: "Count",
|
||||
LastName: "User",
|
||||
Email: "count" + strconv.Itoa(i) + "@example.com",
|
||||
Status: domainAuth.UserStatusActive,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
err := repo.Create(ctx, user)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
count, err := repo.Count(ctx)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(3), count)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserRepository_UserRoles(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
repo := createTestUserRepository(db)
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("get user roles", func(t *testing.T) {
|
||||
user := &domainAuth.User{
|
||||
ID: uuid.New(),
|
||||
FirstName: "Roles",
|
||||
LastName: "User",
|
||||
Email: "roles@example.com",
|
||||
Status: domainAuth.UserStatusActive,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
err := repo.Create(ctx, user)
|
||||
require.NoError(t, err)
|
||||
|
||||
roleRepo := createTestRoleRepository(db)
|
||||
role1 := &domainAuth.Role{
|
||||
ID: uuid.New(),
|
||||
Name: "admin",
|
||||
Description: "Admin role",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
err = roleRepo.Create(ctx, role1)
|
||||
require.NoError(t, err)
|
||||
|
||||
role2 := &domainAuth.Role{
|
||||
ID: uuid.New(),
|
||||
Name: "user",
|
||||
Description: "User role",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
err = roleRepo.Create(ctx, role2)
|
||||
require.NoError(t, err)
|
||||
|
||||
userRoleRepo := createTestUserRoleRepository(db)
|
||||
err = userRoleRepo.Create(ctx, user.ID, role1.ID)
|
||||
require.NoError(t, err)
|
||||
err = userRoleRepo.Create(ctx, user.ID, role2.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
roles, err := repo.UserRoles(ctx, user.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, roles, 2)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserRepository_UserAccounts(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
repo := createTestUserRepository(db)
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("get user accounts", func(t *testing.T) {
|
||||
user := &domainAuth.User{
|
||||
ID: uuid.New(),
|
||||
FirstName: "Accounts",
|
||||
LastName: "User",
|
||||
Email: "accounts@example.com",
|
||||
Status: domainAuth.UserStatusActive,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
err := repo.Create(ctx, user)
|
||||
require.NoError(t, err)
|
||||
|
||||
accountRepo := createTestAccountRepository(db)
|
||||
account1 := &domainAuth.Account{
|
||||
ID: uuid.New(),
|
||||
UserID: user.ID,
|
||||
Provider: oauth.Google,
|
||||
Scope: []string{"read"},
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
err = accountRepo.Create(ctx, account1)
|
||||
require.NoError(t, err)
|
||||
|
||||
account2 := &domainAuth.Account{
|
||||
ID: uuid.New(),
|
||||
UserID: user.ID,
|
||||
Provider: oauth.GitHub,
|
||||
Scope: []string{"read", "write"},
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
err = accountRepo.Create(ctx, account2)
|
||||
require.NoError(t, err)
|
||||
|
||||
accounts, err := repo.UserAccounts(ctx, user.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, accounts, 2)
|
||||
})
|
||||
}
|
||||
30
internal/repository/postgres/cache/model.go
vendored
Normal file
30
internal/repository/postgres/cache/model.go
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/datatypes"
|
||||
)
|
||||
|
||||
type KVModel struct {
|
||||
Key string `gorm:"primaryKey"`
|
||||
Value datatypes.JSON
|
||||
ExpiresAt *time.Time
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
func (KVModel) TableName() string {
|
||||
return "cache_kv"
|
||||
}
|
||||
|
||||
type HashModel struct {
|
||||
Key string `gorm:"primaryKey"`
|
||||
Field string `gorm:"primaryKey"`
|
||||
Value datatypes.JSON
|
||||
CreatedAt time.Time
|
||||
ExpiresAt *time.Time
|
||||
}
|
||||
|
||||
func (HashModel) TableName() string {
|
||||
return "cache_hash"
|
||||
}
|
||||
196
internal/repository/postgres/profile/mapper.go
Normal file
196
internal/repository/postgres/profile/mapper.go
Normal file
@@ -0,0 +1,196 @@
|
||||
package profile
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
domainProfile "base/internal/domain/profile"
|
||||
)
|
||||
|
||||
func toProfileModel(profile *domainProfile.Profile) (*Model, error) {
|
||||
pageSectionOrder, err := json.Marshal(profile.PageSectionOrder)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var roleID *uuid.UUID
|
||||
var roleName *string
|
||||
roleLevel := ""
|
||||
if profile.Hero.Role != nil {
|
||||
roleLevel = profile.Hero.Role.Level
|
||||
if profile.Hero.Role.ID != uuid.Nil {
|
||||
roleID = &profile.Hero.Role.ID
|
||||
roleName = &profile.Hero.Role.Title
|
||||
}
|
||||
}
|
||||
|
||||
return &Model{
|
||||
ID: profile.ID,
|
||||
UserID: profile.UserID,
|
||||
Handle: profile.Handle,
|
||||
RoleID: roleID,
|
||||
RoleName: roleName,
|
||||
RoleLevel: roleLevel,
|
||||
FirstName: profile.Hero.FirstName,
|
||||
LastName: profile.Hero.LastName,
|
||||
Company: profile.Hero.Company,
|
||||
ShortDescription: profile.Hero.ShortDescription,
|
||||
ResumeLink: profile.Hero.ResumeLink,
|
||||
CTAEnabled: profile.Hero.CTAEnabled,
|
||||
Avatar: profile.Hero.Avatar,
|
||||
ProfilePicture: profile.About.ProfilePicture,
|
||||
About: profile.About.About,
|
||||
Email: profile.Contact.Email,
|
||||
Phone: profile.Contact.Phone,
|
||||
VisibilityLevel: profile.PageSetting.VisibilityLevel,
|
||||
PageSectionOrder: pageSectionOrder,
|
||||
CreatedAt: profile.CreatedAt,
|
||||
UpdatedAt: profile.UpdatedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toProfileDomain(model *Model, skills []domainProfile.Skill, socialLinks []domainProfile.SocialLink, achievements []domainProfile.Achievement) (*domainProfile.Profile, error) {
|
||||
var pageSectionOrder map[string]int
|
||||
if len(model.PageSectionOrder) > 0 {
|
||||
if err := json.Unmarshal(model.PageSectionOrder, &pageSectionOrder); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var role *domainProfile.Role
|
||||
|
||||
if model.RoleID != nil && *model.RoleID != uuid.Nil {
|
||||
title := ""
|
||||
if model.Role != nil {
|
||||
title = model.Role.Title
|
||||
} else if model.RoleName != nil {
|
||||
title = *model.RoleName
|
||||
}
|
||||
role = &domainProfile.Role{
|
||||
ID: *model.RoleID,
|
||||
Title: title,
|
||||
Level: model.RoleLevel,
|
||||
}
|
||||
} else if model.RoleLevel != "" {
|
||||
role = &domainProfile.Role{Level: model.RoleLevel}
|
||||
}
|
||||
|
||||
hero := domainProfile.Hero{
|
||||
Role: role,
|
||||
FirstName: model.FirstName,
|
||||
LastName: model.LastName,
|
||||
Company: model.Company,
|
||||
ShortDescription: model.ShortDescription,
|
||||
ResumeLink: model.ResumeLink,
|
||||
CTAEnabled: model.CTAEnabled,
|
||||
Avatar: model.Avatar,
|
||||
}
|
||||
|
||||
about := domainProfile.About{
|
||||
ProfilePicture: model.ProfilePicture,
|
||||
About: model.About,
|
||||
Achievements: achievements,
|
||||
}
|
||||
|
||||
contact := domainProfile.Contact{
|
||||
Email: model.Email,
|
||||
Phone: model.Phone,
|
||||
SocialLinks: socialLinks,
|
||||
}
|
||||
|
||||
pageSetting := domainProfile.PageSetting{
|
||||
VisibilityLevel: model.VisibilityLevel,
|
||||
}
|
||||
|
||||
return &domainProfile.Profile{
|
||||
ID: model.ID,
|
||||
UserID: model.UserID,
|
||||
Handle: model.Handle,
|
||||
PageSectionOrder: pageSectionOrder,
|
||||
Hero: hero,
|
||||
About: about,
|
||||
Skills: skills,
|
||||
Contact: contact,
|
||||
PageSetting: pageSetting,
|
||||
CreatedAt: model.CreatedAt,
|
||||
UpdatedAt: model.UpdatedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toSkillModels(profileID uuid.UUID, skills []domainProfile.Skill) []SkillModel {
|
||||
models := make([]SkillModel, len(skills))
|
||||
for i, skill := range skills {
|
||||
models[i] = SkillModel{
|
||||
ProfileID: profileID,
|
||||
SkillName: skill.SkillName,
|
||||
Level: skill.Level,
|
||||
}
|
||||
}
|
||||
return models
|
||||
}
|
||||
|
||||
func toSkillDomains(models []SkillModel) []domainProfile.Skill {
|
||||
skills := make([]domainProfile.Skill, len(models))
|
||||
for i, model := range models {
|
||||
skills[i] = domainProfile.Skill{
|
||||
SkillName: model.SkillName,
|
||||
Level: model.Level,
|
||||
}
|
||||
}
|
||||
return skills
|
||||
}
|
||||
|
||||
func toSocialLinkModels(profileID uuid.UUID, socialLinks []domainProfile.SocialLink) []SocialLinkModel {
|
||||
models := make([]SocialLinkModel, len(socialLinks))
|
||||
for i, link := range socialLinks {
|
||||
models[i] = SocialLinkModel{
|
||||
ProfileID: profileID,
|
||||
LinkType: link.LinkType,
|
||||
Link: link.Link,
|
||||
}
|
||||
}
|
||||
return models
|
||||
}
|
||||
|
||||
func toSocialLinkDomains(models []SocialLinkModel) []domainProfile.SocialLink {
|
||||
links := make([]domainProfile.SocialLink, len(models))
|
||||
for i, model := range models {
|
||||
links[i] = domainProfile.SocialLink{
|
||||
LinkType: model.LinkType,
|
||||
Link: model.Link,
|
||||
}
|
||||
}
|
||||
return links
|
||||
}
|
||||
|
||||
func toAchievementModels(profileID uuid.UUID, achievements []domainProfile.Achievement) []AchievementModel {
|
||||
models := make([]AchievementModel, len(achievements))
|
||||
for i, achievement := range achievements {
|
||||
models[i] = AchievementModel{
|
||||
ProfileID: profileID,
|
||||
Title: achievement.Title,
|
||||
Value: achievement.Value,
|
||||
Enabled: achievement.Enabled,
|
||||
}
|
||||
}
|
||||
return models
|
||||
}
|
||||
|
||||
func toAchievementDomains(models []AchievementModel) []domainProfile.Achievement {
|
||||
achievements := make([]domainProfile.Achievement, len(models))
|
||||
for i, model := range models {
|
||||
achievements[i] = domainProfile.Achievement{
|
||||
Title: model.Title,
|
||||
Value: model.Value,
|
||||
Enabled: model.Enabled,
|
||||
}
|
||||
}
|
||||
return achievements
|
||||
}
|
||||
|
||||
func copyProfileFromModel(profile *domainProfile.Profile, model *Model) error {
|
||||
profile.ID = model.ID
|
||||
profile.Handle = model.Handle
|
||||
return nil
|
||||
}
|
||||
315
internal/repository/postgres/profile/profile.go
Normal file
315
internal/repository/postgres/profile/profile.go
Normal file
@@ -0,0 +1,315 @@
|
||||
package profile
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"go.uber.org/fx"
|
||||
"gorm.io/gorm"
|
||||
|
||||
domainProfile "base/internal/domain/profile"
|
||||
)
|
||||
|
||||
type profileRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewProfileRepository(lc fx.Lifecycle, db *gorm.DB) domainProfile.Repository {
|
||||
lc.Append(
|
||||
fx.Hook{
|
||||
OnStart: func(ctx context.Context) error {
|
||||
return nil
|
||||
},
|
||||
OnStop: func(ctx context.Context) error {
|
||||
return nil
|
||||
},
|
||||
})
|
||||
return &profileRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *profileRepository) Create(ctx context.Context, profile *domainProfile.Profile) error {
|
||||
model, err := toProfileModel(profile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Start a transaction
|
||||
tx := r.db.WithContext(ctx).Begin()
|
||||
if tx.Error != nil {
|
||||
return tx.Error
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
// Create profile
|
||||
if err := tx.Create(model).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create skills if any
|
||||
if len(profile.Skills) > 0 {
|
||||
skillModels := toSkillModels(model.ID, profile.Skills)
|
||||
if err := tx.Create(&skillModels).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Create social links if any
|
||||
if len(profile.Contact.SocialLinks) > 0 {
|
||||
socialLinkModels := toSocialLinkModels(model.ID, profile.Contact.SocialLinks)
|
||||
if err := tx.Create(&socialLinkModels).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Create achievements if any
|
||||
if len(profile.About.Achievements) > 0 {
|
||||
achievementModels := toAchievementModels(model.ID, profile.About.Achievements)
|
||||
if err := tx.Create(&achievementModels).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return copyProfileFromModel(profile, model)
|
||||
}
|
||||
|
||||
func (r *profileRepository) loadRelatedData(ctx context.Context, profileID uuid.UUID) ([]domainProfile.Skill, []domainProfile.SocialLink, []domainProfile.Achievement, error) {
|
||||
// Load skills
|
||||
var skillModels []SkillModel
|
||||
if err := r.db.WithContext(ctx).Where("profile_id = ?", profileID).Find(&skillModels).Error; err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
skills := toSkillDomains(skillModels)
|
||||
|
||||
// Load social links
|
||||
var socialLinkModels []SocialLinkModel
|
||||
if err := r.db.WithContext(ctx).Where("profile_id = ?", profileID).Find(&socialLinkModels).Error; err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
socialLinks := toSocialLinkDomains(socialLinkModels)
|
||||
|
||||
// Load achievements
|
||||
var achievementModels []AchievementModel
|
||||
if err := r.db.WithContext(ctx).Where("profile_id = ?", profileID).Find(&achievementModels).Error; err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
achievements := toAchievementDomains(achievementModels)
|
||||
|
||||
return skills, socialLinks, achievements, nil
|
||||
}
|
||||
|
||||
func (r *profileRepository) FindByID(ctx context.Context, id uuid.UUID) (*domainProfile.Profile, error) {
|
||||
var model Model
|
||||
if err := r.db.WithContext(ctx).Preload("Role").Where("id = ?", id).First(&model).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("profile not found")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
skills, socialLinks, achievements, err := r.loadRelatedData(ctx, model.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return toProfileDomain(&model, skills, socialLinks, achievements)
|
||||
}
|
||||
|
||||
func (r *profileRepository) FindByHandle(ctx context.Context, handle string) (*domainProfile.Profile, error) {
|
||||
var model Model
|
||||
if err := r.db.WithContext(ctx).Preload("Role").Where("handle = ?", handle).First(&model).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("profile not found")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
skills, socialLinks, achievements, err := r.loadRelatedData(ctx, model.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return toProfileDomain(&model, skills, socialLinks, achievements)
|
||||
}
|
||||
|
||||
func (r *profileRepository) FindByUserID(ctx context.Context, userID uuid.UUID) (*domainProfile.Profile, error) {
|
||||
var model Model
|
||||
if err := r.db.WithContext(ctx).Preload("Role").Where("user_id = ? AND user_id IS NOT NULL", userID).First(&model).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("profile not found")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
skills, socialLinks, achievements, err := r.loadRelatedData(ctx, model.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return toProfileDomain(&model, skills, socialLinks, achievements)
|
||||
}
|
||||
|
||||
func (r *profileRepository) Update(ctx context.Context, profile *domainProfile.Profile) error {
|
||||
model, err := toProfileModel(profile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Start a transaction
|
||||
tx := r.db.WithContext(ctx).Begin()
|
||||
if tx.Error != nil {
|
||||
return tx.Error
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
// Update profile
|
||||
if err := tx.Model(&Model{}).Where("id = ?", profile.ID).Updates(model).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete existing related data
|
||||
if err := tx.Where("profile_id = ?", profile.ID).Delete(&SkillModel{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("profile_id = ?", profile.ID).Delete(&SocialLinkModel{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("profile_id = ?", profile.ID).Delete(&AchievementModel{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create new skills
|
||||
if len(profile.Skills) > 0 {
|
||||
skillModels := toSkillModels(profile.ID, profile.Skills)
|
||||
if err := tx.Create(&skillModels).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Create new social links
|
||||
if len(profile.Contact.SocialLinks) > 0 {
|
||||
socialLinkModels := toSocialLinkModels(profile.ID, profile.Contact.SocialLinks)
|
||||
if err := tx.Create(&socialLinkModels).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Create new achievements
|
||||
if len(profile.About.Achievements) > 0 {
|
||||
achievementModels := toAchievementModels(profile.ID, profile.About.Achievements)
|
||||
if err := tx.Create(&achievementModels).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Commit().Error
|
||||
}
|
||||
|
||||
func (r *profileRepository) Delete(ctx context.Context, profile *domainProfile.Profile) error {
|
||||
// Start a transaction
|
||||
tx := r.db.WithContext(ctx).Begin()
|
||||
if tx.Error != nil {
|
||||
return tx.Error
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
// Delete related data first
|
||||
if err := tx.Where("profile_id = ?", profile.ID).Delete(&SkillModel{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("profile_id = ?", profile.ID).Delete(&SocialLinkModel{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("profile_id = ?", profile.ID).Delete(&AchievementModel{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete profile
|
||||
if err := tx.Delete(&Model{}, "id = ?", profile.ID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.Commit().Error
|
||||
}
|
||||
|
||||
// buildBaseQuery applies common filters to a query
|
||||
func (r *profileRepository) buildBaseQuery(ctx context.Context, filter domainProfile.Filter) *gorm.DB {
|
||||
query := r.db.WithContext(ctx).Model(&Model{})
|
||||
|
||||
if filter.RoleID != uuid.Nil {
|
||||
query = query.Where("role_id = ?", filter.RoleID)
|
||||
}
|
||||
if filter.FirstName != "" {
|
||||
query = query.Where("LOWER(first_name) LIKE ?", "%"+filter.FirstName+"%")
|
||||
}
|
||||
if filter.LastName != "" {
|
||||
query = query.Where("LOWER(last_name) LIKE ?", "%"+filter.LastName+"%")
|
||||
}
|
||||
if filter.Company != "" {
|
||||
query = query.Where("LOWER(company) LIKE ?", "%"+filter.Company+"%")
|
||||
}
|
||||
if filter.SkillName != "" {
|
||||
subQuery := r.db.WithContext(ctx).Model(&SkillModel{}).
|
||||
Select("DISTINCT profile_id").
|
||||
Where("LOWER(skill_name) LIKE ? AND deleted_at IS NULL", "%"+filter.SkillName+"%")
|
||||
query = query.Where("profiles.id IN (?)", subQuery)
|
||||
}
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
func (r *profileRepository) FindAll(ctx context.Context, filter domainProfile.Filter) ([]*domainProfile.Profile, int, error) {
|
||||
baseQuery := r.buildBaseQuery(ctx, filter)
|
||||
|
||||
var total int64
|
||||
if err := baseQuery.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
query := baseQuery
|
||||
offset := int((filter.Page - 1) * filter.PageSize)
|
||||
limit := int(filter.PageSize)
|
||||
|
||||
if limit > 0 {
|
||||
query = query.Limit(limit).Offset(offset)
|
||||
}
|
||||
|
||||
if filter.SortedBy != "" {
|
||||
order := "ASC"
|
||||
if !filter.Ascending {
|
||||
order = "DESC"
|
||||
}
|
||||
query = query.Order("profiles." + filter.SortedBy + " " + order)
|
||||
} else {
|
||||
query = query.Order("profiles.created_at DESC")
|
||||
}
|
||||
|
||||
var models []Model
|
||||
if err := query.Preload("Role").Find(&models).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
if len(models) == 0 {
|
||||
return nil, int(total), nil
|
||||
}
|
||||
|
||||
profiles := make([]*domainProfile.Profile, len(models))
|
||||
for i, model := range models {
|
||||
skills, socialLinks, achievements, err := r.loadRelatedData(ctx, model.ID)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
profile, err := toProfileDomain(&model, skills, socialLinks, achievements)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
profiles[i] = profile
|
||||
}
|
||||
|
||||
return profiles, int(total), nil
|
||||
}
|
||||
870
internal/repository/postgres/profile/profile_test.go
Normal file
870
internal/repository/postgres/profile/profile_test.go
Normal file
@@ -0,0 +1,870 @@
|
||||
package profile
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
domainProfile "base/internal/domain/profile"
|
||||
)
|
||||
|
||||
func TestProfileRepository_Create(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
repo := createTestProfileRepository(db)
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("create profile successfully", func(t *testing.T) {
|
||||
profile := &domainProfile.Profile{
|
||||
ID: uuid.New(),
|
||||
Handle: "test-handle",
|
||||
PageSectionOrder: map[string]int{
|
||||
"hero": 1,
|
||||
"about": 2,
|
||||
"skills": 3,
|
||||
},
|
||||
Hero: domainProfile.Hero{
|
||||
FirstName: "John",
|
||||
LastName: "Doe",
|
||||
Company: "Test Company",
|
||||
ShortDescription: "Test description",
|
||||
CTAEnabled: true,
|
||||
},
|
||||
About: domainProfile.About{
|
||||
ProfilePicture: "https://example.com/pic.jpg",
|
||||
About: "About me",
|
||||
},
|
||||
Contact: domainProfile.Contact{
|
||||
Email: "john.doe@example.com",
|
||||
Phone: "1234567890",
|
||||
},
|
||||
PageSetting: domainProfile.PageSetting{
|
||||
VisibilityLevel: "public",
|
||||
},
|
||||
}
|
||||
|
||||
err := repo.Create(ctx, profile)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEqual(t, uuid.Nil, profile.ID)
|
||||
|
||||
// Verify profile was created
|
||||
found, err := repo.FindByHandle(ctx, profile.Handle)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, profile.Handle, found.Handle)
|
||||
assert.Equal(t, profile.Hero.FirstName, found.Hero.FirstName)
|
||||
assert.Equal(t, profile.Hero.LastName, found.Hero.LastName)
|
||||
assert.Equal(t, profile.Contact.Email, found.Contact.Email)
|
||||
})
|
||||
|
||||
t.Run("create profile with skills", func(t *testing.T) {
|
||||
profile := &domainProfile.Profile{
|
||||
ID: uuid.New(),
|
||||
Handle: "test-handle-with-skills",
|
||||
Hero: domainProfile.Hero{
|
||||
FirstName: "Jane",
|
||||
LastName: "Smith",
|
||||
},
|
||||
Skills: []domainProfile.Skill{
|
||||
{SkillName: "Go", Level: "expert"},
|
||||
{SkillName: "Python", Level: "intermediate"},
|
||||
},
|
||||
}
|
||||
|
||||
err := repo.Create(ctx, profile)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify profile with skills
|
||||
found, err := repo.FindByHandle(ctx, profile.Handle)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, found.Skills, 2)
|
||||
assert.Equal(t, "Go", found.Skills[0].SkillName)
|
||||
assert.Equal(t, "expert", found.Skills[0].Level)
|
||||
})
|
||||
|
||||
t.Run("create profile with social links", func(t *testing.T) {
|
||||
profile := &domainProfile.Profile{
|
||||
ID: uuid.New(),
|
||||
Handle: "test-handle-with-links",
|
||||
Hero: domainProfile.Hero{
|
||||
FirstName: "Bob",
|
||||
LastName: "Johnson",
|
||||
},
|
||||
Contact: domainProfile.Contact{
|
||||
SocialLinks: []domainProfile.SocialLink{
|
||||
{LinkType: "linkedin", Link: "https://linkedin.com/in/bob"},
|
||||
{LinkType: "github", Link: "https://github.com/bob"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := repo.Create(ctx, profile)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify profile with social links
|
||||
found, err := repo.FindByHandle(ctx, profile.Handle)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, found.Contact.SocialLinks, 2)
|
||||
assert.Equal(t, "linkedin", found.Contact.SocialLinks[0].LinkType)
|
||||
})
|
||||
|
||||
t.Run("create profile with achievements", func(t *testing.T) {
|
||||
profile := &domainProfile.Profile{
|
||||
ID: uuid.New(),
|
||||
Handle: "test-handle-with-achievements",
|
||||
Hero: domainProfile.Hero{
|
||||
FirstName: "Alice",
|
||||
LastName: "Williams",
|
||||
},
|
||||
About: domainProfile.About{
|
||||
Achievements: []domainProfile.Achievement{
|
||||
{Title: "Projects", Value: "50", Enabled: true},
|
||||
{Title: "Clients", Value: "100", Enabled: true},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := repo.Create(ctx, profile)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify profile with achievements
|
||||
found, err := repo.FindByHandle(ctx, profile.Handle)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, found.About.Achievements, 2)
|
||||
assert.Equal(t, "Projects", found.About.Achievements[0].Title)
|
||||
assert.Equal(t, "50", found.About.Achievements[0].Value)
|
||||
})
|
||||
|
||||
t.Run("create profile with duplicate handle fails", func(t *testing.T) {
|
||||
handle := "duplicate-handle"
|
||||
profile1 := &domainProfile.Profile{
|
||||
ID: uuid.New(),
|
||||
Handle: handle,
|
||||
Hero: domainProfile.Hero{
|
||||
FirstName: "First",
|
||||
LastName: "User",
|
||||
},
|
||||
}
|
||||
|
||||
err := repo.Create(ctx, profile1)
|
||||
assert.NoError(t, err)
|
||||
|
||||
profile2 := &domainProfile.Profile{
|
||||
ID: uuid.New(),
|
||||
Handle: handle,
|
||||
Hero: domainProfile.Hero{
|
||||
FirstName: "Second",
|
||||
LastName: "User",
|
||||
},
|
||||
}
|
||||
|
||||
err = repo.Create(ctx, profile2)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("create profile with role", func(t *testing.T) {
|
||||
roleID := uuid.New()
|
||||
roleName := "Software Engineer"
|
||||
profile := &domainProfile.Profile{
|
||||
ID: uuid.New(),
|
||||
Handle: "test-handle-with-role",
|
||||
Hero: domainProfile.Hero{
|
||||
Role: &domainProfile.Role{
|
||||
ID: roleID,
|
||||
Title: roleName,
|
||||
},
|
||||
FirstName: "Role",
|
||||
LastName: "User",
|
||||
},
|
||||
}
|
||||
|
||||
err := repo.Create(ctx, profile)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify profile with role
|
||||
found, err := repo.FindByHandle(ctx, profile.Handle)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, found.Hero.Role)
|
||||
assert.Equal(t, roleID, found.Hero.Role.ID)
|
||||
assert.Equal(t, roleName, found.Hero.Role.Title)
|
||||
})
|
||||
}
|
||||
|
||||
func TestProfileRepository_FindByHandle(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
repo := createTestProfileRepository(db)
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("find profile by handle successfully", func(t *testing.T) {
|
||||
profile := &domainProfile.Profile{
|
||||
ID: uuid.New(),
|
||||
Handle: "find-by-handle",
|
||||
Hero: domainProfile.Hero{
|
||||
FirstName: "Find",
|
||||
LastName: "Handle",
|
||||
},
|
||||
Contact: domainProfile.Contact{
|
||||
Email: "find@example.com",
|
||||
},
|
||||
}
|
||||
|
||||
err := repo.Create(ctx, profile)
|
||||
require.NoError(t, err)
|
||||
|
||||
found, err := repo.FindByHandle(ctx, profile.Handle)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, found)
|
||||
assert.Equal(t, profile.Handle, found.Handle)
|
||||
assert.Equal(t, profile.Hero.FirstName, found.Hero.FirstName)
|
||||
assert.Equal(t, profile.Contact.Email, found.Contact.Email)
|
||||
})
|
||||
|
||||
t.Run("find non-existent profile returns error", func(t *testing.T) {
|
||||
found, err := repo.FindByHandle(ctx, "non-existent-handle")
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, found)
|
||||
assert.Contains(t, err.Error(), "profile not found")
|
||||
})
|
||||
|
||||
t.Run("find profile with all related data", func(t *testing.T) {
|
||||
profile := &domainProfile.Profile{
|
||||
ID: uuid.New(),
|
||||
Handle: "find-with-all-data",
|
||||
Hero: domainProfile.Hero{
|
||||
FirstName: "All",
|
||||
LastName: "Data",
|
||||
},
|
||||
Skills: []domainProfile.Skill{
|
||||
{SkillName: "JavaScript", Level: "advanced"},
|
||||
},
|
||||
Contact: domainProfile.Contact{
|
||||
SocialLinks: []domainProfile.SocialLink{
|
||||
{LinkType: "twitter", Link: "https://twitter.com/user"},
|
||||
},
|
||||
},
|
||||
About: domainProfile.About{
|
||||
Achievements: []domainProfile.Achievement{
|
||||
{Title: "Years", Value: "10", Enabled: true},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := repo.Create(ctx, profile)
|
||||
require.NoError(t, err)
|
||||
|
||||
found, err := repo.FindByHandle(ctx, profile.Handle)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, found.Skills, 1)
|
||||
assert.Len(t, found.Contact.SocialLinks, 1)
|
||||
assert.Len(t, found.About.Achievements, 1)
|
||||
})
|
||||
}
|
||||
|
||||
func TestProfileRepository_FindByUserID(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
repo := createTestProfileRepository(db)
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("find profile by user ID successfully", func(t *testing.T) {
|
||||
userID := uuid.New()
|
||||
profile := &domainProfile.Profile{
|
||||
ID: uuid.New(),
|
||||
Handle: "find-by-user-id",
|
||||
Hero: domainProfile.Hero{
|
||||
FirstName: "User",
|
||||
LastName: "ID",
|
||||
},
|
||||
}
|
||||
|
||||
// Create profile with user_id manually since it's not in the domain model
|
||||
model, err := toProfileModel(profile)
|
||||
require.NoError(t, err)
|
||||
model.UserID = &userID
|
||||
err = db.WithContext(ctx).Create(model).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
found, err := repo.FindByUserID(ctx, userID)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, found)
|
||||
assert.Equal(t, profile.Handle, found.Handle)
|
||||
})
|
||||
|
||||
t.Run("find non-existent user ID returns error", func(t *testing.T) {
|
||||
nonExistentUserID := uuid.New()
|
||||
found, err := repo.FindByUserID(ctx, nonExistentUserID)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, found)
|
||||
assert.Contains(t, err.Error(), "profile not found")
|
||||
})
|
||||
}
|
||||
|
||||
func TestProfileRepository_Update(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
repo := createTestProfileRepository(db)
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("update profile successfully", func(t *testing.T) {
|
||||
profile := &domainProfile.Profile{
|
||||
ID: uuid.New(),
|
||||
Handle: "update-profile",
|
||||
Hero: domainProfile.Hero{
|
||||
FirstName: "Original",
|
||||
LastName: "Name",
|
||||
Company: "Old Company",
|
||||
},
|
||||
Contact: domainProfile.Contact{
|
||||
Email: "original@example.com",
|
||||
},
|
||||
}
|
||||
|
||||
err := repo.Create(ctx, profile)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Update profile
|
||||
profile.Hero.FirstName = "Updated"
|
||||
profile.Hero.Company = "New Company"
|
||||
profile.Contact.Email = "updated@example.com"
|
||||
|
||||
err = repo.Update(ctx, profile)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify update
|
||||
found, err := repo.FindByHandle(ctx, profile.Handle)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Updated", found.Hero.FirstName)
|
||||
assert.Equal(t, "New Company", found.Hero.Company)
|
||||
assert.Equal(t, "updated@example.com", found.Contact.Email)
|
||||
})
|
||||
|
||||
t.Run("update profile with new skills", func(t *testing.T) {
|
||||
profile := &domainProfile.Profile{
|
||||
ID: uuid.New(),
|
||||
Handle: "update-skills",
|
||||
Hero: domainProfile.Hero{
|
||||
FirstName: "Skills",
|
||||
LastName: "User",
|
||||
},
|
||||
Skills: []domainProfile.Skill{
|
||||
{SkillName: "Go", Level: "beginner"},
|
||||
},
|
||||
}
|
||||
|
||||
err := repo.Create(ctx, profile)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Update with new skills
|
||||
profile.Skills = []domainProfile.Skill{
|
||||
{SkillName: "Go", Level: "expert"},
|
||||
{SkillName: "Rust", Level: "intermediate"},
|
||||
}
|
||||
|
||||
err = repo.Update(ctx, profile)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify skills were updated
|
||||
found, err := repo.FindByHandle(ctx, profile.Handle)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, found.Skills, 2)
|
||||
// Check that old skill is gone and new ones exist
|
||||
skillMap := make(map[string]string)
|
||||
for _, skill := range found.Skills {
|
||||
skillMap[skill.SkillName] = skill.Level
|
||||
}
|
||||
assert.Equal(t, "expert", skillMap["Go"])
|
||||
assert.Equal(t, "intermediate", skillMap["Rust"])
|
||||
})
|
||||
|
||||
t.Run("update profile with new social links", func(t *testing.T) {
|
||||
profile := &domainProfile.Profile{
|
||||
ID: uuid.New(),
|
||||
Handle: "update-links",
|
||||
Hero: domainProfile.Hero{
|
||||
FirstName: "Links",
|
||||
LastName: "User",
|
||||
},
|
||||
Contact: domainProfile.Contact{
|
||||
SocialLinks: []domainProfile.SocialLink{
|
||||
{LinkType: "linkedin", Link: "https://linkedin.com/old"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := repo.Create(ctx, profile)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Update with new social links
|
||||
profile.Contact.SocialLinks = []domainProfile.SocialLink{
|
||||
{LinkType: "github", Link: "https://github.com/new"},
|
||||
{LinkType: "twitter", Link: "https://twitter.com/new"},
|
||||
}
|
||||
|
||||
err = repo.Update(ctx, profile)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify social links were updated
|
||||
found, err := repo.FindByHandle(ctx, profile.Handle)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, found.Contact.SocialLinks, 2)
|
||||
linkTypes := make(map[string]bool)
|
||||
for _, link := range found.Contact.SocialLinks {
|
||||
linkTypes[link.LinkType] = true
|
||||
}
|
||||
assert.True(t, linkTypes["github"])
|
||||
assert.True(t, linkTypes["twitter"])
|
||||
assert.False(t, linkTypes["linkedin"])
|
||||
})
|
||||
|
||||
t.Run("update profile with new achievements", func(t *testing.T) {
|
||||
profile := &domainProfile.Profile{
|
||||
ID: uuid.New(),
|
||||
Handle: "update-achievements",
|
||||
Hero: domainProfile.Hero{
|
||||
FirstName: "Achievements",
|
||||
LastName: "User",
|
||||
},
|
||||
About: domainProfile.About{
|
||||
Achievements: []domainProfile.Achievement{
|
||||
{Title: "Old", Value: "1", Enabled: true},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := repo.Create(ctx, profile)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Update with new achievements
|
||||
profile.About.Achievements = []domainProfile.Achievement{
|
||||
{Title: "New1", Value: "10", Enabled: true},
|
||||
{Title: "New2", Value: "20", Enabled: false},
|
||||
}
|
||||
|
||||
err = repo.Update(ctx, profile)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify achievements were updated
|
||||
found, err := repo.FindByHandle(ctx, profile.Handle)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, found.About.Achievements, 2)
|
||||
achievementMap := make(map[string]string)
|
||||
for _, achievement := range found.About.Achievements {
|
||||
achievementMap[achievement.Title] = achievement.Value
|
||||
}
|
||||
assert.Equal(t, "10", achievementMap["New1"])
|
||||
assert.Equal(t, "20", achievementMap["New2"])
|
||||
})
|
||||
|
||||
t.Run("update profile page section order", func(t *testing.T) {
|
||||
profile := &domainProfile.Profile{
|
||||
ID: uuid.New(),
|
||||
Handle: "update-page-order",
|
||||
Hero: domainProfile.Hero{
|
||||
FirstName: "Page",
|
||||
LastName: "Order",
|
||||
},
|
||||
PageSectionOrder: map[string]int{
|
||||
"hero": 1,
|
||||
},
|
||||
}
|
||||
|
||||
err := repo.Create(ctx, profile)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Update page section order
|
||||
profile.PageSectionOrder = map[string]int{
|
||||
"about": 1,
|
||||
"hero": 2,
|
||||
"skills": 3,
|
||||
}
|
||||
|
||||
err = repo.Update(ctx, profile)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify page section order was updated
|
||||
found, err := repo.FindByHandle(ctx, profile.Handle)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, found.PageSectionOrder["about"])
|
||||
assert.Equal(t, 2, found.PageSectionOrder["hero"])
|
||||
assert.Equal(t, 3, found.PageSectionOrder["skills"])
|
||||
})
|
||||
}
|
||||
|
||||
func TestProfileRepository_Delete(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
repo := createTestProfileRepository(db)
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("delete profile successfully", func(t *testing.T) {
|
||||
profile := &domainProfile.Profile{
|
||||
ID: uuid.New(),
|
||||
Handle: "delete-profile",
|
||||
Hero: domainProfile.Hero{
|
||||
FirstName: "Delete",
|
||||
LastName: "User",
|
||||
},
|
||||
Skills: []domainProfile.Skill{
|
||||
{SkillName: "Go", Level: "expert"},
|
||||
},
|
||||
Contact: domainProfile.Contact{
|
||||
SocialLinks: []domainProfile.SocialLink{
|
||||
{LinkType: "github", Link: "https://github.com/user"},
|
||||
},
|
||||
},
|
||||
About: domainProfile.About{
|
||||
Achievements: []domainProfile.Achievement{
|
||||
{Title: "Projects", Value: "10", Enabled: true},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := repo.Create(ctx, profile)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = repo.Delete(ctx, profile)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify deletion
|
||||
found, err := repo.FindByHandle(ctx, profile.Handle)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, found)
|
||||
assert.Contains(t, err.Error(), "profile not found")
|
||||
})
|
||||
}
|
||||
|
||||
func TestProfileRepository_FindAll(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
repo := createTestProfileRepository(db)
|
||||
ctx := context.Background()
|
||||
|
||||
// Create test profiles
|
||||
roleID1 := uuid.New()
|
||||
roleID2 := uuid.New()
|
||||
|
||||
profiles := []*domainProfile.Profile{
|
||||
{
|
||||
ID: uuid.New(),
|
||||
Handle: "findall-1",
|
||||
Hero: domainProfile.Hero{
|
||||
Role: &domainProfile.Role{
|
||||
ID: roleID1,
|
||||
Title: "Engineer",
|
||||
},
|
||||
FirstName: "Alice",
|
||||
LastName: "Anderson",
|
||||
Company: "Tech Corp",
|
||||
},
|
||||
Skills: []domainProfile.Skill{
|
||||
{SkillName: "Go", Level: "expert"},
|
||||
{SkillName: "Python", Level: "intermediate"},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: uuid.New(),
|
||||
Handle: "findall-2",
|
||||
Hero: domainProfile.Hero{
|
||||
Role: &domainProfile.Role{
|
||||
ID: roleID1,
|
||||
Title: "Engineer",
|
||||
},
|
||||
FirstName: "Bob",
|
||||
LastName: "Brown",
|
||||
Company: "Tech Corp",
|
||||
},
|
||||
Skills: []domainProfile.Skill{
|
||||
{SkillName: "JavaScript", Level: "expert"},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: uuid.New(),
|
||||
Handle: "findall-3",
|
||||
Hero: domainProfile.Hero{
|
||||
Role: &domainProfile.Role{
|
||||
ID: roleID2,
|
||||
Title: "Designer",
|
||||
},
|
||||
FirstName: "Charlie",
|
||||
LastName: "Clark",
|
||||
Company: "Design Inc",
|
||||
},
|
||||
Skills: []domainProfile.Skill{
|
||||
{SkillName: "Figma", Level: "expert"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, profile := range profiles {
|
||||
err := repo.Create(ctx, profile)
|
||||
require.NoError(t, err)
|
||||
// Add small delay to ensure different timestamps
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
|
||||
t.Run("find all profiles without filters", func(t *testing.T) {
|
||||
filter := domainProfile.Filter{
|
||||
Page: 1,
|
||||
PageSize: 10,
|
||||
}
|
||||
|
||||
results, total, err := repo.FindAll(ctx, filter)
|
||||
assert.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, total, 3)
|
||||
assert.GreaterOrEqual(t, len(results), 3)
|
||||
})
|
||||
|
||||
t.Run("find profiles by role ID", func(t *testing.T) {
|
||||
filter := domainProfile.Filter{
|
||||
RoleID: roleID1,
|
||||
Page: 1,
|
||||
PageSize: 10,
|
||||
}
|
||||
|
||||
results, total, err := repo.FindAll(ctx, filter)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 2, total)
|
||||
assert.Len(t, results, 2)
|
||||
for _, result := range results {
|
||||
assert.NotNil(t, result.Hero.Role)
|
||||
assert.Equal(t, roleID1, result.Hero.Role.ID)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("find profiles by first name", func(t *testing.T) {
|
||||
filter := domainProfile.Filter{
|
||||
FirstName: "alice",
|
||||
Page: 1,
|
||||
PageSize: 10,
|
||||
}
|
||||
|
||||
results, total, err := repo.FindAll(ctx, filter)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, total)
|
||||
assert.Len(t, results, 1)
|
||||
assert.Equal(t, "Alice", results[0].Hero.FirstName)
|
||||
})
|
||||
|
||||
t.Run("find profiles by last name", func(t *testing.T) {
|
||||
filter := domainProfile.Filter{
|
||||
LastName: "brown",
|
||||
Page: 1,
|
||||
PageSize: 10,
|
||||
}
|
||||
|
||||
results, total, err := repo.FindAll(ctx, filter)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, total)
|
||||
assert.Len(t, results, 1)
|
||||
assert.Equal(t, "Brown", results[0].Hero.LastName)
|
||||
})
|
||||
|
||||
t.Run("find profiles by company", func(t *testing.T) {
|
||||
filter := domainProfile.Filter{
|
||||
Company: "tech",
|
||||
Page: 1,
|
||||
PageSize: 10,
|
||||
}
|
||||
|
||||
results, total, err := repo.FindAll(ctx, filter)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 2, total)
|
||||
assert.Len(t, results, 2)
|
||||
for _, result := range results {
|
||||
assert.Contains(t, result.Hero.Company, "Tech")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("find profiles by skill name", func(t *testing.T) {
|
||||
filter := domainProfile.Filter{
|
||||
SkillName: "go",
|
||||
Page: 1,
|
||||
PageSize: 10,
|
||||
}
|
||||
|
||||
results, total, err := repo.FindAll(ctx, filter)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, total)
|
||||
assert.Len(t, results, 1)
|
||||
assert.Equal(t, "findall-1", results[0].Handle)
|
||||
// Verify the profile has the skill
|
||||
hasGoSkill := false
|
||||
for _, skill := range results[0].Skills {
|
||||
if skill.SkillName == "Go" {
|
||||
hasGoSkill = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(t, hasGoSkill)
|
||||
})
|
||||
|
||||
t.Run("find profiles with pagination", func(t *testing.T) {
|
||||
filter := domainProfile.Filter{
|
||||
Page: 1,
|
||||
PageSize: 2,
|
||||
}
|
||||
|
||||
results, total, err := repo.FindAll(ctx, filter)
|
||||
assert.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, total, 3)
|
||||
assert.Len(t, results, 2)
|
||||
|
||||
// Second page
|
||||
filter.Page = 2
|
||||
results2, total2, err := repo.FindAll(ctx, filter)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, total, total2)
|
||||
assert.GreaterOrEqual(t, len(results2), 1)
|
||||
})
|
||||
|
||||
t.Run("find profiles with sorting", func(t *testing.T) {
|
||||
filter := domainProfile.Filter{
|
||||
Page: 1,
|
||||
PageSize: 10,
|
||||
SortedBy: "first_name",
|
||||
Ascending: true,
|
||||
}
|
||||
|
||||
results, total, err := repo.FindAll(ctx, filter)
|
||||
assert.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, total, 3)
|
||||
assert.GreaterOrEqual(t, len(results), 3)
|
||||
// Verify sorting (first result should be Alice)
|
||||
assert.Equal(t, "Alice", results[0].Hero.FirstName)
|
||||
|
||||
// Test descending order
|
||||
filter.Ascending = false
|
||||
results2, _, err := repo.FindAll(ctx, filter)
|
||||
assert.NoError(t, err)
|
||||
// Last result should be Alice (or one of the first names alphabetically)
|
||||
assert.NotEqual(t, "Alice", results2[0].Hero.FirstName)
|
||||
})
|
||||
|
||||
t.Run("find profiles with combined filters", func(t *testing.T) {
|
||||
filter := domainProfile.Filter{
|
||||
RoleID: roleID1,
|
||||
Company: "tech",
|
||||
Page: 1,
|
||||
PageSize: 10,
|
||||
}
|
||||
|
||||
results, total, err := repo.FindAll(ctx, filter)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 2, total)
|
||||
assert.Len(t, results, 2)
|
||||
for _, result := range results {
|
||||
assert.NotNil(t, result.Hero.Role)
|
||||
assert.Equal(t, roleID1, result.Hero.Role.ID)
|
||||
assert.Contains(t, result.Hero.Company, "Tech")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("find profiles with empty result", func(t *testing.T) {
|
||||
filter := domainProfile.Filter{
|
||||
FirstName: "nonexistent",
|
||||
Page: 1,
|
||||
PageSize: 10,
|
||||
}
|
||||
|
||||
results, total, err := repo.FindAll(ctx, filter)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, total)
|
||||
assert.Len(t, results, 0)
|
||||
})
|
||||
}
|
||||
|
||||
func TestProfileRepository_PageSectionOrder(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
repo := createTestProfileRepository(db)
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("create and retrieve profile with page section order", func(t *testing.T) {
|
||||
pageSectionOrder := map[string]int{
|
||||
"hero": 1,
|
||||
"about": 2,
|
||||
"skills": 3,
|
||||
"contact": 4,
|
||||
}
|
||||
|
||||
profile := &domainProfile.Profile{
|
||||
ID: uuid.New(),
|
||||
Handle: "page-order-test",
|
||||
PageSectionOrder: pageSectionOrder,
|
||||
Hero: domainProfile.Hero{
|
||||
FirstName: "Page",
|
||||
LastName: "Order",
|
||||
},
|
||||
}
|
||||
|
||||
err := repo.Create(ctx, profile)
|
||||
assert.NoError(t, err)
|
||||
|
||||
found, err := repo.FindByHandle(ctx, profile.Handle)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, found.PageSectionOrder)
|
||||
assert.Equal(t, 1, found.PageSectionOrder["hero"])
|
||||
assert.Equal(t, 2, found.PageSectionOrder["about"])
|
||||
assert.Equal(t, 3, found.PageSectionOrder["skills"])
|
||||
assert.Equal(t, 4, found.PageSectionOrder["contact"])
|
||||
})
|
||||
|
||||
t.Run("create profile with empty page section order", func(t *testing.T) {
|
||||
profile := &domainProfile.Profile{
|
||||
ID: uuid.New(),
|
||||
Handle: "empty-page-order",
|
||||
Hero: domainProfile.Hero{
|
||||
FirstName: "Empty",
|
||||
LastName: "Order",
|
||||
},
|
||||
}
|
||||
|
||||
err := repo.Create(ctx, profile)
|
||||
assert.NoError(t, err)
|
||||
|
||||
found, err := repo.FindByHandle(ctx, profile.Handle)
|
||||
assert.NoError(t, err)
|
||||
// Empty map should be returned as empty or nil
|
||||
assert.NotNil(t, found)
|
||||
})
|
||||
}
|
||||
|
||||
// Helper function to verify JSON marshaling/unmarshaling works correctly
|
||||
func TestProfileRepository_JSONSerialization(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
repo := createTestProfileRepository(db)
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("verify page section order JSON serialization", func(t *testing.T) {
|
||||
complexOrder := map[string]int{
|
||||
"section1": 10,
|
||||
"section2": 20,
|
||||
"section3": 30,
|
||||
}
|
||||
|
||||
profile := &domainProfile.Profile{
|
||||
ID: uuid.New(),
|
||||
Handle: "json-test",
|
||||
PageSectionOrder: complexOrder,
|
||||
Hero: domainProfile.Hero{
|
||||
FirstName: "JSON",
|
||||
LastName: "Test",
|
||||
},
|
||||
}
|
||||
|
||||
err := repo.Create(ctx, profile)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify the data can be serialized/deserialized correctly
|
||||
found, err := repo.FindByHandle(ctx, profile.Handle)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Re-serialize to verify round-trip
|
||||
jsonData, err := json.Marshal(found.PageSectionOrder)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var unmarshaled map[string]int
|
||||
err = json.Unmarshal(jsonData, &unmarshaled)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, complexOrder, unmarshaled)
|
||||
})
|
||||
}
|
||||
|
||||
112
internal/repository/postgres/profile/role.go
Normal file
112
internal/repository/postgres/profile/role.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package profile
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"go.uber.org/fx"
|
||||
"gorm.io/gorm"
|
||||
|
||||
domainProfile "base/internal/domain/profile"
|
||||
)
|
||||
|
||||
type roleRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewRoleRepository creates a RoleRepository for profile_roles.
|
||||
func NewRoleRepository(lc fx.Lifecycle, db *gorm.DB) domainProfile.RoleRepository {
|
||||
lc.Append(
|
||||
fx.Hook{
|
||||
OnStart: func(ctx context.Context) error { return nil },
|
||||
OnStop: func(ctx context.Context) error { return nil },
|
||||
})
|
||||
return &roleRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *roleRepository) FindByID(ctx context.Context, id uuid.UUID) (*domainProfile.Role, error) {
|
||||
var model RoleModel
|
||||
if err := r.db.WithContext(ctx).Where("id = ?", id).First(&model).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, domainProfile.ErrRoleNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return roleModelToDomain(&model), nil
|
||||
}
|
||||
|
||||
func (r *roleRepository) FindAll(ctx context.Context) ([]*domainProfile.Role, error) {
|
||||
var models []RoleModel
|
||||
if err := r.db.WithContext(ctx).Order("status DESC, title ASC").Find(&models).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]*domainProfile.Role, len(models))
|
||||
for i := range models {
|
||||
out[i] = roleModelToDomain(&models[i])
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (r *roleRepository) List(ctx context.Context, limit, offset int) ([]*domainProfile.Role, error) {
|
||||
var models []RoleModel
|
||||
q := r.db.WithContext(ctx).Order("status DESC, title ASC")
|
||||
if limit > 0 {
|
||||
q = q.Limit(limit)
|
||||
}
|
||||
if offset > 0 {
|
||||
q = q.Offset(offset)
|
||||
}
|
||||
if err := q.Find(&models).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]*domainProfile.Role, len(models))
|
||||
for i := range models {
|
||||
out[i] = roleModelToDomain(&models[i])
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func roleModelToDomain(m *RoleModel) *domainProfile.Role {
|
||||
return &domainProfile.Role{
|
||||
ID: m.ID,
|
||||
Title: m.Title,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *roleRepository) Create(ctx context.Context, role *domainProfile.Role) error {
|
||||
now := time.Now()
|
||||
model := &RoleModel{
|
||||
ID: role.ID,
|
||||
Title: role.Title,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
if err := r.db.WithContext(ctx).Create(model).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *roleRepository) Update(ctx context.Context, role *domainProfile.Role) error {
|
||||
result := r.db.WithContext(ctx).Model(&RoleModel{}).Where("id = ?", role.ID).Updates(map[string]interface{}{"title": role.Title})
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return domainProfile.ErrRoleNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *roleRepository) Delete(ctx context.Context, id uuid.UUID) error {
|
||||
result := r.db.WithContext(ctx).Delete(&RoleModel{}, "id = ?", id)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return domainProfile.ErrRoleNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
134
internal/repository/postgres/profile/role_mock.go
Normal file
134
internal/repository/postgres/profile/role_mock.go
Normal file
@@ -0,0 +1,134 @@
|
||||
package profile
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
domainProfile "base/internal/domain/profile"
|
||||
)
|
||||
|
||||
// mockRoleData holds the mocked profile roles (matches seed data).
|
||||
var mockRoleData = []*domainProfile.Role{
|
||||
{ID: uuid.MustParse("0199b964-5dc0-7657-9178-2a844e23e5b5"), Title: "Data Scientist"},
|
||||
{ID: uuid.MustParse("0199b964-5dc0-7a1a-94c7-d68daf420e50"), Title: "Machine Learning Engineer"},
|
||||
{ID: uuid.MustParse("0199b964-5dc0-7759-8221-71f57f5b2b57"), Title: "AI Engineer"},
|
||||
{ID: uuid.MustParse("0199b964-5dc0-7b79-a268-331f39c35366"), Title: "Data Engineer"},
|
||||
{ID: uuid.MustParse("0199b964-5dc0-7062-b219-11733a1ab94b"), Title: "Data Analyst"},
|
||||
{ID: uuid.MustParse("0199b964-5dc0-7434-b105-f2ff49573fe2"), Title: "Business Intelligence Developer"},
|
||||
{ID: uuid.MustParse("0199b964-5dc0-77f8-be02-f76937f60ba6"), Title: "MLOps Engineer"},
|
||||
{ID: uuid.MustParse("0199b964-5dc0-7107-907c-6c013cbc08b9"), Title: "AI Product Manager"},
|
||||
{ID: uuid.MustParse("0199b964-5dc0-72f9-8e0f-dfa2950a8182"), Title: "AI Research Scientist"},
|
||||
{ID: uuid.MustParse("0199b964-5dc0-7177-829b-f3d05081201e"), Title: "Computer Vision Engineer"},
|
||||
{ID: uuid.MustParse("0199b964-5dc0-74b7-b427-a500ddb9f435"), Title: "NLP Engineer"},
|
||||
{ID: uuid.MustParse("0199b964-5dc0-780d-876f-a7b4d15b0ef5"), Title: "Data Architect"},
|
||||
{ID: uuid.MustParse("0199b964-5dc0-7d3f-af44-19dc33f50b21"), Title: "Big Data Engineer"},
|
||||
{ID: uuid.MustParse("0199b964-5dc0-7600-9a16-74f17be7ce4b"), Title: "Cloud AI/ML Specialist"},
|
||||
{ID: uuid.MustParse("0199b964-5dc0-73c2-b9a0-78347ae945d7"), Title: "Generative AI Specialist"},
|
||||
{ID: uuid.MustParse("0199b964-5dc0-70a8-b710-1f424a776083"), Title: "AI Ethics Officer"},
|
||||
{ID: uuid.MustParse("0199b964-5dc0-7c87-91c0-348e6f8b43d6"), Title: "AI Governance Manager"},
|
||||
{ID: uuid.MustParse("0199b964-5dc0-7441-b306-bc2e3d4e4152"), Title: "Data Privacy Engineer"},
|
||||
{ID: uuid.MustParse("0199b964-5dc0-747f-97b4-c4d98a257dee"), Title: "AI Solutions Architect"},
|
||||
{ID: uuid.MustParse("0199b964-5dc0-7fa5-8fe0-9eb7831554ed"), Title: "Chief Data & AI Officer"},
|
||||
{ID: uuid.MustParse("0199b964-5dc0-7447-8785-f246ff9ec309"), Title: "AI Developer Advocate"},
|
||||
{ID: uuid.MustParse("0199b964-5dc0-7b24-9b1b-c7ca8f08527f"), Title: "AI/ML Educator & Trainer"},
|
||||
{ID: uuid.MustParse("0199b964-5dc0-756f-ab44-48169ecfbb5e"), Title: "Technical Content Creator (AI/ML)"},
|
||||
{ID: uuid.MustParse("0199b964-5dc0-79d1-9086-c809d8989cac"), Title: "Open Source AI Contributor"},
|
||||
{ID: uuid.MustParse("0199b964-5dc0-774e-9011-b9fe6c29f52f"), Title: "AI Course Instructor (Udemy, Coursera, etc.)"},
|
||||
{ID: uuid.MustParse("0199b964-5dc0-7f1d-80a4-96810af9f9ac"), Title: "AI Community Manager"},
|
||||
{ID: uuid.MustParse("0199b964-5dc0-7352-8553-edd37324ffd9"), Title: "AI Evangelist"},
|
||||
{ID: uuid.MustParse("0199b964-5dc0-7864-a2b5-473cfd8f7aa0"), Title: "Research Engineer (applied AI research, publishing GitHub repos)"},
|
||||
{ID: uuid.MustParse("0199b964-5dc0-762e-9a40-0cc112578498"), Title: "Kaggle Competitor / Data Science Challenger"},
|
||||
{ID: uuid.MustParse("0199b964-5dc0-7e13-a1f4-b4ae76bb0b62"), Title: "AI Startup Founder / Indie Hacker (building projects, sharing repos)"},
|
||||
{ID: uuid.MustParse("0199b964-5dc0-7035-bf9b-deb415d852fd"), Title: "Freelancer"},
|
||||
{ID: uuid.MustParse("0199b964-5dc0-7702-b533-72f7c93e19d3"), Title: "Other"},
|
||||
}
|
||||
|
||||
// mockRoleRepository returns mocked profile roles (no DB).
|
||||
type mockRoleRepository struct {
|
||||
mu sync.RWMutex
|
||||
data []*domainProfile.Role
|
||||
}
|
||||
|
||||
// NewMockRoleRepository creates a RoleRepository that returns mocked data.
|
||||
func NewMockRoleRepository() domainProfile.RoleRepository {
|
||||
data := make([]*domainProfile.Role, len(mockRoleData))
|
||||
for i, r := range mockRoleData {
|
||||
data[i] = &domainProfile.Role{ID: r.ID, Title: r.Title}
|
||||
}
|
||||
return &mockRoleRepository{data: data}
|
||||
}
|
||||
|
||||
func (r *mockRoleRepository) FindByID(ctx context.Context, id uuid.UUID) (*domainProfile.Role, error) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
for _, role := range r.data {
|
||||
if role.ID == id {
|
||||
return &domainProfile.Role{ID: role.ID, Title: role.Title}, nil
|
||||
}
|
||||
}
|
||||
return nil, domainProfile.ErrRoleNotFound
|
||||
}
|
||||
|
||||
func (r *mockRoleRepository) FindAll(ctx context.Context) ([]*domainProfile.Role, error) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
out := make([]*domainProfile.Role, len(r.data))
|
||||
for i, role := range r.data {
|
||||
out[i] = &domainProfile.Role{ID: role.ID, Title: role.Title}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (r *mockRoleRepository) List(ctx context.Context, limit, offset int) ([]*domainProfile.Role, error) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
start := offset
|
||||
if start > len(r.data) {
|
||||
start = len(r.data)
|
||||
}
|
||||
end := start + limit
|
||||
if limit <= 0 {
|
||||
end = len(r.data)
|
||||
} else if end > len(r.data) {
|
||||
end = len(r.data)
|
||||
}
|
||||
slice := r.data[start:end]
|
||||
out := make([]*domainProfile.Role, len(slice))
|
||||
for i, role := range slice {
|
||||
out[i] = &domainProfile.Role{ID: role.ID, Title: role.Title}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (r *mockRoleRepository) Create(ctx context.Context, role *domainProfile.Role) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
r.data = append(r.data, &domainProfile.Role{ID: role.ID, Title: role.Title})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *mockRoleRepository) Update(ctx context.Context, role *domainProfile.Role) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
for i, existing := range r.data {
|
||||
if existing.ID == role.ID {
|
||||
r.data[i] = &domainProfile.Role{ID: role.ID, Title: role.Title}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return domainProfile.ErrRoleNotFound
|
||||
}
|
||||
|
||||
func (r *mockRoleRepository) Delete(ctx context.Context, id uuid.UUID) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
for i, role := range r.data {
|
||||
if role.ID == id {
|
||||
r.data = append(r.data[:i], r.data[i+1:]...)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return domainProfile.ErrRoleNotFound
|
||||
}
|
||||
106
internal/repository/postgres/profile/schema.go
Normal file
106
internal/repository/postgres/profile/schema.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package profile
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()"`
|
||||
UserID *uuid.UUID `gorm:"column:user_id;type:uuid;index:profiles_user_id_idx"`
|
||||
Handle string `gorm:"column:handle;type:text;not null;uniqueIndex:profiles_handle_unique"`
|
||||
|
||||
// Hero fields (normalized for search)
|
||||
RoleID *uuid.UUID `gorm:"column:role_id;type:uuid;index:profiles_role_id_idx"`
|
||||
Role *RoleModel `gorm:"foreignKey:RoleID"`
|
||||
RoleName *string `gorm:"column:role_name;type:varchar(100)"` // denormalized fallback
|
||||
RoleLevel string `gorm:"column:role_level;type:text"`
|
||||
FirstName string `gorm:"column:first_name;type:text;index:profiles_name_idx"`
|
||||
LastName string `gorm:"column:last_name;type:text;index:profiles_name_idx"`
|
||||
Company string `gorm:"column:company;type:text;index:profiles_company_idx"`
|
||||
ShortDescription string `gorm:"column:short_description;type:text"`
|
||||
ResumeLink string `gorm:"column:resume_link;type:text"`
|
||||
CTAEnabled bool `gorm:"column:cta_enabled;type:boolean;default:false"`
|
||||
Avatar string `gorm:"column:avatar;type:text"`
|
||||
|
||||
// About fields (normalized for search)
|
||||
ProfilePicture string `gorm:"column:profile_picture;type:text"`
|
||||
About string `gorm:"column:about;type:text"`
|
||||
|
||||
// Contact fields (normalized for search)
|
||||
Email string `gorm:"column:email;type:text;index:profiles_email_idx"`
|
||||
Phone string `gorm:"column:phone;type:text"`
|
||||
|
||||
// PageSetting fields (normalized)
|
||||
VisibilityLevel string `gorm:"column:visibility_level;type:text;default:'public'"`
|
||||
|
||||
// Complex/non-searchable data stored as JSONB
|
||||
PageSectionOrder json.RawMessage `gorm:"column:page_section_order;type:jsonb"`
|
||||
|
||||
CreatedAt time.Time `gorm:"column:created_at;type:timestamptz;not null"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamptz;not null"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;type:timestamptz;index"`
|
||||
}
|
||||
|
||||
func (Model) TableName() string {
|
||||
return "profiles"
|
||||
}
|
||||
|
||||
type SkillModel struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()"`
|
||||
ProfileID uuid.UUID `gorm:"column:profile_id;type:uuid;not null;index:skills_profile_id_idx"`
|
||||
SkillName string `gorm:"column:skill_name;type:text;not null;index:skills_name_idx"`
|
||||
Level string `gorm:"column:level;type:text;not null"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;type:timestamptz;not null"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamptz;not null"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;type:timestamptz;index"`
|
||||
}
|
||||
|
||||
func (SkillModel) TableName() string {
|
||||
return "profile_skills"
|
||||
}
|
||||
|
||||
type SocialLinkModel struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()"`
|
||||
ProfileID uuid.UUID `gorm:"column:profile_id;type:uuid;not null;index:social_links_profile_id_idx"`
|
||||
LinkType string `gorm:"column:link_type;type:text;not null"`
|
||||
Link string `gorm:"column:link;type:text;not null"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;type:timestamptz;not null"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamptz;not null"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;type:timestamptz;index"`
|
||||
}
|
||||
|
||||
func (SocialLinkModel) TableName() string {
|
||||
return "profile_social_links"
|
||||
}
|
||||
|
||||
type AchievementModel struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()"`
|
||||
ProfileID uuid.UUID `gorm:"column:profile_id;type:uuid;not null;index:achievements_profile_id_idx"`
|
||||
Title string `gorm:"column:title;type:text;not null"`
|
||||
Value string `gorm:"column:value;type:text;not null"`
|
||||
Enabled bool `gorm:"column:enabled;type:boolean;default:true"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;type:timestamptz;not null"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamptz;not null"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;type:timestamptz;index"`
|
||||
}
|
||||
|
||||
func (AchievementModel) TableName() string {
|
||||
return "profile_achievements"
|
||||
}
|
||||
|
||||
// RoleModel maps profile_roles table (profiles.role_id references this)
|
||||
type RoleModel struct {
|
||||
ID uuid.UUID `gorm:"column:id;type:uuid;primaryKey"`
|
||||
Title string `gorm:"column:title;type:text;not null"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;type:timestamptz;not null"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamptz;not null"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;type:timestamptz;index"`
|
||||
}
|
||||
|
||||
func (RoleModel) TableName() string {
|
||||
return "profile_roles"
|
||||
}
|
||||
107
internal/repository/postgres/profile/test_helper.go
Normal file
107
internal/repository/postgres/profile/test_helper.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package profile
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
|
||||
domainProfile "base/internal/domain/profile"
|
||||
)
|
||||
|
||||
// setupTestDB creates an in-memory SQLite database for testing
|
||||
func setupTestDB(t *testing.T) *gorm.DB {
|
||||
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
|
||||
DisableForeignKeyConstraintWhenMigrating: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create tables manually with SQLite-compatible syntax
|
||||
// This avoids PostgreSQL-specific syntax like gen_random_uuid() and timestamptz
|
||||
|
||||
createProfilesTable := `
|
||||
CREATE TABLE IF NOT EXISTS profiles (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT,
|
||||
handle TEXT NOT NULL,
|
||||
role_id TEXT,
|
||||
role_name TEXT,
|
||||
first_name TEXT,
|
||||
last_name TEXT,
|
||||
company TEXT,
|
||||
short_description TEXT,
|
||||
resume_link TEXT,
|
||||
cta_enabled INTEGER NOT NULL DEFAULT 0,
|
||||
avatar TEXT,
|
||||
profile_picture TEXT,
|
||||
about TEXT,
|
||||
email TEXT,
|
||||
phone TEXT,
|
||||
visibility_level TEXT NOT NULL DEFAULT 'public',
|
||||
page_section_order TEXT,
|
||||
created_at DATETIME NOT NULL,
|
||||
updated_at DATETIME NOT NULL,
|
||||
deleted_at DATETIME,
|
||||
UNIQUE(handle)
|
||||
)
|
||||
`
|
||||
require.NoError(t, db.Exec(createProfilesTable).Error)
|
||||
require.NoError(t, db.Exec("CREATE INDEX IF NOT EXISTS profiles_user_id_idx ON profiles(user_id)").Error)
|
||||
require.NoError(t, db.Exec("CREATE INDEX IF NOT EXISTS profiles_role_id_idx ON profiles(role_id)").Error)
|
||||
require.NoError(t, db.Exec("CREATE INDEX IF NOT EXISTS profiles_name_idx ON profiles(first_name, last_name)").Error)
|
||||
require.NoError(t, db.Exec("CREATE INDEX IF NOT EXISTS profiles_company_idx ON profiles(company)").Error)
|
||||
require.NoError(t, db.Exec("CREATE INDEX IF NOT EXISTS profiles_email_idx ON profiles(email)").Error)
|
||||
|
||||
createProfileSkillsTable := `
|
||||
CREATE TABLE IF NOT EXISTS profile_skills (
|
||||
id TEXT PRIMARY KEY,
|
||||
profile_id TEXT NOT NULL,
|
||||
skill_name TEXT NOT NULL,
|
||||
level TEXT NOT NULL,
|
||||
created_at DATETIME NOT NULL,
|
||||
updated_at DATETIME NOT NULL,
|
||||
deleted_at DATETIME
|
||||
)
|
||||
`
|
||||
require.NoError(t, db.Exec(createProfileSkillsTable).Error)
|
||||
require.NoError(t, db.Exec("CREATE INDEX IF NOT EXISTS skills_profile_id_idx ON profile_skills(profile_id)").Error)
|
||||
require.NoError(t, db.Exec("CREATE INDEX IF NOT EXISTS skills_name_idx ON profile_skills(skill_name)").Error)
|
||||
|
||||
createProfileSocialLinksTable := `
|
||||
CREATE TABLE IF NOT EXISTS profile_social_links (
|
||||
id TEXT PRIMARY KEY,
|
||||
profile_id TEXT NOT NULL,
|
||||
link_type TEXT NOT NULL,
|
||||
link TEXT NOT NULL,
|
||||
created_at DATETIME NOT NULL,
|
||||
updated_at DATETIME NOT NULL,
|
||||
deleted_at DATETIME
|
||||
)
|
||||
`
|
||||
require.NoError(t, db.Exec(createProfileSocialLinksTable).Error)
|
||||
require.NoError(t, db.Exec("CREATE INDEX IF NOT EXISTS social_links_profile_id_idx ON profile_social_links(profile_id)").Error)
|
||||
|
||||
createProfileAchievementsTable := `
|
||||
CREATE TABLE IF NOT EXISTS profile_achievements (
|
||||
id TEXT PRIMARY KEY,
|
||||
profile_id TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
created_at DATETIME NOT NULL,
|
||||
updated_at DATETIME NOT NULL,
|
||||
deleted_at DATETIME
|
||||
)
|
||||
`
|
||||
require.NoError(t, db.Exec(createProfileAchievementsTable).Error)
|
||||
require.NoError(t, db.Exec("CREATE INDEX IF NOT EXISTS achievements_profile_id_idx ON profile_achievements(profile_id)").Error)
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
// createTestProfileRepository creates a profile repository for testing
|
||||
func createTestProfileRepository(db *gorm.DB) domainProfile.Repository {
|
||||
return &profileRepository{db: db}
|
||||
}
|
||||
|
||||
20
internal/repository/postgres/skill/model.go
Normal file
20
internal/repository/postgres/skill/model.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package skill
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type SkillModel struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()"`
|
||||
Name string `gorm:"column:name;type:text;not null"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;type:timestamptz;not null"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamptz;not null"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;type:timestamptz;index"`
|
||||
}
|
||||
|
||||
func (SkillModel) TableName() string {
|
||||
return "skills"
|
||||
}
|
||||
49
internal/repository/postgres/skill/repository.go
Normal file
49
internal/repository/postgres/skill/repository.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package skill
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"go.uber.org/fx"
|
||||
"gorm.io/gorm"
|
||||
|
||||
domainSkill "base/internal/domain/skill"
|
||||
)
|
||||
|
||||
type repository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewRepository creates a Repository for the skills catalog.
|
||||
func NewRepository(lc fx.Lifecycle, db *gorm.DB) domainSkill.Repository {
|
||||
lc.Append(
|
||||
fx.Hook{
|
||||
OnStart: func(ctx context.Context) error { return nil },
|
||||
OnStop: func(ctx context.Context) error { return nil },
|
||||
})
|
||||
return &repository{db: db}
|
||||
}
|
||||
|
||||
func (r *repository) FindAll(ctx context.Context) ([]*domainSkill.Skill, error) {
|
||||
var models []SkillModel
|
||||
if err := r.db.WithContext(ctx).Order("name ASC").Find(&models).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]*domainSkill.Skill, len(models))
|
||||
for i := range models {
|
||||
out[i] = &domainSkill.Skill{ID: models[i].ID, Name: models[i].Name}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (r *repository) FindByID(ctx context.Context, id uuid.UUID) (*domainSkill.Skill, error) {
|
||||
var model SkillModel
|
||||
if err := r.db.WithContext(ctx).Where("id = ?", id).First(&model).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &domainSkill.Skill{ID: model.ID, Name: model.Name}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user