initial commit
This commit is contained in:
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"
|
||||
}
|
||||
Reference in New Issue
Block a user