-
Notifications
You must be signed in to change notification settings - Fork 0
[comments] introduce module and API #7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
b8dfed8
[db] pagination helper
capcom6 236344f
[projects] use universal pagination and optimize counting
capcom6 6dd8f29
[tasks] use universal pagination and optimize counting
capcom6 b66a4d3
[users] use universal pagination and optimize counting
capcom6 891283e
[users] add admin check method
capcom6 5617b1a
[comments] add module and API
capcom6 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| // Package comments provides functionality for managing comments on tasks. | ||
| // | ||
| // The comments module supports: | ||
| // - Creating comments with Markdown content | ||
| // - Editing comments (author or admin only) | ||
| // - Soft deleting comments (author or admin only) | ||
| // - Retrieving comments by task | ||
| // | ||
| // # Usage | ||
| // | ||
| // Create a new comment: | ||
| // | ||
| // svc := comments.NewService(repo, tasksSvc, logger) | ||
| // input := comments.CommentInput{ | ||
| // TaskID: 123, | ||
| // AuthorID: 456, | ||
| // Content: "This is a comment", | ||
| // } | ||
| // comment, err := svc.Create(ctx, input) | ||
| // | ||
| // List comments for a task: | ||
| // | ||
| // pagination := &db.Pagination{} | ||
| // comments, err := svc.ListByTask(ctx, taskID, pagination) | ||
| // | ||
| // Update a comment: | ||
| // | ||
| // update := comments.CommentUpdate{Content: "Updated content"} | ||
| // err := svc.Update(ctx, userID, commentID, update) | ||
| // | ||
| // Delete a comment (soft delete): | ||
| // | ||
| // err := svc.Delete(ctx, userID, commentID) | ||
| package comments | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| package comments | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "strings" | ||
| "time" | ||
| "unicode/utf8" | ||
| ) | ||
|
|
||
| const ( | ||
| // MaxContentLength is the maximum length of comment content. | ||
| MaxContentLength = 10000 | ||
| ) | ||
|
|
||
| // Comment represents a complete comment entity with all fields. | ||
| type Comment struct { | ||
| ID int64 | ||
| TaskID int64 | ||
| AuthorID int64 | ||
| Content string | ||
| CreatedAt time.Time | ||
| UpdatedAt time.Time | ||
| DeletedAt *time.Time | ||
| } | ||
|
|
||
| // CommentInput contains the data required to create a new comment. | ||
| type CommentInput struct { | ||
| TaskID int64 | ||
| AuthorID int64 | ||
| Content string | ||
| } | ||
|
|
||
| // CommentUpdate represents the data that can be updated for a comment. | ||
| type CommentUpdate struct { | ||
| Content string | ||
| } | ||
|
|
||
| // Validate checks that the input data is valid for comment creation. | ||
| func (i CommentInput) Validate() error { | ||
| // Validate task ID | ||
| if i.TaskID <= 0 { | ||
| return fmt.Errorf("%w: task_id must be positive", ErrValidationFailed) | ||
| } | ||
|
|
||
| // Validate author ID | ||
| if i.AuthorID <= 0 { | ||
| return fmt.Errorf("%w: author_id must be positive", ErrValidationFailed) | ||
| } | ||
|
|
||
| // Validate content | ||
| content := strings.TrimSpace(i.Content) | ||
| if content == "" { | ||
| return fmt.Errorf("%w: content is required", ErrValidationFailed) | ||
| } | ||
|
|
||
| if utf8.RuneCountInString(content) > MaxContentLength { | ||
| return fmt.Errorf("%w: content too long (max %d characters)", ErrValidationFailed, MaxContentLength) | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| // Validate checks that the update data is valid. | ||
| func (u CommentUpdate) Validate() error { | ||
| // Validate content | ||
| content := strings.TrimSpace(u.Content) | ||
| if content == "" { | ||
| return fmt.Errorf("%w: content is required", ErrValidationFailed) | ||
| } | ||
|
|
||
| if utf8.RuneCountInString(content) > MaxContentLength { | ||
| return fmt.Errorf("%w: content too long (max %d characters)", ErrValidationFailed, MaxContentLength) | ||
| } | ||
|
|
||
| return nil | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| package comments | ||
|
|
||
| import "errors" | ||
|
|
||
| // Module-specific error definitions. | ||
| // These errors can be checked using [errors.Is](err, ErrXXX). | ||
| var ( | ||
| // ErrNotFound indicates the requested comment does not exist. | ||
| ErrNotFound = errors.New("comment not found") | ||
|
|
||
| // ErrValidationFailed indicates input validation failed. | ||
| ErrValidationFailed = errors.New("validation failed") | ||
|
|
||
| // ErrUnauthorized indicates the user lacks permission for this action. | ||
| ErrUnauthorized = errors.New("unauthorized") | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| package comments | ||
|
|
||
| import ( | ||
| "time" | ||
|
|
||
| "github.com/go-core-fx/bunfx" | ||
| "github.com/uptrace/bun" | ||
| ) | ||
|
|
||
| // commentModel is the database representation using Bun ORM. | ||
| // This struct maps to the `comments` table and is used for all database operations. | ||
| type commentModel struct { | ||
| bun.BaseModel `bun:"table:comments,alias:c"` | ||
| bunfx.TimedModel | ||
|
|
||
| ID int64 `bun:"id,pk,autoincrement"` | ||
| TaskID int64 `bun:"task_id,notnull"` | ||
| AuthorID int64 `bun:"author_id,notnull"` | ||
| Content string `bun:"content,notnull,type:text"` | ||
| DeletedAt *time.Time `bun:"deleted_at,soft_delete,nullzero"` | ||
| } | ||
|
|
||
| // newCommentModel creates a new commentModel from CommentInput. | ||
| func newCommentModel(input CommentInput) *commentModel { | ||
| now := time.Now() | ||
| return &commentModel{ | ||
| BaseModel: bun.BaseModel{}, | ||
| TimedModel: bunfx.TimedModel{CreatedAt: now, UpdatedAt: now}, | ||
| ID: 0, // Auto-generated by database | ||
| TaskID: input.TaskID, | ||
| AuthorID: input.AuthorID, | ||
| Content: input.Content, | ||
| DeletedAt: nil, | ||
| } | ||
| } | ||
|
|
||
| // toDomain converts the database model to a domain Comment entity. | ||
| // Returns nil if the model is nil. | ||
| func (m *commentModel) toDomain() *Comment { | ||
| if m == nil { | ||
| return nil | ||
| } | ||
| return &Comment{ | ||
| ID: m.ID, | ||
| TaskID: m.TaskID, | ||
| AuthorID: m.AuthorID, | ||
| Content: m.Content, | ||
| CreatedAt: m.CreatedAt, | ||
| UpdatedAt: m.UpdatedAt, | ||
| DeletedAt: m.DeletedAt, | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| package comments | ||
|
|
||
| import ( | ||
| "github.com/go-core-fx/logger" | ||
| "go.uber.org/fx" | ||
| ) | ||
|
|
||
| // Module creates and returns an FX module for the comments package. | ||
| // | ||
| // The module provides: | ||
| // - comments.Service (public) - for use by HTTP handlers and other modules | ||
| // - comments.Repository (private) - internal data access layer | ||
| // - Named logger "comments" for structured logging | ||
| func Module() fx.Option { | ||
| return fx.Module( | ||
| "comments", | ||
| logger.WithNamedLogger("comments"), | ||
| fx.Provide(NewRepository, fx.Private), | ||
| fx.Provide(NewService), | ||
| ) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| package comments | ||
|
|
||
| import ( | ||
| "context" | ||
| "database/sql" | ||
| "errors" | ||
| "fmt" | ||
|
|
||
| "github.com/uptrace/bun" | ||
| ) | ||
|
|
||
| // Repository handles data access operations for comments. | ||
| type Repository struct { | ||
| db *bun.DB | ||
| } | ||
|
|
||
| // NewRepository creates a new Repository instance with the given database connection. | ||
| func NewRepository(db *bun.DB) *Repository { | ||
| return &Repository{db: db} | ||
| } | ||
|
|
||
| // Create inserts a new comment. | ||
| func (r *Repository) Create(ctx context.Context, input CommentInput) (*Comment, error) { | ||
| model := newCommentModel(input) | ||
|
|
||
| if _, err := r.db.NewInsert().Model(model).Returning("*").Exec(ctx); err != nil { | ||
| return nil, fmt.Errorf("failed to insert comment: %w", err) | ||
| } | ||
|
|
||
| return model.toDomain(), nil | ||
| } | ||
|
|
||
| // GetByID retrieves a comment by its ID. | ||
| func (r *Repository) GetByID(ctx context.Context, id int64) (*Comment, error) { | ||
| var model commentModel | ||
| if err := r.db.NewSelect().Model(&model).Where("id = ?", id).Scan(ctx); err != nil { | ||
| if errors.Is(err, sql.ErrNoRows) { | ||
| return nil, ErrNotFound | ||
| } | ||
| return nil, fmt.Errorf("failed to get comment by ID: %w", err) | ||
| } | ||
| return model.toDomain(), nil | ||
| } | ||
|
|
||
| // List retrieves all comments for a specific task. | ||
| func (r *Repository) List(ctx context.Context, taskID int64) ([]Comment, error) { | ||
| models := make([]commentModel, 0) | ||
|
|
||
| query := r.db.NewSelect().Model(&models).Where("task_id = ?", taskID) | ||
|
|
||
| if err := query.OrderExpr("created_at ASC").Scan(ctx); err != nil { | ||
| return nil, fmt.Errorf("failed to list comments by task: %w", err) | ||
| } | ||
|
|
||
| comments := make([]Comment, 0, len(models)) | ||
| for _, model := range models { | ||
| comments = append(comments, *model.toDomain()) | ||
| } | ||
|
|
||
| return comments, nil | ||
| } | ||
|
|
||
| // Update modifies an existing comment with the provided content. | ||
| func (r *Repository) Update(ctx context.Context, id int64, content string) error { | ||
| if _, err := r.db.NewUpdate(). | ||
| Model((*commentModel)(nil)). | ||
| Set("content = ?", content). | ||
| Where("id = ?", id). | ||
| Exec(ctx); err != nil { | ||
| return fmt.Errorf("failed to update comment: %w", err) | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| // Delete soft-deletes a comment by setting the deleted_at timestamp. | ||
| func (r *Repository) Delete(ctx context.Context, id int64) error { | ||
| _, err := r.db.NewDelete(). | ||
| Model((*commentModel)(nil)). | ||
| Where("id = ?", id). | ||
| Exec(ctx) | ||
|
|
||
| if err != nil { | ||
| return fmt.Errorf("failed to delete comment: %w", err) | ||
| } | ||
|
|
||
| return nil | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.