Module project, commit, submission
All checks were successful
Build and Release / release (push) Successful in 1m15s

This commit is contained in:
2026-04-26 16:31:03 +07:00
parent ac90236022
commit 6918a100fc
60 changed files with 5957 additions and 1020 deletions

View File

@@ -0,0 +1,137 @@
package controllers
import (
"context"
"history-api/internal/dtos/request"
"history-api/internal/dtos/response"
"history-api/internal/services"
"history-api/pkg/validator"
"time"
"github.com/gofiber/fiber/v3"
)
type CommitController struct {
service services.CommitService
}
func NewCommitController(service services.CommitService) *CommitController {
return &CommitController{
service: service,
}
}
// CreateCommit godoc
// @Summary Create a new commit
// @Description Create a new commit and update project's latest commit ID. Only owner/editor allowed.
// @Tags Commits
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path string true "Project ID"
// @Param request body request.CreateCommitDto true "Commit Data"
// @Success 201 {object} response.CommonResponse
// @Failure 400 {object} response.CommonResponse
// @Failure 403 {object} response.CommonResponse
// @Failure 500 {object} response.CommonResponse
// @Router /projects/{id}/commits [post]
func (h *CommitController) CreateCommit(c fiber.Ctx) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
projectID := c.Params("id")
dto := &request.CreateCommitDto{}
if err := validator.ValidateBodyDto(c, dto); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(response.CommonResponse{
Status: false,
Errors: err,
})
}
uid := c.Locals("uid").(string)
res, err := h.service.CreateCommit(ctx, uid, projectID, dto)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
Status: false,
Message: err.Error(),
})
}
return c.Status(fiber.StatusCreated).JSON(response.CommonResponse{
Status: true,
Data: res,
})
}
// RestoreCommit godoc
// @Summary Restore project to a commit
// @Description Update project's latest commit ID to an older commit. Only owner/editor allowed.
// @Tags Commits
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path string true "Project ID"
// @Param request body request.RestoreCommitDto true "Restore Data"
// @Success 200 {object} response.CommonResponse
// @Failure 400 {object} response.CommonResponse
// @Failure 403 {object} response.CommonResponse
// @Failure 404 {object} response.CommonResponse
// @Failure 500 {object} response.CommonResponse
// @Router /projects/{id}/commits/restore [post]
func (h *CommitController) RestoreCommit(c fiber.Ctx) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
projectID := c.Params("id")
dto := &request.RestoreCommitDto{}
if err := validator.ValidateBodyDto(c, dto); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(response.CommonResponse{
Status: false,
Errors: err,
})
}
uid := c.Locals("uid").(string)
err := h.service.RestoreCommit(ctx, uid, projectID, dto)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
Status: false,
Message: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
Status: true,
Message: "Project restored successfully",
})
}
// GetProjectCommits godoc
// @Summary Get project commits
// @Description Retrieve all commits for a specific project
// @Tags Commits
// @Accept json
// @Produce json
// @Param id path string true "Project ID"
// @Success 200 {object} response.CommonResponse
// @Failure 400 {object} response.CommonResponse
// @Failure 500 {object} response.CommonResponse
// @Router /projects/{id}/commits [get]
func (h *CommitController) GetProjectCommits(c fiber.Ctx) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
projectID := c.Params("id")
res, err := h.service.GetProjectCommits(ctx, projectID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
Status: false,
Message: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
Status: true,
Data: res,
})
}

View File

@@ -198,3 +198,170 @@ func (h *ProjectController) DeleteProject(c fiber.Ctx) error {
Message: "Project deleted successfully",
})
}
// AddMember godoc
// @Summary Add a member to project
// @Description Invite a user to the project with a specific role (EDITOR or VIEWER). Only project owner can do this.
// @Tags Projects
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path string true "Project ID"
// @Param request body request.AddProjectMemberDto true "Member Data"
// @Success 201 {object} response.CommonResponse
// @Failure 400 {object} response.CommonResponse
// @Failure 403 {object} response.CommonResponse
// @Failure 409 {object} response.CommonResponse
// @Failure 500 {object} response.CommonResponse
// @Router /projects/{id}/members [post]
func (h *ProjectController) AddMember(c fiber.Ctx) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
projectID := c.Params("id")
dto := &request.AddProjectMemberDto{}
if err := validator.ValidateBodyDto(c, dto); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(response.CommonResponse{
Status: false,
Errors: err,
})
}
uid := c.Locals("uid").(string)
res, err := h.service.AddMember(ctx, uid, projectID, dto)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
Status: false,
Message: err.Error(),
})
}
return c.Status(fiber.StatusCreated).JSON(response.CommonResponse{
Status: true,
Data: res,
})
}
// UpdateMemberRole godoc
// @Summary Update member role
// @Description Change a member's role in the project. Only project owner can do this.
// @Tags Projects
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path string true "Project ID"
// @Param userId path string true "Member User ID"
// @Param request body request.UpdateProjectMemberDto true "Role Data"
// @Success 200 {object} response.CommonResponse
// @Failure 400 {object} response.CommonResponse
// @Failure 403 {object} response.CommonResponse
// @Failure 404 {object} response.CommonResponse
// @Failure 500 {object} response.CommonResponse
// @Router /projects/{id}/members/{userId} [put]
func (h *ProjectController) UpdateMemberRole(c fiber.Ctx) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
projectID := c.Params("id")
memberUserID := c.Params("userId")
dto := &request.UpdateProjectMemberDto{}
if err := validator.ValidateBodyDto(c, dto); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(response.CommonResponse{
Status: false,
Errors: err,
})
}
uid := c.Locals("uid").(string)
res, err := h.service.UpdateMemberRole(ctx, uid, projectID, memberUserID, dto)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
Status: false,
Message: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
Status: true,
Data: res,
})
}
// RemoveMember godoc
// @Summary Remove a member from project
// @Description Remove a user from the project. Only project owner can do this.
// @Tags Projects
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path string true "Project ID"
// @Param userId path string true "Member User ID"
// @Success 200 {object} response.CommonResponse
// @Failure 400 {object} response.CommonResponse
// @Failure 403 {object} response.CommonResponse
// @Failure 404 {object} response.CommonResponse
// @Failure 500 {object} response.CommonResponse
// @Router /projects/{id}/members/{userId} [delete]
func (h *ProjectController) RemoveMember(c fiber.Ctx) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
projectID := c.Params("id")
memberUserID := c.Params("userId")
uid := c.Locals("uid").(string)
err := h.service.RemoveMember(ctx, uid, projectID, memberUserID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
Status: false,
Message: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
Status: true,
Message: "Member removed successfully",
})
}
// ChangeOwner godoc
// @Summary Transfer project ownership
// @Description Transfer project ownership to an existing member. Only the current owner can do this.
// @Tags Projects
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path string true "Project ID"
// @Param request body request.ChangeOwnerDto true "New Owner Data"
// @Success 200 {object} response.CommonResponse
// @Failure 400 {object} response.CommonResponse
// @Failure 403 {object} response.CommonResponse
// @Failure 500 {object} response.CommonResponse
// @Router /projects/{id}/change-owner [put]
func (h *ProjectController) ChangeOwner(c fiber.Ctx) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
projectID := c.Params("id")
dto := &request.ChangeOwnerDto{}
if err := validator.ValidateBodyDto(c, dto); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(response.CommonResponse{
Status: false,
Errors: err,
})
}
uid := c.Locals("uid").(string)
res, err := h.service.ChangeOwner(ctx, uid, projectID, dto.NewOwnerID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
Status: false,
Message: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
Status: true,
Data: res,
})
}

View File

@@ -0,0 +1,213 @@
package controllers
import (
"context"
"history-api/internal/dtos/request"
"history-api/internal/dtos/response"
"history-api/internal/services"
"history-api/pkg/validator"
"time"
"github.com/gofiber/fiber/v3"
)
type SubmissionController interface {
CreateSubmission(c fiber.Ctx) error
UpdateSubmissionStatus(c fiber.Ctx) error
GetSubmissionByID(c fiber.Ctx) error
SearchSubmissions(c fiber.Ctx) error
DeleteSubmission(c fiber.Ctx) error
}
type submissionController struct {
submissionService services.SubmissionService
}
func NewSubmissionController(submissionService services.SubmissionService) SubmissionController {
return &submissionController{
submissionService: submissionService,
}
}
// @Summary Create submission
// @Description Submit a new submission for a project commit
// @Tags Submission
// @Accept json
// @Produce json
// @Param body body request.CreateSubmissionDto true "Submission data"
// @Success 200 {object} response.CommonResponse
// @Failure 400 {object} response.CommonResponse
// @Failure 500 {object} response.CommonResponse
// @Security BearerAuth
// @Router /submissions [post]
func (s *submissionController) CreateSubmission(c fiber.Ctx) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
dto := &request.CreateSubmissionDto{}
if err := validator.ValidateBodyDto(c, dto); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(response.CommonResponse{
Status: false,
Errors: err,
})
}
res, err := s.submissionService.CreateSubmission(ctx, c.Locals("uid").(string), dto)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
Status: false,
Message: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
Status: true,
Data: res,
})
}
// @Summary Update submission status
// @Description Approve or reject a submission
// @Tags Submission
// @Accept json
// @Produce json
// @Param id path string true "Submission ID"
// @Param body body request.UpdateSubmissionStatusDto true "Status update data"
// @Success 200 {object} response.CommonResponse
// @Failure 400 {object} response.CommonResponse
// @Failure 500 {object} response.CommonResponse
// @Security BearerAuth
// @Router /submissions/{id}/status [patch]
func (s *submissionController) UpdateSubmissionStatus(c fiber.Ctx) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
id := c.Params("id")
uid := c.Locals("uid").(string)
dto := &request.UpdateSubmissionStatusDto{}
if err := validator.ValidateBodyDto(c, dto); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(response.CommonResponse{
Status: false,
Errors: err,
})
}
res, err := s.submissionService.UpdateSubmissionStatus(ctx, uid, id, dto)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
Status: false,
Message: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
Status: true,
Data: res,
})
}
// @Summary Get submission by ID
// @Description Get detailed information of a submission
// @Tags Submission
// @Produce json
// @Param id path string true "Submission ID"
// @Success 200 {object} response.CommonResponse
// @Failure 500 {object} response.CommonResponse
// @Security BearerAuth
// @Router /submissions/{id} [get]
func (s *submissionController) GetSubmissionByID(c fiber.Ctx) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
id := c.Params("id")
res, err := s.submissionService.GetSubmissionByID(ctx, id)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
Status: false,
Message: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
Status: true,
Data: res,
})
}
// @Summary Search submissions
// @Description Get a list of submissions with filters
// @Tags Submission
// @Accept json
// @Produce json
// @Param query query request.SearchSubmissionDto false "Search Query"
// @Success 200 {object} response.CommonResponse
// @Failure 400 {object} response.CommonResponse
// @Failure 500 {object} response.CommonResponse
// @Security BearerAuth
// @Router /submissions [get]
func (s *submissionController) SearchSubmissions(c fiber.Ctx) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
dto := &request.SearchSubmissionDto{}
if err := validator.ValidateQueryDto(c, dto); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(response.CommonResponse{
Status: false,
Errors: err,
})
}
res, err := s.submissionService.SearchSubmissions(ctx, dto)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
Status: false,
Message: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
Status: true,
Data: res,
})
}
// @Summary Delete submission
// @Description Delete a submission by ID
// @Tags Submission
// @Produce json
// @Param id path string true "Submission ID"
// @Success 200 {object} response.CommonResponse
// @Failure 401 {object} response.CommonResponse
// @Failure 500 {object} response.CommonResponse
// @Security BearerAuth
// @Router /submissions/{id} [delete]
func (s *submissionController) DeleteSubmission(c fiber.Ctx) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
id := c.Params("id")
claimsVal := c.Locals("user_claims")
if claimsVal == nil {
return c.Status(fiber.StatusUnauthorized).JSON(response.CommonResponse{
Status: false,
Message: "Unauthorized",
})
}
claims, ok := claimsVal.(*response.JWTClaims)
if !ok {
return c.Status(fiber.StatusUnauthorized).JSON(response.CommonResponse{
Status: false,
Message: "Invalid user claims",
})
}
err := s.submissionService.DeleteSubmission(ctx, c.Locals("uid").(string), id, claims)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
Status: false,
Message: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
Status: true,
Message: "Submission deleted successfully",
})
}

View File

@@ -46,7 +46,7 @@ func (h *UserController) GetUserCurrent(c fiber.Ctx) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
res, err := h.service.GetUserCurrent(ctx, c.Locals("uid").(string))
res, err := h.service.GetUserByID(ctx, c.Locals("uid").(string))
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
Status: false,

View File

@@ -2,8 +2,8 @@ package controllers
import (
"context"
"history-api/internal/dtos/response"
"history-api/internal/dtos/request"
"history-api/internal/dtos/response"
"history-api/internal/services"
"history-api/pkg/validator"
"time"
@@ -63,7 +63,7 @@ func (m *VerificationController) SearchVerification(c fiber.Ctx) error {
dto := &request.SearchUserVerificationDto{}
if err := validator.ValidateQueryDto(c, dto); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(response.CommonResponse{
Status: false,
Status: false,
Errors: err,
})
}
@@ -137,7 +137,7 @@ func (m *VerificationController) CreateVerification(c fiber.Ctx) error {
dto := &request.CreateUserVerificationDto{}
if err := validator.ValidateBodyDto(c, dto); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(response.CommonResponse{
Status: false,
Status: false,
Errors: err,
})
}
@@ -148,7 +148,10 @@ func (m *VerificationController) CreateVerification(c fiber.Ctx) error {
Message: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(res)
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
Status: true,
Data: res,
})
}
// @Summary Update application status
@@ -170,7 +173,7 @@ func (m *VerificationController) UpdateVerificationStatus(c fiber.Ctx) error {
dto := &request.UpdateVerificationStatusDto{}
if err := validator.ValidateBodyDto(c, dto); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(response.CommonResponse{
Status: false,
Status: false,
Errors: err,
})
}
@@ -182,5 +185,8 @@ func (m *VerificationController) UpdateVerificationStatus(c fiber.Ctx) error {
Message: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(res)
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
Status: true,
Data: res,
})
}

View File

@@ -0,0 +1,12 @@
package request
import "encoding/json"
type CreateCommitDto struct {
SnapshotJson json.RawMessage `json:"snapshot_json" validate:"required"`
EditSummary string `json:"edit_summary" validate:"required,max=500"`
}
type RestoreCommitDto struct {
CommitID string `json:"commit_id" validate:"required,uuid"`
}

View File

@@ -0,0 +1,14 @@
package request
type AddProjectMemberDto struct {
UserID string `json:"user_id" validate:"required,uuid"`
Role string `json:"role" validate:"required,oneof=EDITOR VIEWER"`
}
type UpdateProjectMemberDto struct {
Role string `json:"role" validate:"required,oneof=EDITOR VIEWER"`
}
type ChangeOwnerDto struct {
NewOwnerID string `json:"new_owner_id" validate:"required,uuid"`
}

View File

@@ -0,0 +1,26 @@
package request
import "time"
type CreateSubmissionDto struct {
ProjectID string `json:"project_id" validate:"required"`
CommitID string `json:"commit_id" validate:"required"`
Content string `json:"content"`
}
type UpdateSubmissionStatusDto struct {
Status string `json:"status" validate:"required"`
ReviewNote string `json:"review_note" validate:"required,min=10"`
}
type SearchSubmissionDto struct {
PaginationDto
ProjectID string `json:"project_id" query:"project_id" validate:"omitempty,uuid"`
Sort string `json:"sort" query:"sort" validate:"omitempty,oneof=id created_at reviewed_at status"`
Search string `json:"search" query:"search" validate:"omitempty,min=2,max=200"`
UserIDs []string `json:"user_ids" query:"user_ids" validate:"omitempty,dive,uuid"`
Statuses []string `json:"statuses" query:"statuses" validate:"omitempty,dive,oneof=PENDING APPROVED REJECTED"`
ReviewedBy *string `json:"reviewed_by" query:"reviewed_by" validate:"omitempty,uuid"`
CreatedFrom *time.Time `json:"created_from" query:"created_from"`
CreatedTo *time.Time `json:"created_to" query:"created_to"`
}

View File

@@ -0,0 +1,17 @@
package response
import (
"encoding/json"
"time"
)
type CommitResponse struct {
ID string `json:"id"`
ProjectID string `json:"project_id"`
SnapshotJson json.RawMessage `json:"snapshot_json"`
SnapshotHash string `json:"snapshot_hash"`
UserID string `json:"user_id"`
EditSummary string `json:"edit_summary"`
CreatedAt *time.Time `json:"created_at"`
User *UserSimpleResponse `json:"user,omitempty"`
}

View File

@@ -2,19 +2,31 @@ package response
import "time"
type ProjectResponse struct {
ID string `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
LatestRevisionID *string `json:"latest_revision_id,omitempty"`
VersionCount int32 `json:"version_count"`
ProjectStatus string `json:"project_status"`
LockedBy *string `json:"locked_by,omitempty"`
IsDeleted bool `json:"is_deleted"`
UserID string `json:"user_id"`
CreatedAt *time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at"`
User *UserSimpleResponse `json:"user,omitempty"`
CommitIds []string `json:"commit_ids"`
SubmissionIds []string `json:"submission_ids"`
type CommitSimpleResponse struct {
ID string `json:"id"`
EditSummary string `json:"edit_summary"`
}
type MemberSimpleResponse struct {
UserID string `json:"user_id"`
Role string `json:"role"`
DisplayName string `json:"display_name"`
AvatarUrl string `json:"avatar_url"`
}
type ProjectResponse struct {
ID string `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
LatestCommitID *string `json:"latest_commit_id,omitempty"`
ProjectStatus string `json:"project_status"`
LockedBy *string `json:"locked_by,omitempty"`
IsDeleted bool `json:"is_deleted"`
UserID string `json:"user_id"`
CreatedAt *time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at"`
User *UserSimpleResponse `json:"user,omitempty"`
Commits []CommitSimpleResponse `json:"commits"`
SubmissionIds []string `json:"submission_ids"`
Members []MemberSimpleResponse `json:"members"`
}

View File

@@ -0,0 +1,18 @@
package response
import "time"
type SubmissionResponse struct {
ID string `json:"id"`
ProjectID string `json:"project_id"`
CommitID string `json:"commit_id"`
UserID string `json:"user_id"`
CreatedAt *time.Time `json:"created_at"`
Status string `json:"status"`
ReviewedBy *string `json:"reviewed_by,omitempty"`
ReviewedAt *time.Time `json:"reviewed_at,omitempty"`
ReviewNote *string `json:"review_note,omitempty"`
Content *string `json:"content,omitempty"`
User *UserSimpleResponse `json:"user"`
Reviewer *UserSimpleResponse `json:"reviewer,omitempty"`
}

View File

@@ -0,0 +1,206 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: commit.sql
package sqlc
import (
"context"
"encoding/json"
"github.com/jackc/pgx/v5/pgtype"
)
const createCommit = `-- name: CreateCommit :one
INSERT INTO commits (
project_id, snapshot_json, snapshot_hash, user_id, edit_summary
) VALUES (
$1, $2, $3, $4, $5
)
RETURNING id, project_id, snapshot_json, snapshot_hash, user_id, edit_summary, is_deleted, created_at
`
type CreateCommitParams struct {
ProjectID pgtype.UUID `json:"project_id"`
SnapshotJson json.RawMessage `json:"snapshot_json"`
SnapshotHash pgtype.Text `json:"snapshot_hash"`
UserID pgtype.UUID `json:"user_id"`
EditSummary pgtype.Text `json:"edit_summary"`
}
func (q *Queries) CreateCommit(ctx context.Context, arg CreateCommitParams) (Commit, error) {
row := q.db.QueryRow(ctx, createCommit,
arg.ProjectID,
arg.SnapshotJson,
arg.SnapshotHash,
arg.UserID,
arg.EditSummary,
)
var i Commit
err := row.Scan(
&i.ID,
&i.ProjectID,
&i.SnapshotJson,
&i.SnapshotHash,
&i.UserID,
&i.EditSummary,
&i.IsDeleted,
&i.CreatedAt,
)
return i, err
}
const deleteCommit = `-- name: DeleteCommit :exec
UPDATE commits
SET is_deleted = true
WHERE id = $1
`
func (q *Queries) DeleteCommit(ctx context.Context, id pgtype.UUID) error {
_, err := q.db.Exec(ctx, deleteCommit, id)
return err
}
const getCommitById = `-- name: GetCommitById :one
SELECT id, project_id, snapshot_json, snapshot_hash, user_id, edit_summary, is_deleted, created_at
FROM commits
WHERE id = $1 AND is_deleted = false
`
func (q *Queries) GetCommitById(ctx context.Context, id pgtype.UUID) (Commit, error) {
row := q.db.QueryRow(ctx, getCommitById, id)
var i Commit
err := row.Scan(
&i.ID,
&i.ProjectID,
&i.SnapshotJson,
&i.SnapshotHash,
&i.UserID,
&i.EditSummary,
&i.IsDeleted,
&i.CreatedAt,
)
return i, err
}
const getCommitsByIDs = `-- name: GetCommitsByIDs :many
SELECT id, project_id, snapshot_json, snapshot_hash, user_id, edit_summary, is_deleted, created_at FROM commits WHERE id = ANY($1::uuid[]) AND is_deleted = false
`
func (q *Queries) GetCommitsByIDs(ctx context.Context, dollar_1 []pgtype.UUID) ([]Commit, error) {
rows, err := q.db.Query(ctx, getCommitsByIDs, dollar_1)
if err != nil {
return nil, err
}
defer rows.Close()
items := []Commit{}
for rows.Next() {
var i Commit
if err := rows.Scan(
&i.ID,
&i.ProjectID,
&i.SnapshotJson,
&i.SnapshotHash,
&i.UserID,
&i.EditSummary,
&i.IsDeleted,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getCommitsByProjectID = `-- name: GetCommitsByProjectID :many
SELECT id, project_id, snapshot_json, snapshot_hash, user_id, edit_summary, is_deleted, created_at
FROM commits
WHERE project_id = $1 AND is_deleted = false
ORDER BY created_at DESC
`
func (q *Queries) GetCommitsByProjectID(ctx context.Context, projectID pgtype.UUID) ([]Commit, error) {
rows, err := q.db.Query(ctx, getCommitsByProjectID, projectID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []Commit{}
for rows.Next() {
var i Commit
if err := rows.Scan(
&i.ID,
&i.ProjectID,
&i.SnapshotJson,
&i.SnapshotHash,
&i.UserID,
&i.EditSummary,
&i.IsDeleted,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const searchCommits = `-- name: SearchCommits :many
SELECT id, project_id, snapshot_json, snapshot_hash, user_id, edit_summary, is_deleted, created_at
FROM commits
WHERE is_deleted = false
AND ($1::uuid IS NULL OR project_id = $1)
AND ($2::uuid IS NULL OR user_id = $2)
AND ($3::uuid IS NULL OR id < $3::uuid)
ORDER BY created_at DESC
LIMIT $4
`
type SearchCommitsParams struct {
ProjectID pgtype.UUID `json:"project_id"`
UserID pgtype.UUID `json:"user_id"`
CursorID pgtype.UUID `json:"cursor_id"`
Limit int32 `json:"limit"`
}
func (q *Queries) SearchCommits(ctx context.Context, arg SearchCommitsParams) ([]Commit, error) {
rows, err := q.db.Query(ctx, searchCommits,
arg.ProjectID,
arg.UserID,
arg.CursorID,
arg.Limit,
)
if err != nil {
return nil, err
}
defer rows.Close()
items := []Commit{}
for rows.Next() {
var i Commit
if err := rows.Scan(
&i.ID,
&i.ProjectID,
&i.SnapshotJson,
&i.SnapshotHash,
&i.UserID,
&i.EditSummary,
&i.IsDeleted,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}

View File

@@ -10,6 +10,17 @@ import (
"github.com/jackc/pgx/v5/pgtype"
)
type Commit struct {
ID pgtype.UUID `json:"id"`
ProjectID pgtype.UUID `json:"project_id"`
SnapshotJson json.RawMessage `json:"snapshot_json"`
SnapshotHash pgtype.Text `json:"snapshot_hash"`
UserID pgtype.UUID `json:"user_id"`
EditSummary pgtype.Text `json:"edit_summary"`
IsDeleted bool `json:"is_deleted"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
}
type Entity struct {
ID pgtype.UUID `json:"id"`
Name string `json:"name"`
@@ -56,30 +67,24 @@ type Media struct {
}
type Project struct {
ID pgtype.UUID `json:"id"`
Title string `json:"title"`
Description pgtype.Text `json:"description"`
LatestRevisionID pgtype.UUID `json:"latest_revision_id"`
VersionCount int32 `json:"version_count"`
ProjectStatus int16 `json:"project_status"`
LockedBy pgtype.UUID `json:"locked_by"`
IsDeleted bool `json:"is_deleted"`
UserID pgtype.UUID `json:"user_id"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
ID pgtype.UUID `json:"id"`
Title string `json:"title"`
Description pgtype.Text `json:"description"`
LatestCommitID pgtype.UUID `json:"latest_commit_id"`
ProjectStatus int16 `json:"project_status"`
LockedBy pgtype.UUID `json:"locked_by"`
IsDeleted bool `json:"is_deleted"`
UserID pgtype.UUID `json:"user_id"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
type Revision struct {
ID pgtype.UUID `json:"id"`
ProjectID pgtype.UUID `json:"project_id"`
VersionNo int32 `json:"version_no"`
SnapshotJson json.RawMessage `json:"snapshot_json"`
SnapshotHash pgtype.Text `json:"snapshot_hash"`
ParentID pgtype.UUID `json:"parent_id"`
UserID pgtype.UUID `json:"user_id"`
EditSummary pgtype.Text `json:"edit_summary"`
IsDeleted bool `json:"is_deleted"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
type ProjectMember struct {
ProjectID pgtype.UUID `json:"project_id"`
UserID pgtype.UUID `json:"user_id"`
Role int16 `json:"role"`
InvitedBy pgtype.UUID `json:"invited_by"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
}
type Role struct {
@@ -91,16 +96,17 @@ type Role struct {
}
type Submission struct {
ID pgtype.UUID `json:"id"`
ProjectID pgtype.UUID `json:"project_id"`
RevisionID pgtype.UUID `json:"revision_id"`
SubmittedBy pgtype.UUID `json:"submitted_by"`
SubmittedAt pgtype.Timestamptz `json:"submitted_at"`
Status int16 `json:"status"`
ReviewedBy pgtype.UUID `json:"reviewed_by"`
ReviewedAt pgtype.Timestamptz `json:"reviewed_at"`
ReviewNote pgtype.Text `json:"review_note"`
IsDeleted bool `json:"is_deleted"`
ID pgtype.UUID `json:"id"`
ProjectID pgtype.UUID `json:"project_id"`
CommitID pgtype.UUID `json:"commit_id"`
UserID pgtype.UUID `json:"user_id"`
Status int16 `json:"status"`
ReviewedBy pgtype.UUID `json:"reviewed_by"`
ReviewedAt pgtype.Timestamptz `json:"reviewed_at"`
ReviewNote pgtype.Text `json:"review_note"`
Content pgtype.Text `json:"content"`
IsDeleted bool `json:"is_deleted"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
}
type User struct {

View File

@@ -11,13 +11,80 @@ import (
"github.com/jackc/pgx/v5/pgtype"
)
const addProjectMember = `-- name: AddProjectMember :one
INSERT INTO project_members (
project_id, user_id, role, invited_by
) VALUES (
$1, $2, $3, $4
)
RETURNING project_id, user_id, role, invited_by, created_at
`
type AddProjectMemberParams struct {
ProjectID pgtype.UUID `json:"project_id"`
UserID pgtype.UUID `json:"user_id"`
Role int16 `json:"role"`
InvitedBy pgtype.UUID `json:"invited_by"`
}
func (q *Queries) AddProjectMember(ctx context.Context, arg AddProjectMemberParams) (ProjectMember, error) {
row := q.db.QueryRow(ctx, addProjectMember,
arg.ProjectID,
arg.UserID,
arg.Role,
arg.InvitedBy,
)
var i ProjectMember
err := row.Scan(
&i.ProjectID,
&i.UserID,
&i.Role,
&i.InvitedBy,
&i.CreatedAt,
)
return i, err
}
const changeProjectOwner = `-- name: ChangeProjectOwner :exec
UPDATE projects
SET user_id = $2, updated_at = NOW()
WHERE id = $1 AND is_deleted = false
`
type ChangeProjectOwnerParams struct {
ID pgtype.UUID `json:"id"`
UserID pgtype.UUID `json:"user_id"`
}
func (q *Queries) ChangeProjectOwner(ctx context.Context, arg ChangeProjectOwnerParams) error {
_, err := q.db.Exec(ctx, changeProjectOwner, arg.ID, arg.UserID)
return err
}
const checkProjectPermission = `-- name: CheckProjectPermission :one
SELECT role FROM project_members
WHERE project_id = $1 AND user_id = $2
`
type CheckProjectPermissionParams struct {
ProjectID pgtype.UUID `json:"project_id"`
UserID pgtype.UUID `json:"user_id"`
}
func (q *Queries) CheckProjectPermission(ctx context.Context, arg CheckProjectPermissionParams) (int16, error) {
row := q.db.QueryRow(ctx, checkProjectPermission, arg.ProjectID, arg.UserID)
var role int16
err := row.Scan(&role)
return role, err
}
const countProjects = `-- name: CountProjects :one
SELECT count(*)
FROM projects p
WHERE p.is_deleted = false
AND (
$1::text[] IS NULL
OR p.project_status = ANY($1::text[])
$1::smallint[] IS NULL
OR p.project_status = ANY($1::smallint[])
)
AND ($2::uuid[] IS NULL OR p.user_id = ANY($2::uuid[]))
AND (
@@ -30,7 +97,7 @@ WHERE p.is_deleted = false
`
type CountProjectsParams struct {
Statuses []string `json:"statuses"`
Statuses []int16 `json:"statuses"`
UserIds []pgtype.UUID `json:"user_ids"`
SearchText pgtype.Text `json:"search_text"`
CreatedFrom pgtype.Timestamptz `json:"created_from"`
@@ -57,7 +124,7 @@ INSERT INTO projects (
$1, $2, $3, $4
)
RETURNING
id, title, description, latest_revision_id, version_count, project_status, locked_by, is_deleted, user_id, created_at, updated_at,
id, title, description, latest_commit_id, project_status, locked_by, is_deleted, user_id, created_at, updated_at,
json_build_object(
'id', u.id,
'email', u.email,
@@ -65,8 +132,9 @@ RETURNING
'full_name', up.full_name,
'avatar_url', up.avatar_url
)::json AS user,
'{}'::uuid[] AS commit_ids,
'{}'::uuid[] AS submission_ids
'[]'::json AS commits,
'{}'::uuid[] AS submission_ids,
'[]'::json AS members
`
type CreateProjectParams struct {
@@ -77,20 +145,20 @@ type CreateProjectParams struct {
}
type CreateProjectRow struct {
ID pgtype.UUID `json:"id"`
Title string `json:"title"`
Description pgtype.Text `json:"description"`
LatestRevisionID pgtype.UUID `json:"latest_revision_id"`
VersionCount int32 `json:"version_count"`
ProjectStatus int16 `json:"project_status"`
LockedBy pgtype.UUID `json:"locked_by"`
IsDeleted bool `json:"is_deleted"`
UserID pgtype.UUID `json:"user_id"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
User []byte `json:"user"`
CommitIds []pgtype.UUID `json:"commit_ids"`
SubmissionIds []pgtype.UUID `json:"submission_ids"`
ID pgtype.UUID `json:"id"`
Title string `json:"title"`
Description pgtype.Text `json:"description"`
LatestCommitID pgtype.UUID `json:"latest_commit_id"`
ProjectStatus int16 `json:"project_status"`
LockedBy pgtype.UUID `json:"locked_by"`
IsDeleted bool `json:"is_deleted"`
UserID pgtype.UUID `json:"user_id"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
User []byte `json:"user"`
Commits []byte `json:"commits"`
SubmissionIds []pgtype.UUID `json:"submission_ids"`
Members []byte `json:"members"`
}
func (q *Queries) CreateProject(ctx context.Context, arg CreateProjectParams) (CreateProjectRow, error) {
@@ -105,8 +173,7 @@ func (q *Queries) CreateProject(ctx context.Context, arg CreateProjectParams) (C
&i.ID,
&i.Title,
&i.Description,
&i.LatestRevisionID,
&i.VersionCount,
&i.LatestCommitID,
&i.ProjectStatus,
&i.LockedBy,
&i.IsDeleted,
@@ -114,8 +181,9 @@ func (q *Queries) CreateProject(ctx context.Context, arg CreateProjectParams) (C
&i.CreatedAt,
&i.UpdatedAt,
&i.User,
&i.CommitIds,
&i.Commits,
&i.SubmissionIds,
&i.Members,
)
return i, err
}
@@ -134,11 +202,12 @@ func (q *Queries) DeleteProject(ctx context.Context, id pgtype.UUID) error {
const getProjectById = `-- name: GetProjectById :one
SELECT
p.id, p.title, p.description, p.latest_revision_id, p.version_count, p.project_status, p.locked_by, p.is_deleted, p.user_id, p.created_at, p.updated_at,
p.id, p.title, p.description, p.latest_commit_id, p.project_status, p.locked_by, p.is_deleted, p.user_id, p.created_at, p.updated_at,
COALESCE(
(SELECT array_agg(id) FROM revisions WHERE project_id = p.id),
'{}'
)::uuid[] AS commit_ids,
(SELECT json_agg(json_build_object('id', c.id, 'edit_summary', c.edit_summary) ORDER BY c.created_at DESC)
FROM commits c WHERE c.project_id = p.id AND c.is_deleted = false),
'[]'
)::json AS commits,
COALESCE(
(SELECT array_agg(id) FROM submissions WHERE project_id = p.id),
'{}'
@@ -149,7 +218,18 @@ SELECT
'display_name', up.display_name,
'full_name', up.full_name,
'avatar_url', up.avatar_url
)::json AS user
)::json AS user,
COALESCE(
(SELECT json_agg(json_build_object(
'user_id', pm.user_id, 'role', pm.role,
'display_name', mup.display_name, 'avatar_url', mup.avatar_url
) ORDER BY pm.role ASC, pm.created_at ASC)
FROM project_members pm
JOIN users mu ON pm.user_id = mu.id
LEFT JOIN user_profiles mup ON mu.id = mup.user_id
WHERE pm.project_id = p.id),
'[]'
)::json AS members
FROM projects p
JOIN users u ON p.user_id = u.id
LEFT JOIN user_profiles up ON u.id = up.user_id
@@ -157,20 +237,20 @@ WHERE p.id = $1 AND p.is_deleted = false
`
type GetProjectByIdRow struct {
ID pgtype.UUID `json:"id"`
Title string `json:"title"`
Description pgtype.Text `json:"description"`
LatestRevisionID pgtype.UUID `json:"latest_revision_id"`
VersionCount int32 `json:"version_count"`
ProjectStatus int16 `json:"project_status"`
LockedBy pgtype.UUID `json:"locked_by"`
IsDeleted bool `json:"is_deleted"`
UserID pgtype.UUID `json:"user_id"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
CommitIds []pgtype.UUID `json:"commit_ids"`
SubmissionIds []pgtype.UUID `json:"submission_ids"`
User []byte `json:"user"`
ID pgtype.UUID `json:"id"`
Title string `json:"title"`
Description pgtype.Text `json:"description"`
LatestCommitID pgtype.UUID `json:"latest_commit_id"`
ProjectStatus int16 `json:"project_status"`
LockedBy pgtype.UUID `json:"locked_by"`
IsDeleted bool `json:"is_deleted"`
UserID pgtype.UUID `json:"user_id"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
Commits []byte `json:"commits"`
SubmissionIds []pgtype.UUID `json:"submission_ids"`
User []byte `json:"user"`
Members []byte `json:"members"`
}
func (q *Queries) GetProjectById(ctx context.Context, id pgtype.UUID) (GetProjectByIdRow, error) {
@@ -180,25 +260,29 @@ func (q *Queries) GetProjectById(ctx context.Context, id pgtype.UUID) (GetProjec
&i.ID,
&i.Title,
&i.Description,
&i.LatestRevisionID,
&i.VersionCount,
&i.LatestCommitID,
&i.ProjectStatus,
&i.LockedBy,
&i.IsDeleted,
&i.UserID,
&i.CreatedAt,
&i.UpdatedAt,
&i.CommitIds,
&i.Commits,
&i.SubmissionIds,
&i.User,
&i.Members,
)
return i, err
}
const getProjectsByIDs = `-- name: GetProjectsByIDs :many
SELECT
p.id, p.title, p.description, p.latest_revision_id, p.version_count, p.project_status, p.locked_by, p.is_deleted, p.user_id, p.created_at, p.updated_at,
COALESCE((SELECT array_agg(id) FROM revisions WHERE project_id = p.id), '{}')::uuid[] AS commit_ids,
p.id, p.title, p.description, p.latest_commit_id, p.project_status, p.locked_by, p.is_deleted, p.user_id, p.created_at, p.updated_at,
COALESCE(
(SELECT json_agg(json_build_object('id', c.id, 'edit_summary', c.edit_summary) ORDER BY c.created_at DESC)
FROM commits c WHERE c.project_id = p.id AND c.is_deleted = false),
'[]'
)::json AS commits,
COALESCE((SELECT array_agg(id) FROM submissions WHERE project_id = p.id), '{}')::uuid[] AS submission_ids,
json_build_object(
'id', u.id,
@@ -206,7 +290,18 @@ SELECT
'display_name', up.display_name,
'full_name', up.full_name,
'avatar_url', up.avatar_url
)::json AS user
)::json AS user,
COALESCE(
(SELECT json_agg(json_build_object(
'user_id', pm.user_id, 'role', pm.role,
'display_name', mup.display_name, 'avatar_url', mup.avatar_url
) ORDER BY pm.role ASC, pm.created_at ASC)
FROM project_members pm
JOIN users mu ON pm.user_id = mu.id
LEFT JOIN user_profiles mup ON mu.id = mup.user_id
WHERE pm.project_id = p.id),
'[]'
)::json AS members
FROM projects p
JOIN users u ON p.user_id = u.id
LEFT JOIN user_profiles up ON u.id = up.user_id
@@ -214,20 +309,20 @@ WHERE p.id = ANY($1::uuid[]) AND p.is_deleted = false
`
type GetProjectsByIDsRow struct {
ID pgtype.UUID `json:"id"`
Title string `json:"title"`
Description pgtype.Text `json:"description"`
LatestRevisionID pgtype.UUID `json:"latest_revision_id"`
VersionCount int32 `json:"version_count"`
ProjectStatus int16 `json:"project_status"`
LockedBy pgtype.UUID `json:"locked_by"`
IsDeleted bool `json:"is_deleted"`
UserID pgtype.UUID `json:"user_id"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
CommitIds []pgtype.UUID `json:"commit_ids"`
SubmissionIds []pgtype.UUID `json:"submission_ids"`
User []byte `json:"user"`
ID pgtype.UUID `json:"id"`
Title string `json:"title"`
Description pgtype.Text `json:"description"`
LatestCommitID pgtype.UUID `json:"latest_commit_id"`
ProjectStatus int16 `json:"project_status"`
LockedBy pgtype.UUID `json:"locked_by"`
IsDeleted bool `json:"is_deleted"`
UserID pgtype.UUID `json:"user_id"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
Commits []byte `json:"commits"`
SubmissionIds []pgtype.UUID `json:"submission_ids"`
User []byte `json:"user"`
Members []byte `json:"members"`
}
func (q *Queries) GetProjectsByIDs(ctx context.Context, dollar_1 []pgtype.UUID) ([]GetProjectsByIDsRow, error) {
@@ -243,17 +338,17 @@ func (q *Queries) GetProjectsByIDs(ctx context.Context, dollar_1 []pgtype.UUID)
&i.ID,
&i.Title,
&i.Description,
&i.LatestRevisionID,
&i.VersionCount,
&i.LatestCommitID,
&i.ProjectStatus,
&i.LockedBy,
&i.IsDeleted,
&i.UserID,
&i.CreatedAt,
&i.UpdatedAt,
&i.CommitIds,
&i.Commits,
&i.SubmissionIds,
&i.User,
&i.Members,
); err != nil {
return nil, err
}
@@ -267,11 +362,12 @@ func (q *Queries) GetProjectsByIDs(ctx context.Context, dollar_1 []pgtype.UUID)
const getProjectsByUserId = `-- name: GetProjectsByUserId :many
SELECT
p.id, p.title, p.description, p.latest_revision_id, p.version_count, p.project_status, p.locked_by, p.is_deleted, p.user_id, p.created_at, p.updated_at,
p.id, p.title, p.description, p.latest_commit_id, p.project_status, p.locked_by, p.is_deleted, p.user_id, p.created_at, p.updated_at,
COALESCE(
(SELECT array_agg(id) FROM revisions WHERE project_id = p.id),
'{}'
)::uuid[] AS commit_ids,
(SELECT json_agg(json_build_object('id', c.id, 'edit_summary', c.edit_summary) ORDER BY c.created_at DESC)
FROM commits c WHERE c.project_id = p.id AND c.is_deleted = false),
'[]'
)::json AS commits,
COALESCE(
(SELECT array_agg(id) FROM submissions WHERE project_id = p.id),
'{}'
@@ -282,11 +378,22 @@ SELECT
'display_name', up.display_name,
'full_name', up.full_name,
'avatar_url', up.avatar_url
)::json AS user
)::json AS user,
COALESCE(
(SELECT json_agg(json_build_object(
'user_id', pm.user_id, 'role', pm.role,
'display_name', mup.display_name, 'avatar_url', mup.avatar_url
) ORDER BY pm.role ASC, pm.created_at ASC)
FROM project_members pm
JOIN users mu ON pm.user_id = mu.id
LEFT JOIN user_profiles mup ON mu.id = mup.user_id
WHERE pm.project_id = p.id),
'[]'
)::json AS members
FROM projects p
JOIN users u ON p.user_id = u.id
LEFT JOIN user_profiles up ON u.id = up.user_id
WHERE p.user_id = $1
WHERE (p.user_id = $1 OR EXISTS (SELECT 1 FROM project_members pm2 WHERE pm2.project_id = p.id AND pm2.user_id = $1))
AND p.is_deleted = false
AND ($2::uuid IS NULL OR p.id < $2::uuid)
ORDER BY p.updated_at DESC
@@ -300,20 +407,20 @@ type GetProjectsByUserIdParams struct {
}
type GetProjectsByUserIdRow struct {
ID pgtype.UUID `json:"id"`
Title string `json:"title"`
Description pgtype.Text `json:"description"`
LatestRevisionID pgtype.UUID `json:"latest_revision_id"`
VersionCount int32 `json:"version_count"`
ProjectStatus int16 `json:"project_status"`
LockedBy pgtype.UUID `json:"locked_by"`
IsDeleted bool `json:"is_deleted"`
UserID pgtype.UUID `json:"user_id"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
CommitIds []pgtype.UUID `json:"commit_ids"`
SubmissionIds []pgtype.UUID `json:"submission_ids"`
User []byte `json:"user"`
ID pgtype.UUID `json:"id"`
Title string `json:"title"`
Description pgtype.Text `json:"description"`
LatestCommitID pgtype.UUID `json:"latest_commit_id"`
ProjectStatus int16 `json:"project_status"`
LockedBy pgtype.UUID `json:"locked_by"`
IsDeleted bool `json:"is_deleted"`
UserID pgtype.UUID `json:"user_id"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
Commits []byte `json:"commits"`
SubmissionIds []pgtype.UUID `json:"submission_ids"`
User []byte `json:"user"`
Members []byte `json:"members"`
}
func (q *Queries) GetProjectsByUserId(ctx context.Context, arg GetProjectsByUserIdParams) ([]GetProjectsByUserIdRow, error) {
@@ -329,17 +436,17 @@ func (q *Queries) GetProjectsByUserId(ctx context.Context, arg GetProjectsByUser
&i.ID,
&i.Title,
&i.Description,
&i.LatestRevisionID,
&i.VersionCount,
&i.LatestCommitID,
&i.ProjectStatus,
&i.LockedBy,
&i.IsDeleted,
&i.UserID,
&i.CreatedAt,
&i.UpdatedAt,
&i.CommitIds,
&i.Commits,
&i.SubmissionIds,
&i.User,
&i.Members,
); err != nil {
return nil, err
}
@@ -351,13 +458,29 @@ func (q *Queries) GetProjectsByUserId(ctx context.Context, arg GetProjectsByUser
return items, nil
}
const removeProjectMember = `-- name: RemoveProjectMember :exec
DELETE FROM project_members
WHERE project_id = $1 AND user_id = $2
`
type RemoveProjectMemberParams struct {
ProjectID pgtype.UUID `json:"project_id"`
UserID pgtype.UUID `json:"user_id"`
}
func (q *Queries) RemoveProjectMember(ctx context.Context, arg RemoveProjectMemberParams) error {
_, err := q.db.Exec(ctx, removeProjectMember, arg.ProjectID, arg.UserID)
return err
}
const searchProjects = `-- name: SearchProjects :many
SELECT
p.id, p.title, p.description, p.latest_revision_id, p.version_count, p.project_status, p.locked_by, p.is_deleted, p.user_id, p.created_at, p.updated_at,
p.id, p.title, p.description, p.latest_commit_id, p.project_status, p.locked_by, p.is_deleted, p.user_id, p.created_at, p.updated_at,
COALESCE(
(SELECT array_agg(id) FROM revisions WHERE project_id = p.id),
'{}'
)::uuid[] AS commit_ids,
(SELECT json_agg(json_build_object('id', c.id, 'edit_summary', c.edit_summary) ORDER BY c.created_at DESC)
FROM commits c WHERE c.project_id = p.id AND c.is_deleted = false),
'[]'
)::json AS commits,
COALESCE(
(SELECT array_agg(id) FROM submissions WHERE project_id = p.id),
'{}'
@@ -368,14 +491,25 @@ SELECT
'display_name', up.display_name,
'full_name', up.full_name,
'avatar_url', up.avatar_url
)::json AS user
)::json AS user,
COALESCE(
(SELECT json_agg(json_build_object(
'user_id', pm.user_id, 'role', pm.role,
'display_name', mup.display_name, 'avatar_url', mup.avatar_url
) ORDER BY pm.role ASC, pm.created_at ASC)
FROM project_members pm
JOIN users mu ON pm.user_id = mu.id
LEFT JOIN user_profiles mup ON mu.id = mup.user_id
WHERE pm.project_id = p.id),
'[]'
)::json AS members
FROM projects p
JOIN users u ON p.user_id = u.id
LEFT JOIN user_profiles up ON u.id = up.user_id
WHERE p.is_deleted = false
AND (
$1::text[] IS NULL
OR p.project_status = ANY($1::text[])
$1::smallint[] IS NULL
OR p.project_status = ANY($1::smallint[])
)
AND ($2::uuid[] IS NULL OR p.user_id = ANY($2::uuid[]))
AND (
@@ -398,7 +532,7 @@ OFFSET $8
`
type SearchProjectsParams struct {
Statuses []string `json:"statuses"`
Statuses []int16 `json:"statuses"`
UserIds []pgtype.UUID `json:"user_ids"`
SearchText pgtype.Text `json:"search_text"`
CreatedFrom pgtype.Timestamptz `json:"created_from"`
@@ -410,20 +544,20 @@ type SearchProjectsParams struct {
}
type SearchProjectsRow struct {
ID pgtype.UUID `json:"id"`
Title string `json:"title"`
Description pgtype.Text `json:"description"`
LatestRevisionID pgtype.UUID `json:"latest_revision_id"`
VersionCount int32 `json:"version_count"`
ProjectStatus int16 `json:"project_status"`
LockedBy pgtype.UUID `json:"locked_by"`
IsDeleted bool `json:"is_deleted"`
UserID pgtype.UUID `json:"user_id"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
CommitIds []pgtype.UUID `json:"commit_ids"`
SubmissionIds []pgtype.UUID `json:"submission_ids"`
User []byte `json:"user"`
ID pgtype.UUID `json:"id"`
Title string `json:"title"`
Description pgtype.Text `json:"description"`
LatestCommitID pgtype.UUID `json:"latest_commit_id"`
ProjectStatus int16 `json:"project_status"`
LockedBy pgtype.UUID `json:"locked_by"`
IsDeleted bool `json:"is_deleted"`
UserID pgtype.UUID `json:"user_id"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
Commits []byte `json:"commits"`
SubmissionIds []pgtype.UUID `json:"submission_ids"`
User []byte `json:"user"`
Members []byte `json:"members"`
}
func (q *Queries) SearchProjects(ctx context.Context, arg SearchProjectsParams) ([]SearchProjectsRow, error) {
@@ -449,17 +583,17 @@ func (q *Queries) SearchProjects(ctx context.Context, arg SearchProjectsParams)
&i.ID,
&i.Title,
&i.Description,
&i.LatestRevisionID,
&i.VersionCount,
&i.LatestCommitID,
&i.ProjectStatus,
&i.LockedBy,
&i.IsDeleted,
&i.UserID,
&i.CreatedAt,
&i.UpdatedAt,
&i.CommitIds,
&i.Commits,
&i.SubmissionIds,
&i.User,
&i.Members,
); err != nil {
return nil, err
}
@@ -471,22 +605,37 @@ func (q *Queries) SearchProjects(ctx context.Context, arg SearchProjectsParams)
return items, nil
}
const updateLatestCommit = `-- name: UpdateLatestCommit :exec
UPDATE projects
SET latest_commit_id = $2, updated_at = NOW()
WHERE id = $1 AND is_deleted = false
`
type UpdateLatestCommitParams struct {
ID pgtype.UUID `json:"id"`
LatestCommitID pgtype.UUID `json:"latest_commit_id"`
}
func (q *Queries) UpdateLatestCommit(ctx context.Context, arg UpdateLatestCommitParams) error {
_, err := q.db.Exec(ctx, updateLatestCommit, arg.ID, arg.LatestCommitID)
return err
}
const updateProject = `-- name: UpdateProject :one
UPDATE projects
SET
title = COALESCE($1, title),
description = COALESCE($2, description),
latest_revision_id = COALESCE($3, latest_revision_id),
version_count = COALESCE($4, version_count),
project_status = COALESCE($5, status),
locked_by = COALESCE($6, locked_by),
latest_commit_id = COALESCE($3, latest_commit_id),
project_status = COALESCE($4, status),
locked_by = COALESCE($5, locked_by),
updated_at = NOW()
FROM projects p
JOIN users u ON p.user_id = u.id
LEFT JOIN user_profiles up ON u.id = up.user_id
WHERE p.id = $7 AND p.is_deleted = false
WHERE p.id = $6 AND p.is_deleted = false
RETURNING
p.id, p.title, p.description, p.latest_revision_id, p.version_count, p.project_status, p.locked_by, p.is_deleted, p.user_id, p.created_at, p.updated_at,
p.id, p.title, p.description, p.latest_commit_id, p.project_status, p.locked_by, p.is_deleted, p.user_id, p.created_at, p.updated_at,
json_build_object(
'id', u.id,
'email', u.email,
@@ -494,43 +643,56 @@ RETURNING
'full_name', up.full_name,
'avatar_url', up.avatar_url
)::json AS user,
COALESCE((SELECT array_agg(id) FROM revisions WHERE project_id = projects.id), '{}')::uuid[] AS commit_ids,
COALESCE((SELECT array_agg(id) FROM submissions WHERE project_id = projects.id), '{}')::uuid[] AS submission_ids
COALESCE(
(SELECT json_agg(json_build_object('id', c.id, 'edit_summary', c.edit_summary) ORDER BY c.created_at DESC)
FROM commits c WHERE c.project_id = projects.id AND c.is_deleted = false),
'[]'
)::json AS commits,
COALESCE((SELECT array_agg(id) FROM submissions WHERE project_id = projects.id), '{}')::uuid[] AS submission_ids,
COALESCE(
(SELECT json_agg(json_build_object(
'user_id', pm.user_id, 'role', pm.role,
'display_name', mup.display_name, 'avatar_url', mup.avatar_url
) ORDER BY pm.role ASC, pm.created_at ASC)
FROM project_members pm
JOIN users mu ON pm.user_id = mu.id
LEFT JOIN user_profiles mup ON mu.id = mup.user_id
WHERE pm.project_id = projects.id),
'[]'
)::json AS members
`
type UpdateProjectParams struct {
Title pgtype.Text `json:"title"`
Description pgtype.Text `json:"description"`
LatestRevisionID pgtype.UUID `json:"latest_revision_id"`
VersionCount pgtype.Int4 `json:"version_count"`
Status pgtype.Int2 `json:"status"`
LockedBy pgtype.UUID `json:"locked_by"`
ID pgtype.UUID `json:"id"`
Title pgtype.Text `json:"title"`
Description pgtype.Text `json:"description"`
LatestCommitID pgtype.UUID `json:"latest_commit_id"`
Status pgtype.Int2 `json:"status"`
LockedBy pgtype.UUID `json:"locked_by"`
ID pgtype.UUID `json:"id"`
}
type UpdateProjectRow struct {
ID pgtype.UUID `json:"id"`
Title string `json:"title"`
Description pgtype.Text `json:"description"`
LatestRevisionID pgtype.UUID `json:"latest_revision_id"`
VersionCount int32 `json:"version_count"`
ProjectStatus int16 `json:"project_status"`
LockedBy pgtype.UUID `json:"locked_by"`
IsDeleted bool `json:"is_deleted"`
UserID pgtype.UUID `json:"user_id"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
User []byte `json:"user"`
CommitIds []pgtype.UUID `json:"commit_ids"`
SubmissionIds []pgtype.UUID `json:"submission_ids"`
ID pgtype.UUID `json:"id"`
Title string `json:"title"`
Description pgtype.Text `json:"description"`
LatestCommitID pgtype.UUID `json:"latest_commit_id"`
ProjectStatus int16 `json:"project_status"`
LockedBy pgtype.UUID `json:"locked_by"`
IsDeleted bool `json:"is_deleted"`
UserID pgtype.UUID `json:"user_id"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
User []byte `json:"user"`
Commits []byte `json:"commits"`
SubmissionIds []pgtype.UUID `json:"submission_ids"`
Members []byte `json:"members"`
}
func (q *Queries) UpdateProject(ctx context.Context, arg UpdateProjectParams) (UpdateProjectRow, error) {
row := q.db.QueryRow(ctx, updateProject,
arg.Title,
arg.Description,
arg.LatestRevisionID,
arg.VersionCount,
arg.LatestCommitID,
arg.Status,
arg.LockedBy,
arg.ID,
@@ -540,8 +702,7 @@ func (q *Queries) UpdateProject(ctx context.Context, arg UpdateProjectParams) (U
&i.ID,
&i.Title,
&i.Description,
&i.LatestRevisionID,
&i.VersionCount,
&i.LatestCommitID,
&i.ProjectStatus,
&i.LockedBy,
&i.IsDeleted,
@@ -549,8 +710,35 @@ func (q *Queries) UpdateProject(ctx context.Context, arg UpdateProjectParams) (U
&i.CreatedAt,
&i.UpdatedAt,
&i.User,
&i.CommitIds,
&i.Commits,
&i.SubmissionIds,
&i.Members,
)
return i, err
}
const updateProjectMemberRole = `-- name: UpdateProjectMemberRole :one
UPDATE project_members
SET role = $3
WHERE project_id = $1 AND user_id = $2
RETURNING project_id, user_id, role, invited_by, created_at
`
type UpdateProjectMemberRoleParams struct {
ProjectID pgtype.UUID `json:"project_id"`
UserID pgtype.UUID `json:"user_id"`
Role int16 `json:"role"`
}
func (q *Queries) UpdateProjectMemberRole(ctx context.Context, arg UpdateProjectMemberRoleParams) (ProjectMember, error) {
row := q.db.QueryRow(ctx, updateProjectMemberRole, arg.ProjectID, arg.UserID, arg.Role)
var i ProjectMember
err := row.Scan(
&i.ProjectID,
&i.UserID,
&i.Role,
&i.InvitedBy,
&i.CreatedAt,
)
return i, err
}

View File

@@ -1,182 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: revision.sql
package sqlc
import (
"context"
"encoding/json"
"github.com/jackc/pgx/v5/pgtype"
)
const createRevision = `-- name: CreateRevision :one
INSERT INTO revisions (
project_id, version_no, snapshot_json, snapshot_hash, parent_id, user_id, edit_summary
) VALUES (
$1, $2, $3, $4, $5, $6, $7
)
RETURNING id, project_id, version_no, snapshot_json, snapshot_hash, parent_id, user_id, edit_summary, is_deleted, created_at
`
type CreateRevisionParams struct {
ProjectID pgtype.UUID `json:"project_id"`
VersionNo int32 `json:"version_no"`
SnapshotJson json.RawMessage `json:"snapshot_json"`
SnapshotHash pgtype.Text `json:"snapshot_hash"`
ParentID pgtype.UUID `json:"parent_id"`
UserID pgtype.UUID `json:"user_id"`
EditSummary pgtype.Text `json:"edit_summary"`
}
func (q *Queries) CreateRevision(ctx context.Context, arg CreateRevisionParams) (Revision, error) {
row := q.db.QueryRow(ctx, createRevision,
arg.ProjectID,
arg.VersionNo,
arg.SnapshotJson,
arg.SnapshotHash,
arg.ParentID,
arg.UserID,
arg.EditSummary,
)
var i Revision
err := row.Scan(
&i.ID,
&i.ProjectID,
&i.VersionNo,
&i.SnapshotJson,
&i.SnapshotHash,
&i.ParentID,
&i.UserID,
&i.EditSummary,
&i.IsDeleted,
&i.CreatedAt,
)
return i, err
}
const deleteRevision = `-- name: DeleteRevision :exec
UPDATE revisions
SET is_deleted = true
WHERE id = $1
`
func (q *Queries) DeleteRevision(ctx context.Context, id pgtype.UUID) error {
_, err := q.db.Exec(ctx, deleteRevision, id)
return err
}
const getRevisionById = `-- name: GetRevisionById :one
SELECT id, project_id, version_no, snapshot_json, snapshot_hash, parent_id, user_id, edit_summary, is_deleted, created_at
FROM revisions
WHERE id = $1 AND is_deleted = false
`
func (q *Queries) GetRevisionById(ctx context.Context, id pgtype.UUID) (Revision, error) {
row := q.db.QueryRow(ctx, getRevisionById, id)
var i Revision
err := row.Scan(
&i.ID,
&i.ProjectID,
&i.VersionNo,
&i.SnapshotJson,
&i.SnapshotHash,
&i.ParentID,
&i.UserID,
&i.EditSummary,
&i.IsDeleted,
&i.CreatedAt,
)
return i, err
}
const getRevisionsByIDs = `-- name: GetRevisionsByIDs :many
SELECT id, project_id, version_no, snapshot_json, snapshot_hash, parent_id, user_id, edit_summary, is_deleted, created_at FROM revisions WHERE id = ANY($1::uuid[]) AND is_deleted = false
`
func (q *Queries) GetRevisionsByIDs(ctx context.Context, dollar_1 []pgtype.UUID) ([]Revision, error) {
rows, err := q.db.Query(ctx, getRevisionsByIDs, dollar_1)
if err != nil {
return nil, err
}
defer rows.Close()
items := []Revision{}
for rows.Next() {
var i Revision
if err := rows.Scan(
&i.ID,
&i.ProjectID,
&i.VersionNo,
&i.SnapshotJson,
&i.SnapshotHash,
&i.ParentID,
&i.UserID,
&i.EditSummary,
&i.IsDeleted,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const searchRevisions = `-- name: SearchRevisions :many
SELECT id, project_id, version_no, snapshot_json, snapshot_hash, parent_id, user_id, edit_summary, is_deleted, created_at
FROM revisions
WHERE is_deleted = false
AND ($1::uuid IS NULL OR project_id = $1)
AND ($2::uuid IS NULL OR user_id = $2)
AND ($3::uuid IS NULL OR id < $3::uuid)
ORDER BY version_no DESC
LIMIT $4
`
type SearchRevisionsParams struct {
ProjectID pgtype.UUID `json:"project_id"`
UserID pgtype.UUID `json:"user_id"`
CursorID pgtype.UUID `json:"cursor_id"`
Limit int32 `json:"limit"`
}
func (q *Queries) SearchRevisions(ctx context.Context, arg SearchRevisionsParams) ([]Revision, error) {
rows, err := q.db.Query(ctx, searchRevisions,
arg.ProjectID,
arg.UserID,
arg.CursorID,
arg.Limit,
)
if err != nil {
return nil, err
}
defer rows.Close()
items := []Revision{}
for rows.Next() {
var i Revision
if err := rows.Scan(
&i.ID,
&i.ProjectID,
&i.VersionNo,
&i.SnapshotJson,
&i.SnapshotHash,
&i.ParentID,
&i.UserID,
&i.EditSummary,
&i.IsDeleted,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}

View File

@@ -16,26 +16,27 @@ SELECT count(*)
FROM submissions s
WHERE s.is_deleted = false
AND ($1::uuid IS NULL OR s.project_id = $1)
AND ($2::uuid IS NULL OR s.submitted_by = $2)
AND ($2::uuid[] IS NULL OR uv.user_id = ANY($2::uuid[]))
AND ($3::uuid IS NULL OR s.reviewed_by = $3)
AND (
$4::text[] IS NULL
OR s.status = ANY($4::text[])
$4::smallint[] IS NULL
OR s.status = ANY($4::smallint[])
)
AND ($5::timestamptz IS NULL OR s.submitted_at >= $5::timestamptz)
AND ($6::timestamptz IS NULL OR s.submitted_at <= $6::timestamptz)
AND ($5::timestamptz IS NULL OR s.created_at >= $5::timestamptz)
AND ($6::timestamptz IS NULL OR s.created_at <= $6::timestamptz)
AND (
$7::text IS NULL OR
s.id::text ILIKE '%' || $7::text || '%' OR
s.review_note ILIKE '%' || $7::text || '%'
s.review_note ILIKE '%' || $7::text || '%' OR
s.content ILIKE '%' || $7::text || '%'
)
`
type CountSubmissionsParams struct {
ProjectID pgtype.UUID `json:"project_id"`
SubmittedBy pgtype.UUID `json:"submitted_by"`
UserIds []pgtype.UUID `json:"user_ids"`
ReviewedBy pgtype.UUID `json:"reviewed_by"`
Statuses []string `json:"statuses"`
Statuses []int16 `json:"statuses"`
CreatedFrom pgtype.Timestamptz `json:"created_from"`
CreatedTo pgtype.Timestamptz `json:"created_to"`
SearchText pgtype.Text `json:"search_text"`
@@ -44,7 +45,7 @@ type CountSubmissionsParams struct {
func (q *Queries) CountSubmissions(ctx context.Context, arg CountSubmissionsParams) (int64, error) {
row := q.db.QueryRow(ctx, countSubmissions,
arg.ProjectID,
arg.SubmittedBy,
arg.UserIds,
arg.ReviewedBy,
arg.Statuses,
arg.CreatedFrom,
@@ -57,81 +58,85 @@ func (q *Queries) CountSubmissions(ctx context.Context, arg CountSubmissionsPara
}
const createSubmission = `-- name: CreateSubmission :one
WITH inserted_submission AS (
INSERT INTO submissions (
project_id, revision_id, submitted_by, status
) VALUES (
$1, $2, $3, $4
)
RETURNING id, project_id, revision_id, submitted_by, submitted_at, status, reviewed_by, reviewed_at, review_note, is_deleted
INSERT INTO submissions (
project_id, commit_id, user_id, status, content
) VALUES (
$1, $2, $3, $4, $5
)
SELECT
s.id, s.project_id, s.revision_id, s.submitted_by, s.submitted_at, s.status, s.reviewed_by, s.reviewed_at, s.review_note, s.is_deleted,
json_build_object(
'id', u.id,
'email', u.email,
'display_name', up.display_name,
'full_name', up.full_name,
'avatar_url', up.avatar_url
)::json AS submitter,
CASE WHEN s.reviewed_by IS NOT NULL THEN
json_build_object(
RETURNING
id, project_id, commit_id, user_id, created_at, status, reviewed_by, reviewed_at, review_note, content, is_deleted,
(
SELECT json_build_object(
'id', u.id,
'email', u.email,
'display_name', up.display_name,
'full_name', up.full_name,
'avatar_url', up.avatar_url
)
FROM users u
LEFT JOIN user_profiles up ON u.id = up.user_id
WHERE u.id = submissions.user_id
)::json AS user,
(
SELECT json_build_object(
'id', ru.id,
'email', ru.email,
'display_name', rup.display_name,
'full_name', rup.full_name,
'avatar_url', rup.avatar_url
)::json
ELSE NULL::json END AS reviewer
FROM inserted_submission s
JOIN users u ON s.submitted_by = u.id
LEFT JOIN user_profiles up ON u.id = up.user_id
LEFT JOIN users ru ON s.reviewed_by = ru.id
LEFT JOIN user_profiles rup ON ru.id = rup.user_id
)
FROM users ru
LEFT JOIN user_profiles rup ON ru.id = rup.user_id
WHERE ru.id = submissions.reviewed_by
)::json AS reviewer
`
type CreateSubmissionParams struct {
ProjectID pgtype.UUID `json:"project_id"`
RevisionID pgtype.UUID `json:"revision_id"`
SubmittedBy pgtype.UUID `json:"submitted_by"`
Status int16 `json:"status"`
ProjectID pgtype.UUID `json:"project_id"`
CommitID pgtype.UUID `json:"commit_id"`
UserID pgtype.UUID `json:"user_id"`
Status int16 `json:"status"`
Content pgtype.Text `json:"content"`
}
type CreateSubmissionRow struct {
ID pgtype.UUID `json:"id"`
ProjectID pgtype.UUID `json:"project_id"`
RevisionID pgtype.UUID `json:"revision_id"`
SubmittedBy pgtype.UUID `json:"submitted_by"`
SubmittedAt pgtype.Timestamptz `json:"submitted_at"`
Status int16 `json:"status"`
ReviewedBy pgtype.UUID `json:"reviewed_by"`
ReviewedAt pgtype.Timestamptz `json:"reviewed_at"`
ReviewNote pgtype.Text `json:"review_note"`
IsDeleted bool `json:"is_deleted"`
Submitter []byte `json:"submitter"`
Reviewer []byte `json:"reviewer"`
ID pgtype.UUID `json:"id"`
ProjectID pgtype.UUID `json:"project_id"`
CommitID pgtype.UUID `json:"commit_id"`
UserID pgtype.UUID `json:"user_id"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
Status int16 `json:"status"`
ReviewedBy pgtype.UUID `json:"reviewed_by"`
ReviewedAt pgtype.Timestamptz `json:"reviewed_at"`
ReviewNote pgtype.Text `json:"review_note"`
Content pgtype.Text `json:"content"`
IsDeleted bool `json:"is_deleted"`
User []byte `json:"user"`
Reviewer []byte `json:"reviewer"`
}
func (q *Queries) CreateSubmission(ctx context.Context, arg CreateSubmissionParams) (CreateSubmissionRow, error) {
row := q.db.QueryRow(ctx, createSubmission,
arg.ProjectID,
arg.RevisionID,
arg.SubmittedBy,
arg.CommitID,
arg.UserID,
arg.Status,
arg.Content,
)
var i CreateSubmissionRow
err := row.Scan(
&i.ID,
&i.ProjectID,
&i.RevisionID,
&i.SubmittedBy,
&i.SubmittedAt,
&i.CommitID,
&i.UserID,
&i.CreatedAt,
&i.Status,
&i.ReviewedBy,
&i.ReviewedAt,
&i.ReviewNote,
&i.Content,
&i.IsDeleted,
&i.Submitter,
&i.User,
&i.Reviewer,
)
return i, err
@@ -150,14 +155,14 @@ func (q *Queries) DeleteSubmission(ctx context.Context, id pgtype.UUID) error {
const getSubmissionById = `-- name: GetSubmissionById :one
SELECT
s.id, s.project_id, s.revision_id, s.submitted_by, s.submitted_at, s.status, s.reviewed_by, s.reviewed_at, s.review_note, s.is_deleted,
s.id, s.project_id, s.commit_id, s.user_id, s.created_at, s.status, s.reviewed_by, s.reviewed_at, s.review_note, s.content, s.is_deleted,
json_build_object(
'id', u.id,
'email', u.email,
'display_name', up.display_name,
'full_name', up.full_name,
'avatar_url', up.avatar_url
)::json AS submitter,
)::json AS user,
CASE WHEN s.reviewed_by IS NOT NULL THEN
json_build_object(
'id', ru.id,
@@ -168,7 +173,7 @@ SELECT
)::json
ELSE NULL::json END AS reviewer
FROM submissions s
JOIN users u ON s.submitted_by = u.id
JOIN users u ON s.user_id = u.id
LEFT JOIN user_profiles up ON u.id = up.user_id
LEFT JOIN users ru ON s.reviewed_by = ru.id
LEFT JOIN user_profiles rup ON ru.id = rup.user_id
@@ -176,18 +181,19 @@ WHERE s.id = $1 AND s.is_deleted = false
`
type GetSubmissionByIdRow struct {
ID pgtype.UUID `json:"id"`
ProjectID pgtype.UUID `json:"project_id"`
RevisionID pgtype.UUID `json:"revision_id"`
SubmittedBy pgtype.UUID `json:"submitted_by"`
SubmittedAt pgtype.Timestamptz `json:"submitted_at"`
Status int16 `json:"status"`
ReviewedBy pgtype.UUID `json:"reviewed_by"`
ReviewedAt pgtype.Timestamptz `json:"reviewed_at"`
ReviewNote pgtype.Text `json:"review_note"`
IsDeleted bool `json:"is_deleted"`
Submitter []byte `json:"submitter"`
Reviewer []byte `json:"reviewer"`
ID pgtype.UUID `json:"id"`
ProjectID pgtype.UUID `json:"project_id"`
CommitID pgtype.UUID `json:"commit_id"`
UserID pgtype.UUID `json:"user_id"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
Status int16 `json:"status"`
ReviewedBy pgtype.UUID `json:"reviewed_by"`
ReviewedAt pgtype.Timestamptz `json:"reviewed_at"`
ReviewNote pgtype.Text `json:"review_note"`
Content pgtype.Text `json:"content"`
IsDeleted bool `json:"is_deleted"`
User []byte `json:"user"`
Reviewer []byte `json:"reviewer"`
}
func (q *Queries) GetSubmissionById(ctx context.Context, id pgtype.UUID) (GetSubmissionByIdRow, error) {
@@ -196,15 +202,16 @@ func (q *Queries) GetSubmissionById(ctx context.Context, id pgtype.UUID) (GetSub
err := row.Scan(
&i.ID,
&i.ProjectID,
&i.RevisionID,
&i.SubmittedBy,
&i.SubmittedAt,
&i.CommitID,
&i.UserID,
&i.CreatedAt,
&i.Status,
&i.ReviewedBy,
&i.ReviewedAt,
&i.ReviewNote,
&i.Content,
&i.IsDeleted,
&i.Submitter,
&i.User,
&i.Reviewer,
)
return i, err
@@ -212,14 +219,14 @@ func (q *Queries) GetSubmissionById(ctx context.Context, id pgtype.UUID) (GetSub
const getSubmissionsByIDs = `-- name: GetSubmissionsByIDs :many
SELECT
s.id, s.project_id, s.revision_id, s.submitted_by, s.submitted_at, s.status, s.reviewed_by, s.reviewed_at, s.review_note, s.is_deleted,
s.id, s.project_id, s.commit_id, s.user_id, s.created_at, s.status, s.reviewed_by, s.reviewed_at, s.review_note, s.content, s.is_deleted,
json_build_object(
'id', u.id,
'email', u.email,
'display_name', up.display_name,
'full_name', up.full_name,
'avatar_url', up.avatar_url
)::json AS submitter,
)::json AS user,
CASE WHEN s.reviewed_by IS NOT NULL THEN
json_build_object(
'id', ru.id,
@@ -230,7 +237,7 @@ SELECT
)::json
ELSE NULL::json END AS reviewer
FROM submissions s
JOIN users u ON s.submitted_by = u.id
JOIN users u ON s.user_id = u.id
LEFT JOIN user_profiles up ON u.id = up.user_id
LEFT JOIN users ru ON s.reviewed_by = ru.id
LEFT JOIN user_profiles rup ON ru.id = rup.user_id
@@ -238,18 +245,19 @@ WHERE s.id = ANY($1::uuid[]) AND s.is_deleted = false
`
type GetSubmissionsByIDsRow struct {
ID pgtype.UUID `json:"id"`
ProjectID pgtype.UUID `json:"project_id"`
RevisionID pgtype.UUID `json:"revision_id"`
SubmittedBy pgtype.UUID `json:"submitted_by"`
SubmittedAt pgtype.Timestamptz `json:"submitted_at"`
Status int16 `json:"status"`
ReviewedBy pgtype.UUID `json:"reviewed_by"`
ReviewedAt pgtype.Timestamptz `json:"reviewed_at"`
ReviewNote pgtype.Text `json:"review_note"`
IsDeleted bool `json:"is_deleted"`
Submitter []byte `json:"submitter"`
Reviewer []byte `json:"reviewer"`
ID pgtype.UUID `json:"id"`
ProjectID pgtype.UUID `json:"project_id"`
CommitID pgtype.UUID `json:"commit_id"`
UserID pgtype.UUID `json:"user_id"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
Status int16 `json:"status"`
ReviewedBy pgtype.UUID `json:"reviewed_by"`
ReviewedAt pgtype.Timestamptz `json:"reviewed_at"`
ReviewNote pgtype.Text `json:"review_note"`
Content pgtype.Text `json:"content"`
IsDeleted bool `json:"is_deleted"`
User []byte `json:"user"`
Reviewer []byte `json:"reviewer"`
}
func (q *Queries) GetSubmissionsByIDs(ctx context.Context, dollar_1 []pgtype.UUID) ([]GetSubmissionsByIDsRow, error) {
@@ -264,15 +272,16 @@ func (q *Queries) GetSubmissionsByIDs(ctx context.Context, dollar_1 []pgtype.UUI
if err := rows.Scan(
&i.ID,
&i.ProjectID,
&i.RevisionID,
&i.SubmittedBy,
&i.SubmittedAt,
&i.CommitID,
&i.UserID,
&i.CreatedAt,
&i.Status,
&i.ReviewedBy,
&i.ReviewedAt,
&i.ReviewNote,
&i.Content,
&i.IsDeleted,
&i.Submitter,
&i.User,
&i.Reviewer,
); err != nil {
return nil, err
@@ -287,14 +296,14 @@ func (q *Queries) GetSubmissionsByIDs(ctx context.Context, dollar_1 []pgtype.UUI
const searchSubmissions = `-- name: SearchSubmissions :many
SELECT
s.id, s.project_id, s.revision_id, s.submitted_by, s.submitted_at, s.status, s.reviewed_by, s.reviewed_at, s.review_note, s.is_deleted,
s.id, s.project_id, s.commit_id, s.user_id, s.created_at, s.status, s.reviewed_by, s.reviewed_at, s.review_note, s.content, s.is_deleted,
json_build_object(
'id', u.id,
'email', u.email,
'display_name', up.display_name,
'full_name', up.full_name,
'avatar_url', up.avatar_url
)::json AS submitter,
)::json AS user,
CASE WHEN s.reviewed_by IS NOT NULL THEN
json_build_object(
'id', ru.id,
@@ -305,42 +314,43 @@ SELECT
)::json
ELSE NULL::json END AS reviewer
FROM submissions s
JOIN users u ON s.submitted_by = u.id
JOIN users u ON s.user_id = u.id
LEFT JOIN user_profiles up ON u.id = up.user_id
LEFT JOIN users ru ON s.reviewed_by = ru.id
LEFT JOIN user_profiles rup ON ru.id = rup.user_id
WHERE s.is_deleted = false
AND ($1::uuid IS NULL OR s.project_id = $1)
AND ($2::uuid IS NULL OR s.submitted_by = $2)
AND ($2::uuid[] IS NULL OR uv.user_id = ANY($2::uuid[]))
AND ($3::uuid IS NULL OR s.reviewed_by = $3)
AND (
$4::text[] IS NULL
OR s.status = ANY($4::text[])
$4::smallint[] IS NULL
OR s.status = ANY($4::smallint[])
)
AND ($5::timestamptz IS NULL OR s.submitted_at >= $5::timestamptz)
AND ($6::timestamptz IS NULL OR s.submitted_at <= $6::timestamptz)
AND ($5::timestamptz IS NULL OR s.created_at >= $5::timestamptz)
AND ($6::timestamptz IS NULL OR s.created_at <= $6::timestamptz)
AND (
$7::text IS NULL OR
s.id::text ILIKE '%' || $7::text || '%' OR
s.review_note ILIKE '%' || $7::text || '%'
s.review_note ILIKE '%' || $7::text || '%' OR
s.content ILIKE '%' || $7::text || '%'
)
ORDER BY
CASE WHEN $8 = 'submitted_at' AND $9 = 'asc' THEN s.submitted_at END ASC,
CASE WHEN $8 = 'submitted_at' AND $9 = 'desc' THEN s.submitted_at END DESC,
CASE WHEN $8 = 'created_at' AND $9 = 'asc' THEN s.created_at END ASC,
CASE WHEN $8 = 'created_at' AND $9 = 'desc' THEN s.created_at END DESC,
CASE WHEN $8 = 'reviewed_at' AND $9 = 'asc' THEN s.reviewed_at END ASC,
CASE WHEN $8 = 'reviewed_at' AND $9 = 'desc' THEN s.reviewed_at END DESC,
CASE WHEN $8 = 'status' AND $9 = 'asc' THEN s.status END ASC,
CASE WHEN $8 = 'status' AND $9 = 'desc' THEN s.status END DESC,
CASE WHEN $8 IS NULL THEN s.submitted_at END DESC
CASE WHEN $8 IS NULL THEN s.created_at END DESC
LIMIT $11
OFFSET $10
`
type SearchSubmissionsParams struct {
ProjectID pgtype.UUID `json:"project_id"`
SubmittedBy pgtype.UUID `json:"submitted_by"`
UserIds []pgtype.UUID `json:"user_ids"`
ReviewedBy pgtype.UUID `json:"reviewed_by"`
Statuses []string `json:"statuses"`
Statuses []int16 `json:"statuses"`
CreatedFrom pgtype.Timestamptz `json:"created_from"`
CreatedTo pgtype.Timestamptz `json:"created_to"`
SearchText pgtype.Text `json:"search_text"`
@@ -351,24 +361,25 @@ type SearchSubmissionsParams struct {
}
type SearchSubmissionsRow struct {
ID pgtype.UUID `json:"id"`
ProjectID pgtype.UUID `json:"project_id"`
RevisionID pgtype.UUID `json:"revision_id"`
SubmittedBy pgtype.UUID `json:"submitted_by"`
SubmittedAt pgtype.Timestamptz `json:"submitted_at"`
Status int16 `json:"status"`
ReviewedBy pgtype.UUID `json:"reviewed_by"`
ReviewedAt pgtype.Timestamptz `json:"reviewed_at"`
ReviewNote pgtype.Text `json:"review_note"`
IsDeleted bool `json:"is_deleted"`
Submitter []byte `json:"submitter"`
Reviewer []byte `json:"reviewer"`
ID pgtype.UUID `json:"id"`
ProjectID pgtype.UUID `json:"project_id"`
CommitID pgtype.UUID `json:"commit_id"`
UserID pgtype.UUID `json:"user_id"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
Status int16 `json:"status"`
ReviewedBy pgtype.UUID `json:"reviewed_by"`
ReviewedAt pgtype.Timestamptz `json:"reviewed_at"`
ReviewNote pgtype.Text `json:"review_note"`
Content pgtype.Text `json:"content"`
IsDeleted bool `json:"is_deleted"`
User []byte `json:"user"`
Reviewer []byte `json:"reviewer"`
}
func (q *Queries) SearchSubmissions(ctx context.Context, arg SearchSubmissionsParams) ([]SearchSubmissionsRow, error) {
rows, err := q.db.Query(ctx, searchSubmissions,
arg.ProjectID,
arg.SubmittedBy,
arg.UserIds,
arg.ReviewedBy,
arg.Statuses,
arg.CreatedFrom,
@@ -389,15 +400,16 @@ func (q *Queries) SearchSubmissions(ctx context.Context, arg SearchSubmissionsPa
if err := rows.Scan(
&i.ID,
&i.ProjectID,
&i.RevisionID,
&i.SubmittedBy,
&i.SubmittedAt,
&i.CommitID,
&i.UserID,
&i.CreatedAt,
&i.Status,
&i.ReviewedBy,
&i.ReviewedAt,
&i.ReviewNote,
&i.Content,
&i.IsDeleted,
&i.Submitter,
&i.User,
&i.Reviewer,
); err != nil {
return nil, err
@@ -415,23 +427,23 @@ UPDATE submissions
SET
status = COALESCE($1, status),
reviewed_by = COALESCE($2, reviewed_by),
reviewed_at = COALESCE($3, reviewed_at),
review_note = COALESCE($4, review_note)
review_note = COALESCE($3, review_note),
content = COALESCE($4, content)
FROM submissions s
JOIN users u ON s.submitted_by = u.id
JOIN users u ON s.user_id = u.id
LEFT JOIN user_profiles up ON u.id = up.user_id
LEFT JOIN users ru ON s.reviewed_by = ru.id
LEFT JOIN user_profiles rup ON ru.id = rup.user_id
WHERE s.id = $5
RETURNING
s.id, s.project_id, s.revision_id, s.submitted_by, s.submitted_at, s.status, s.reviewed_by, s.reviewed_at, s.review_note, s.is_deleted,
s.id, s.project_id, s.commit_id, s.user_id, s.created_at, s.status, s.reviewed_by, s.reviewed_at, s.review_note, s.content, s.is_deleted,
json_build_object(
'id', u.id,
'email', u.email,
'display_name', up.display_name,
'full_name', up.full_name,
'avatar_url', up.avatar_url
)::json AS submitter,
)::json AS user,
CASE WHEN s.reviewed_by IS NOT NULL THEN
json_build_object(
'id', ru.id,
@@ -444,49 +456,51 @@ RETURNING
`
type UpdateSubmissionParams struct {
Status pgtype.Int2 `json:"status"`
ReviewedBy pgtype.UUID `json:"reviewed_by"`
ReviewedAt pgtype.Timestamptz `json:"reviewed_at"`
ReviewNote pgtype.Text `json:"review_note"`
ID pgtype.UUID `json:"id"`
Status pgtype.Int2 `json:"status"`
ReviewedBy pgtype.UUID `json:"reviewed_by"`
ReviewNote pgtype.Text `json:"review_note"`
Content pgtype.Text `json:"content"`
ID pgtype.UUID `json:"id"`
}
type UpdateSubmissionRow struct {
ID pgtype.UUID `json:"id"`
ProjectID pgtype.UUID `json:"project_id"`
RevisionID pgtype.UUID `json:"revision_id"`
SubmittedBy pgtype.UUID `json:"submitted_by"`
SubmittedAt pgtype.Timestamptz `json:"submitted_at"`
Status int16 `json:"status"`
ReviewedBy pgtype.UUID `json:"reviewed_by"`
ReviewedAt pgtype.Timestamptz `json:"reviewed_at"`
ReviewNote pgtype.Text `json:"review_note"`
IsDeleted bool `json:"is_deleted"`
Submitter []byte `json:"submitter"`
Reviewer []byte `json:"reviewer"`
ID pgtype.UUID `json:"id"`
ProjectID pgtype.UUID `json:"project_id"`
CommitID pgtype.UUID `json:"commit_id"`
UserID pgtype.UUID `json:"user_id"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
Status int16 `json:"status"`
ReviewedBy pgtype.UUID `json:"reviewed_by"`
ReviewedAt pgtype.Timestamptz `json:"reviewed_at"`
ReviewNote pgtype.Text `json:"review_note"`
Content pgtype.Text `json:"content"`
IsDeleted bool `json:"is_deleted"`
User []byte `json:"user"`
Reviewer []byte `json:"reviewer"`
}
func (q *Queries) UpdateSubmission(ctx context.Context, arg UpdateSubmissionParams) (UpdateSubmissionRow, error) {
row := q.db.QueryRow(ctx, updateSubmission,
arg.Status,
arg.ReviewedBy,
arg.ReviewedAt,
arg.ReviewNote,
arg.Content,
arg.ID,
)
var i UpdateSubmissionRow
err := row.Scan(
&i.ID,
&i.ProjectID,
&i.RevisionID,
&i.SubmittedBy,
&i.SubmittedAt,
&i.CommitID,
&i.UserID,
&i.CreatedAt,
&i.Status,
&i.ReviewedBy,
&i.ReviewedAt,
&i.ReviewNote,
&i.Content,
&i.IsDeleted,
&i.Submitter,
&i.User,
&i.Reviewer,
)
return i, err

48
internal/models/commit.go Normal file
View File

@@ -0,0 +1,48 @@
package models
import (
"encoding/json"
"history-api/internal/dtos/response"
"time"
)
type CommitEntity struct {
ID string `json:"id"`
ProjectID string `json:"project_id"`
SnapshotJson json.RawMessage `json:"snapshot_json"`
SnapshotHash string `json:"snapshot_hash"`
UserID string `json:"user_id"`
EditSummary string `json:"edit_summary"`
IsDeleted bool `json:"is_deleted"`
CreatedAt *time.Time `json:"created_at"`
User *UserSimpleEntity `json:"user"`
}
func (c *CommitEntity) ToResponse() *response.CommitResponse {
if c == nil {
return nil
}
var userRes *response.UserSimpleResponse
if c.User != nil {
userRes = c.User.ToResponse()
}
return &response.CommitResponse{
ID: c.ID,
ProjectID: c.ProjectID,
SnapshotJson: c.SnapshotJson,
SnapshotHash: c.SnapshotHash,
UserID: c.UserID,
EditSummary: c.EditSummary,
CreatedAt: c.CreatedAt,
User: userRes,
}
}
func CommitsEntityToResponse(entities []*CommitEntity) []*response.CommitResponse {
res := make([]*response.CommitResponse, 0, len(entities))
for _, e := range entities {
res = append(res, e.ToResponse())
}
return res
}

View File

@@ -7,21 +7,33 @@ import (
"time"
)
type CommitSimple struct {
ID string `json:"id"`
EditSummary string `json:"edit_summary"`
}
type MemberSimple struct {
UserID string `json:"user_id"`
Role constants.ProjectMemberRole `json:"role"`
DisplayName string `json:"display_name"`
AvatarUrl string `json:"avatar_url"`
}
type ProjectEntity struct {
ID string `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
LatestRevisionID *string `json:"latest_revision_id"`
VersionCount int32 `json:"version_count"`
ProjectStatus constants.ProjectStatusType `json:"project_status"`
LockedBy *string `json:"locked_by"`
IsDeleted bool `json:"is_deleted"`
UserID string `json:"user_id"`
CreatedAt *time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at"`
User *UserSimpleEntity `json:"user"`
CommitIds []string `json:"commit_ids"`
SubmissionIds []string `json:"submission_ids"`
ID string `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
LatestCommitID *string `json:"latest_commit_id"`
ProjectStatus constants.ProjectStatusType `json:"project_status"`
LockedBy *string `json:"locked_by"`
IsDeleted bool `json:"is_deleted"`
UserID string `json:"user_id"`
CreatedAt *time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at"`
User *UserSimpleEntity `json:"user"`
Commits []CommitSimple `json:"commits"`
SubmissionIds []string `json:"submission_ids"`
Members []MemberSimple `json:"members"`
}
func (p *ProjectEntity) ParseUser(data []byte) error {
@@ -32,6 +44,22 @@ func (p *ProjectEntity) ParseUser(data []byte) error {
return json.Unmarshal(data, &p.User)
}
func (p *ProjectEntity) ParseCommits(data []byte) error {
if len(data) == 0 || string(data) == "null" || string(data) == "[]" {
p.Commits = []CommitSimple{}
return nil
}
return json.Unmarshal(data, &p.Commits)
}
func (p *ProjectEntity) ParseMembers(data []byte) error {
if len(data) == 0 || string(data) == "null" || string(data) == "[]" {
p.Members = []MemberSimple{}
return nil
}
return json.Unmarshal(data, &p.Members)
}
func (p *ProjectEntity) ToResponse() *response.ProjectResponse {
if p == nil {
return nil
@@ -41,21 +69,39 @@ func (p *ProjectEntity) ToResponse() *response.ProjectResponse {
userResponse = p.User.ToResponse()
}
commits := make([]response.CommitSimpleResponse, 0, len(p.Commits))
for _, c := range p.Commits {
commits = append(commits, response.CommitSimpleResponse{
ID: c.ID,
EditSummary: c.EditSummary,
})
}
members := make([]response.MemberSimpleResponse, 0, len(p.Members))
for _, m := range p.Members {
members = append(members, response.MemberSimpleResponse{
UserID: m.UserID,
Role: m.Role.String(),
DisplayName: m.DisplayName,
AvatarUrl: m.AvatarUrl,
})
}
return &response.ProjectResponse{
ID: p.ID,
Title: p.Title,
Description: p.Description,
LatestRevisionID: p.LatestRevisionID,
VersionCount: p.VersionCount,
ProjectStatus: p.ProjectStatus.String(),
LockedBy: p.LockedBy,
IsDeleted: p.IsDeleted,
UserID: p.UserID,
CreatedAt: p.CreatedAt,
UpdatedAt: p.UpdatedAt,
User: userResponse,
CommitIds: p.CommitIds,
SubmissionIds: p.SubmissionIds,
ID: p.ID,
Title: p.Title,
Description: p.Description,
LatestCommitID: p.LatestCommitID,
ProjectStatus: p.ProjectStatus.String(),
LockedBy: p.LockedBy,
IsDeleted: p.IsDeleted,
UserID: p.UserID,
CreatedAt: p.CreatedAt,
UpdatedAt: p.UpdatedAt,
User: userResponse,
Commits: commits,
SubmissionIds: p.SubmissionIds,
Members: members,
}
}

View File

@@ -0,0 +1,75 @@
package models
import (
"encoding/json"
"history-api/internal/dtos/response"
"history-api/pkg/constants"
"time"
)
type SubmissionEntity struct {
ID string `json:"id"`
ProjectID string `json:"project_id"`
CommitID string `json:"commit_id"`
UserID string `json:"user_id"`
CreatedAt *time.Time `json:"created_at"`
Status constants.StatusType `json:"status"`
ReviewedBy *string `json:"reviewed_by"`
ReviewedAt *time.Time `json:"reviewed_at"`
ReviewNote *string `json:"review_note"`
Content *string `json:"content"`
IsDeleted bool `json:"is_deleted"`
User *UserSimpleEntity `json:"user"`
Reviewer *UserSimpleEntity `json:"reviewer"`
}
func (s *SubmissionEntity) ParseUser(data []byte) error {
if len(data) == 0 || string(data) == "null" {
s.User = nil
return nil
}
return json.Unmarshal(data, &s.User)
}
func (s *SubmissionEntity) ParseReviewer(data []byte) error {
if len(data) == 0 || string(data) == "null" {
s.Reviewer = nil
return nil
}
return json.Unmarshal(data, &s.Reviewer)
}
func (s *SubmissionEntity) ToResponse() *response.SubmissionResponse {
if s == nil {
return nil
}
return &response.SubmissionResponse{
ID: s.ID,
ProjectID: s.ProjectID,
CommitID: s.CommitID,
UserID: s.UserID,
CreatedAt: s.CreatedAt,
Status: s.Status.String(),
ReviewedBy: s.ReviewedBy,
ReviewedAt: s.ReviewedAt,
ReviewNote: s.ReviewNote,
Content: s.Content,
User: s.User.ToResponse(),
Reviewer: s.Reviewer.ToResponse(),
}
}
func SubmissionsEntityToResponse(submissions []*SubmissionEntity) []*response.SubmissionResponse {
out := make([]*response.SubmissionResponse, 0)
if submissions == nil {
return out
}
for _, submission := range submissions {
if submission == nil {
continue
}
out = append(out, submission.ToResponse())
}
return out
}

View File

@@ -0,0 +1,230 @@
package repositories
import (
"context"
"crypto/md5"
"encoding/json"
"fmt"
"history-api/internal/gen/sqlc"
"history-api/internal/models"
"history-api/pkg/cache"
"history-api/pkg/constants"
"history-api/pkg/convert"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
)
type CommitRepository interface {
Create(ctx context.Context, params sqlc.CreateCommitParams) (*models.CommitEntity, error)
GetByID(ctx context.Context, id pgtype.UUID) (*models.CommitEntity, error)
GetByProjectID(ctx context.Context, projectID pgtype.UUID) ([]*models.CommitEntity, error)
Search(ctx context.Context, params sqlc.SearchCommitsParams) ([]*models.CommitEntity, error)
WithTx(tx pgx.Tx) CommitRepository
}
type commitRepository struct {
q *sqlc.Queries
c cache.Cache
}
func NewCommitRepository(db sqlc.DBTX, c cache.Cache) CommitRepository {
return &commitRepository{
q: sqlc.New(db),
c: c,
}
}
func (r *commitRepository) WithTx(tx pgx.Tx) CommitRepository {
return &commitRepository{
q: r.q.WithTx(tx),
c: r.c,
}
}
func (r *commitRepository) Create(ctx context.Context, params sqlc.CreateCommitParams) (*models.CommitEntity, error) {
row, err := r.q.CreateCommit(ctx, params)
if err != nil {
return nil, err
}
return &models.CommitEntity{
ID: convert.UUIDToString(row.ID),
ProjectID: convert.UUIDToString(row.ProjectID),
SnapshotJson: row.SnapshotJson,
SnapshotHash: convert.TextToString(row.SnapshotHash),
UserID: convert.UUIDToString(row.UserID),
EditSummary: convert.TextToString(row.EditSummary),
IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
}, nil
}
func (r *commitRepository) GetByID(ctx context.Context, id pgtype.UUID) (*models.CommitEntity, error) {
cacheId := fmt.Sprintf("commit:id:%s", convert.UUIDToString(id))
var commit models.CommitEntity
err := r.c.Get(ctx, cacheId, &commit)
if err == nil {
_ = r.c.Set(ctx, cacheId, commit, constants.NormalCacheDuration)
return &commit, nil
}
row, err := r.q.GetCommitById(ctx, id)
if err != nil {
return nil, err
}
commit = models.CommitEntity{
ID: convert.UUIDToString(row.ID),
ProjectID: convert.UUIDToString(row.ProjectID),
SnapshotJson: row.SnapshotJson,
SnapshotHash: convert.TextToString(row.SnapshotHash),
UserID: convert.UUIDToString(row.UserID),
EditSummary: convert.TextToString(row.EditSummary),
IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
}
_ = r.c.Set(ctx, cacheId, commit, constants.NormalCacheDuration)
return &commit, nil
}
func (r *commitRepository) generateQueryKey(prefix string, params any) string {
b, _ := json.Marshal(params)
hash := fmt.Sprintf("%x", md5.Sum(b))
return fmt.Sprintf("%s:query:%s", prefix, hash)
}
func (r *commitRepository) getByIDsWithFallback(ctx context.Context, ids []string) ([]*models.CommitEntity, error) {
if len(ids) == 0 {
return []*models.CommitEntity{}, nil
}
keys := make([]string, len(ids))
for i, id := range ids {
keys[i] = fmt.Sprintf("commit:id:%s", id)
}
raws := r.c.MGet(ctx, keys...)
var commits []*models.CommitEntity
missingToCache := make(map[string]any)
var missingPgIds []pgtype.UUID
for i, b := range raws {
if len(b) == 0 {
pgId := pgtype.UUID{}
err := pgId.Scan(ids[i])
if err == nil {
missingPgIds = append(missingPgIds, pgId)
}
}
}
dbMap := make(map[string]*models.CommitEntity)
if len(missingPgIds) > 0 {
dbRows, err := r.q.GetCommitsByIDs(ctx, missingPgIds)
if err == nil {
for _, row := range dbRows {
item := models.CommitEntity{
ID: convert.UUIDToString(row.ID),
ProjectID: convert.UUIDToString(row.ProjectID),
SnapshotJson: row.SnapshotJson,
SnapshotHash: convert.TextToString(row.SnapshotHash),
UserID: convert.UUIDToString(row.UserID),
EditSummary: convert.TextToString(row.EditSummary),
IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
}
dbMap[item.ID] = &item
}
}
}
for i, b := range raws {
if len(b) > 0 {
var c models.CommitEntity
if err := json.Unmarshal(b, &c); err == nil {
commits = append(commits, &c)
}
} else {
if item, ok := dbMap[ids[i]]; ok {
commits = append(commits, item)
missingToCache[keys[i]] = item
}
}
}
if len(missingToCache) > 0 {
_ = r.c.MSet(ctx, missingToCache, constants.NormalCacheDuration)
}
return commits, nil
}
func (r *commitRepository) GetByProjectID(ctx context.Context, projectID pgtype.UUID) ([]*models.CommitEntity, error) {
queryKey := fmt.Sprintf("commit:project:%s", convert.UUIDToString(projectID))
var cachedIDs []string
if err := r.c.Get(ctx, queryKey, &cachedIDs); err == nil && len(cachedIDs) > 0 {
return r.getByIDsWithFallback(ctx, cachedIDs)
}
rows, err := r.q.GetCommitsByProjectID(ctx, projectID)
if err != nil {
return nil, err
}
entities := make([]*models.CommitEntity, 0, len(rows))
for _, row := range rows {
entities = append(entities, &models.CommitEntity{
ID: convert.UUIDToString(row.ID),
ProjectID: convert.UUIDToString(row.ProjectID),
SnapshotJson: row.SnapshotJson,
SnapshotHash: convert.TextToString(row.SnapshotHash),
UserID: convert.UUIDToString(row.UserID),
EditSummary: convert.TextToString(row.EditSummary),
IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
})
}
return entities, nil
}
func (r *commitRepository) Search(ctx context.Context, params sqlc.SearchCommitsParams) ([]*models.CommitEntity, error) {
queryKey := r.generateQueryKey("commit:search", params)
var cachedIDs []string
if err := r.c.Get(ctx, queryKey, &cachedIDs); err == nil && len(cachedIDs) > 0 {
return r.getByIDsWithFallback(ctx, cachedIDs)
}
rows, err := r.q.SearchCommits(ctx, params)
if err != nil {
return nil, err
}
var commits []*models.CommitEntity
var ids []string
commitToCache := make(map[string]any)
for _, row := range rows {
commit := &models.CommitEntity{
ID: convert.UUIDToString(row.ID),
ProjectID: convert.UUIDToString(row.ProjectID),
SnapshotJson: row.SnapshotJson,
SnapshotHash: convert.TextToString(row.SnapshotHash),
UserID: convert.UUIDToString(row.UserID),
EditSummary: convert.TextToString(row.EditSummary),
IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
}
ids = append(ids, commit.ID)
commits = append(commits, commit)
commitToCache[fmt.Sprintf("commit:id:%s", commit.ID)] = commit
}
if len(commitToCache) > 0 {
_ = r.c.MSet(ctx, commitToCache, constants.NormalCacheDuration)
}
if len(ids) > 0 {
_ = r.c.Set(ctx, queryKey, ids, constants.ListCacheDuration)
}
return commits, nil
}

View File

@@ -6,6 +6,7 @@ import (
"encoding/json"
"fmt"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
"history-api/internal/gen/sqlc"
@@ -22,6 +23,7 @@ type EntityRepository interface {
Create(ctx context.Context, params sqlc.CreateEntityParams) (*models.EntityEntity, error)
Update(ctx context.Context, params sqlc.UpdateEntityParams) (*models.EntityEntity, error)
Delete(ctx context.Context, id pgtype.UUID) error
WithTx(tx pgx.Tx) EntityRepository
}
type entityRepository struct {
@@ -36,6 +38,13 @@ func NewEntityRepository(db sqlc.DBTX, c cache.Cache) EntityRepository {
}
}
func (r *entityRepository) WithTx(tx pgx.Tx) EntityRepository {
return &entityRepository{
q: r.q.WithTx(tx),
c: r.c,
}
}
func (r *entityRepository) generateQueryKey(prefix string, params any) string {
b, _ := json.Marshal(params)
hash := fmt.Sprintf("%x", md5.Sum(b))
@@ -193,8 +202,6 @@ func (r *entityRepository) Create(ctx context.Context, params sqlc.CreateEntityP
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
}
_ = r.c.Set(ctx, fmt.Sprintf("entity:id:%s", entity.ID), entity, constants.NormalCacheDuration)
go func() {
_ = r.c.DelByPattern(context.Background(), "entity:search*")
}()
@@ -215,7 +222,7 @@ func (r *entityRepository) Update(ctx context.Context, params sqlc.UpdateEntityP
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
}
_ = r.c.Set(ctx, fmt.Sprintf("entity:id:%s", entity.ID), entity, constants.NormalCacheDuration)
_ = r.c.Del(ctx, fmt.Sprintf("entity:id:%s", entity.ID))
return &entity, nil
}

View File

@@ -6,6 +6,7 @@ import (
"encoding/json"
"fmt"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
"history-api/internal/dtos/response"
@@ -25,6 +26,7 @@ type GeometryRepository interface {
Delete(ctx context.Context, id pgtype.UUID) error
CreateEntityGeometries(ctx context.Context, params sqlc.CreateEntityGeometriesParams) error
BulkDeleteEntityGeometriesByEntityId(ctx context.Context, entityId pgtype.UUID) error
WithTx(tx pgx.Tx) GeometryRepository
}
type geometryRepository struct {
@@ -39,6 +41,13 @@ func NewGeometryRepository(db sqlc.DBTX, c cache.Cache) GeometryRepository {
}
}
func (r *geometryRepository) WithTx(tx pgx.Tx) GeometryRepository {
return &geometryRepository{
q: r.q.WithTx(tx),
c: r.c,
}
}
func (r *geometryRepository) generateQueryKey(prefix string, params any) string {
b, _ := json.Marshal(params)
hash := fmt.Sprintf("%x", md5.Sum(b))
@@ -87,9 +96,9 @@ func (r *geometryRepository) getByIDsWithFallback(ctx context.Context, ids []str
MaxLng: row.MaxLng,
MaxLat: row.MaxLat,
},
IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
}
dbMap[item.ID] = &item
}
@@ -148,9 +157,9 @@ func (r *geometryRepository) GetByID(ctx context.Context, id pgtype.UUID) (*mode
MaxLng: row.MaxLng,
MaxLat: row.MaxLat,
},
IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
}
_ = r.c.Set(ctx, cacheId, geometry, constants.NormalCacheDuration)
@@ -186,9 +195,9 @@ func (r *geometryRepository) Search(ctx context.Context, params sqlc.SearchGeome
MaxLng: row.MaxLng,
MaxLat: row.MaxLat,
},
IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
}
ids = append(ids, geometry.ID)
geometries = append(geometries, geometry)
@@ -224,16 +233,15 @@ func (r *geometryRepository) Create(ctx context.Context, params sqlc.CreateGeome
MaxLng: row.MaxLng,
MaxLat: row.MaxLat,
},
IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
}
_ = r.c.Set(ctx, fmt.Sprintf("geometry:id:%s", geometry.ID), geometry, constants.NormalCacheDuration)
go func() {
bgCtx := context.Background()
_ = r.c.DelByPattern(bgCtx, "geometry:search*")
_ = r.c.DelByPattern(context.Background(), "geometry:search*")
}()
return &geometry, nil
}
@@ -255,11 +263,11 @@ func (r *geometryRepository) Update(ctx context.Context, params sqlc.UpdateGeome
MaxLng: row.MaxLng,
MaxLat: row.MaxLat,
},
IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
}
_ = r.c.Set(ctx, fmt.Sprintf("geometry:id:%s", geometry.ID), geometry, constants.NormalCacheDuration)
_ = r.c.Del(ctx, fmt.Sprintf("geometry:id:%s", geometry.ID))
return &geometry, nil
}

View File

@@ -11,6 +11,7 @@ import (
"history-api/pkg/constants"
"history-api/pkg/convert"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
)
@@ -23,6 +24,7 @@ type MediaRepository interface {
Delete(ctx context.Context, id pgtype.UUID) error
BulkDelete(ctx context.Context, ids []pgtype.UUID) error
Create(ctx context.Context, params sqlc.CreateMediaParams) (*models.MediaEntity, error)
WithTx(tx pgx.Tx) MediaRepository
}
type mediaRepository struct {
@@ -37,6 +39,13 @@ func NewMediaRepository(db sqlc.DBTX, c cache.Cache) MediaRepository {
}
}
func (r *mediaRepository) WithTx(tx pgx.Tx) MediaRepository {
return &mediaRepository{
q: r.q.WithTx(tx),
c: r.c,
}
}
func (r *mediaRepository) generateQueryKey(prefix string, params any) string {
b, _ := json.Marshal(params)
hash := fmt.Sprintf("%x", md5.Sum(b))
@@ -167,8 +176,7 @@ func (r *mediaRepository) Create(ctx context.Context, params sqlc.CreateMediaPar
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
}
_ = r.c.Set(ctx, fmt.Sprintf("media:id:%s", media.ID), media, constants.NormalCacheDuration)
_ = r.c.Del(ctx, fmt.Sprintf("media:userId:%s", convert.UUIDToString(params.UserID)))
return &media, nil
}
@@ -181,8 +189,7 @@ func (r *mediaRepository) Delete(ctx context.Context, id pgtype.UUID) error {
cacheId := fmt.Sprintf("media:id:%s", convert.UUIDToString(id))
_ = r.c.Del(ctx, cacheId)
go func() {
bgCtx := context.Background()
_ = r.c.DelByPattern(bgCtx, "media:count*")
_ = r.c.DelByPattern(context.Background(), "media:count*")
}()
return nil
}

View File

@@ -6,6 +6,7 @@ import (
"encoding/json"
"fmt"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
"history-api/internal/gen/sqlc"
@@ -24,6 +25,13 @@ type ProjectRepository interface {
Create(ctx context.Context, params sqlc.CreateProjectParams) (*models.ProjectEntity, error)
Update(ctx context.Context, params sqlc.UpdateProjectParams) (*models.ProjectEntity, error)
Delete(ctx context.Context, id pgtype.UUID) error
AddMember(ctx context.Context, params sqlc.AddProjectMemberParams) error
UpdateMemberRole(ctx context.Context, params sqlc.UpdateProjectMemberRoleParams) error
RemoveMember(ctx context.Context, params sqlc.RemoveProjectMemberParams) error
CheckPermission(ctx context.Context, params sqlc.CheckProjectPermissionParams) (int16, error)
ChangeOwner(ctx context.Context, params sqlc.ChangeProjectOwnerParams) error
UpdateLatestCommit(ctx context.Context, params sqlc.UpdateLatestCommitParams) error
WithTx(tx pgx.Tx) ProjectRepository
}
type projectRepository struct {
@@ -38,13 +46,18 @@ func NewProjectRepository(db sqlc.DBTX, c cache.Cache) ProjectRepository {
}
}
func (r *projectRepository) WithTx(tx pgx.Tx) ProjectRepository {
return &projectRepository{
q: r.q.WithTx(tx),
c: r.c,
}
}
func (r *projectRepository) generateQueryKey(prefix string, params any) string {
b, _ := json.Marshal(params)
hash := fmt.Sprintf("%x", md5.Sum(b))
return fmt.Sprintf("%s:%s", prefix, hash)
}
func (r *projectRepository) getByIDsWithFallback(ctx context.Context, ids []string) ([]*models.ProjectEntity, error) {
if len(ids) == 0 {
return []*models.ProjectEntity{}, nil
@@ -75,21 +88,21 @@ func (r *projectRepository) getByIDsWithFallback(ctx context.Context, ids []stri
if err == nil {
for _, row := range dbRows {
item := models.ProjectEntity{
ID: convert.UUIDToString(row.ID),
Title: row.Title,
Description: convert.TextToString(row.Description),
LatestRevisionID: convert.UUIDToStringPtr(row.LatestRevisionID),
VersionCount: row.VersionCount,
ProjectStatus: constants.ParseProjectStatusType(row.ProjectStatus),
LockedBy: convert.UUIDToStringPtr(row.LockedBy),
IsDeleted: row.IsDeleted,
UserID: convert.UUIDToString(row.UserID),
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
CommitIds: convert.ListUUIDToString(row.CommitIds),
SubmissionIds: convert.ListUUIDToString(row.SubmissionIds),
ID: convert.UUIDToString(row.ID),
Title: row.Title,
Description: convert.TextToString(row.Description),
LatestCommitID: convert.UUIDToStringPtr(row.LatestCommitID),
ProjectStatus: constants.ParseProjectStatusType(row.ProjectStatus),
LockedBy: convert.UUIDToStringPtr(row.LockedBy),
IsDeleted: row.IsDeleted,
UserID: convert.UUIDToString(row.UserID),
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
SubmissionIds: convert.ListUUIDToString(row.SubmissionIds),
}
_ = item.ParseUser(row.User)
_ = item.ParseCommits(row.Commits)
_ = item.ParseMembers(row.Members)
dbMap[item.ID] = &item
}
}
@@ -135,21 +148,21 @@ func (r *projectRepository) GetByID(ctx context.Context, id pgtype.UUID) (*model
}
project = models.ProjectEntity{
ID: convert.UUIDToString(row.ID),
Title: row.Title,
Description: convert.TextToString(row.Description),
LatestRevisionID: convert.UUIDToStringPtr(row.LatestRevisionID),
VersionCount: row.VersionCount,
ProjectStatus: constants.ParseProjectStatusType(row.ProjectStatus),
LockedBy: convert.UUIDToStringPtr(row.LockedBy),
IsDeleted: row.IsDeleted,
UserID: convert.UUIDToString(row.UserID),
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
CommitIds: convert.ListUUIDToString(row.CommitIds),
SubmissionIds: convert.ListUUIDToString(row.SubmissionIds),
ID: convert.UUIDToString(row.ID),
Title: row.Title,
Description: convert.TextToString(row.Description),
LatestCommitID: convert.UUIDToStringPtr(row.LatestCommitID),
ProjectStatus: constants.ParseProjectStatusType(row.ProjectStatus),
LockedBy: convert.UUIDToStringPtr(row.LockedBy),
IsDeleted: row.IsDeleted,
UserID: convert.UUIDToString(row.UserID),
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
SubmissionIds: convert.ListUUIDToString(row.SubmissionIds),
}
_ = project.ParseUser(row.User)
_ = project.ParseCommits(row.Commits)
_ = project.ParseMembers(row.Members)
_ = r.c.Set(ctx, cacheId, project, constants.NormalCacheDuration)
@@ -167,28 +180,28 @@ func (r *projectRepository) GetByUserID(ctx context.Context, params sqlc.GetProj
if err != nil {
return nil, err
}
var projects []*models.ProjectEntity
var ids []string
projectToCache := make(map[string]any)
for _, row := range rows {
project := &models.ProjectEntity{
ID: convert.UUIDToString(row.ID),
Title: row.Title,
Description: convert.TextToString(row.Description),
LatestRevisionID: convert.UUIDToStringPtr(row.LatestRevisionID),
VersionCount: row.VersionCount,
ProjectStatus: constants.ParseProjectStatusType(row.ProjectStatus),
LockedBy: convert.UUIDToStringPtr(row.LockedBy),
IsDeleted: row.IsDeleted,
UserID: convert.UUIDToString(row.UserID),
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
CommitIds: convert.ListUUIDToString(row.CommitIds),
SubmissionIds: convert.ListUUIDToString(row.SubmissionIds),
ID: convert.UUIDToString(row.ID),
Title: row.Title,
Description: convert.TextToString(row.Description),
LatestCommitID: convert.UUIDToStringPtr(row.LatestCommitID),
ProjectStatus: constants.ParseProjectStatusType(row.ProjectStatus),
LockedBy: convert.UUIDToStringPtr(row.LockedBy),
IsDeleted: row.IsDeleted,
UserID: convert.UUIDToString(row.UserID),
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
SubmissionIds: convert.ListUUIDToString(row.SubmissionIds),
}
_ = project.ParseUser(row.User)
_ = project.ParseCommits(row.Commits)
_ = project.ParseMembers(row.Members)
ids = append(ids, project.ID)
projects = append(projects, project)
@@ -222,21 +235,21 @@ func (r *projectRepository) Search(ctx context.Context, params sqlc.SearchProjec
for _, row := range rows {
project := &models.ProjectEntity{
ID: convert.UUIDToString(row.ID),
Title: row.Title,
Description: convert.TextToString(row.Description),
LatestRevisionID: convert.UUIDToStringPtr(row.LatestRevisionID),
VersionCount: row.VersionCount,
ProjectStatus: constants.ParseProjectStatusType(row.ProjectStatus),
LockedBy: convert.UUIDToStringPtr(row.LockedBy),
IsDeleted: row.IsDeleted,
UserID: convert.UUIDToString(row.UserID),
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
CommitIds: convert.ListUUIDToString(row.CommitIds),
SubmissionIds: convert.ListUUIDToString(row.SubmissionIds),
ID: convert.UUIDToString(row.ID),
Title: row.Title,
Description: convert.TextToString(row.Description),
LatestCommitID: convert.UUIDToStringPtr(row.LatestCommitID),
ProjectStatus: constants.ParseProjectStatusType(row.ProjectStatus),
LockedBy: convert.UUIDToStringPtr(row.LockedBy),
IsDeleted: row.IsDeleted,
UserID: convert.UUIDToString(row.UserID),
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
SubmissionIds: convert.ListUUIDToString(row.SubmissionIds),
}
_ = project.ParseUser(row.User)
_ = project.ParseCommits(row.Commits)
_ = project.ParseMembers(row.Members)
ids = append(ids, project.ID)
projects = append(projects, project)
@@ -276,23 +289,21 @@ func (r *projectRepository) Create(ctx context.Context, params sqlc.CreateProjec
}
project := models.ProjectEntity{
ID: convert.UUIDToString(row.ID),
Title: row.Title,
Description: convert.TextToString(row.Description),
LatestRevisionID: convert.UUIDToStringPtr(row.LatestRevisionID),
VersionCount: row.VersionCount,
ProjectStatus: constants.ParseProjectStatusType(row.ProjectStatus),
LockedBy: convert.UUIDToStringPtr(row.LockedBy),
IsDeleted: row.IsDeleted,
UserID: convert.UUIDToString(row.UserID),
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
CommitIds: convert.ListUUIDToString(row.CommitIds),
SubmissionIds: convert.ListUUIDToString(row.SubmissionIds),
ID: convert.UUIDToString(row.ID),
Title: row.Title,
Description: convert.TextToString(row.Description),
LatestCommitID: convert.UUIDToStringPtr(row.LatestCommitID),
ProjectStatus: constants.ParseProjectStatusType(row.ProjectStatus),
LockedBy: convert.UUIDToStringPtr(row.LockedBy),
IsDeleted: row.IsDeleted,
UserID: convert.UUIDToString(row.UserID),
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
SubmissionIds: convert.ListUUIDToString(row.SubmissionIds),
}
_ = project.ParseUser(row.User)
_ = r.c.Set(ctx, fmt.Sprintf("project:id:%s", project.ID), project, constants.NormalCacheDuration)
_ = project.ParseCommits(row.Commits)
_ = project.ParseMembers(row.Members)
go func() {
bgCtx := context.Background()
@@ -309,23 +320,23 @@ func (r *projectRepository) Update(ctx context.Context, params sqlc.UpdateProjec
return nil, err
}
project := models.ProjectEntity{
ID: convert.UUIDToString(row.ID),
Title: row.Title,
Description: convert.TextToString(row.Description),
LatestRevisionID: convert.UUIDToStringPtr(row.LatestRevisionID),
VersionCount: row.VersionCount,
ProjectStatus: constants.ParseProjectStatusType(row.ProjectStatus),
LockedBy: convert.UUIDToStringPtr(row.LockedBy),
IsDeleted: row.IsDeleted,
UserID: convert.UUIDToString(row.UserID),
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
CommitIds: convert.ListUUIDToString(row.CommitIds),
SubmissionIds: convert.ListUUIDToString(row.SubmissionIds),
ID: convert.UUIDToString(row.ID),
Title: row.Title,
Description: convert.TextToString(row.Description),
LatestCommitID: convert.UUIDToStringPtr(row.LatestCommitID),
ProjectStatus: constants.ParseProjectStatusType(row.ProjectStatus),
LockedBy: convert.UUIDToStringPtr(row.LockedBy),
IsDeleted: row.IsDeleted,
UserID: convert.UUIDToString(row.UserID),
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
SubmissionIds: convert.ListUUIDToString(row.SubmissionIds),
}
_ = project.ParseUser(row.User)
_ = project.ParseCommits(row.Commits)
_ = project.ParseMembers(row.Members)
_ = r.c.Set(ctx, fmt.Sprintf("project:id:%s", project.ID), project, constants.NormalCacheDuration)
_ = r.c.Del(ctx, fmt.Sprintf("project:id:%s", project.ID))
return &project, nil
}
@@ -337,9 +348,75 @@ func (r *projectRepository) Delete(ctx context.Context, id pgtype.UUID) error {
_ = r.c.Del(ctx, fmt.Sprintf("project:id:%s", convert.UUIDToString(id)))
go func() {
bgCtx := context.Background()
_ = r.c.DelByPattern(bgCtx, "project:search*")
_ = r.c.DelByPattern(bgCtx, "project:user*")
_ = r.c.DelByPattern(bgCtx, "project:count*")
}()
return nil
}
func (r *projectRepository) AddMember(ctx context.Context, params sqlc.AddProjectMemberParams) error {
_, err := r.q.AddProjectMember(ctx, params)
if err != nil {
return err
}
_ = r.c.Del(ctx, fmt.Sprintf("project:id:%s", convert.UUIDToString(params.ProjectID)))
_ = r.c.Del(ctx, fmt.Sprintf("project:perm:%s:%s", convert.UUIDToString(params.ProjectID), convert.UUIDToString(params.UserID)))
return nil
}
func (r *projectRepository) UpdateMemberRole(ctx context.Context, params sqlc.UpdateProjectMemberRoleParams) error {
_, err := r.q.UpdateProjectMemberRole(ctx, params)
if err != nil {
return err
}
_ = r.c.Del(ctx, fmt.Sprintf("project:id:%s", convert.UUIDToString(params.ProjectID)))
_ = r.c.Del(ctx, fmt.Sprintf("project:perm:%s:%s", convert.UUIDToString(params.ProjectID), convert.UUIDToString(params.UserID)))
return nil
}
func (r *projectRepository) RemoveMember(ctx context.Context, params sqlc.RemoveProjectMemberParams) error {
err := r.q.RemoveProjectMember(ctx, params)
if err != nil {
return err
}
_ = r.c.Del(ctx, fmt.Sprintf("project:id:%s", convert.UUIDToString(params.ProjectID)))
_ = r.c.Del(ctx, fmt.Sprintf("project:perm:%s:%s", convert.UUIDToString(params.ProjectID), convert.UUIDToString(params.UserID)))
return nil
}
func (r *projectRepository) CheckPermission(ctx context.Context, params sqlc.CheckProjectPermissionParams) (int16, error) {
cacheKey := fmt.Sprintf("project:perm:%s:%s", convert.UUIDToString(params.ProjectID), convert.UUIDToString(params.UserID))
var role int16
if err := r.c.Get(ctx, cacheKey, &role); err == nil {
return role, nil
}
role, err := r.q.CheckProjectPermission(ctx, params)
if err != nil {
return 0, err
}
_ = r.c.Set(ctx, cacheKey, role, constants.NormalCacheDuration)
return role, nil
}
func (r *projectRepository) ChangeOwner(ctx context.Context, params sqlc.ChangeProjectOwnerParams) error {
err := r.q.ChangeProjectOwner(ctx, params)
if err != nil {
return err
}
projectID := convert.UUIDToString(params.ID)
_ = r.c.Del(ctx, fmt.Sprintf("project:id:%s", projectID))
go func() {
_ = r.c.DelByPattern(context.Background(), fmt.Sprintf("project:perm:%s:*", projectID))
}()
return nil
}
func (r *projectRepository) UpdateLatestCommit(ctx context.Context, params sqlc.UpdateLatestCommitParams) error {
err := r.q.UpdateLatestCommit(ctx, params)
if err != nil {
return err
}
_ = r.c.Del(ctx, fmt.Sprintf("project:id:%s", convert.UUIDToString(params.ID)))
return nil
}

View File

@@ -6,6 +6,7 @@ import (
"encoding/json"
"fmt"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
"history-api/internal/gen/sqlc"
@@ -18,7 +19,7 @@ import (
type RoleRepository interface {
GetByID(ctx context.Context, id pgtype.UUID) (*models.RoleEntity, error)
GetByIDs(ctx context.Context, ids []string) ([]*models.RoleEntity, error)
GetByname(ctx context.Context, name string) (*models.RoleEntity, error)
GetByName(ctx context.Context, name string) (*models.RoleEntity, error)
All(ctx context.Context) ([]*models.RoleEntity, error)
Create(ctx context.Context, name string) (*models.RoleEntity, error)
Update(ctx context.Context, params sqlc.UpdateRoleParams) (*models.RoleEntity, error)
@@ -28,6 +29,7 @@ type RoleRepository interface {
DeleteUserRole(ctx context.Context, params sqlc.DeleteUserRoleParams) error
BulkDeleteRolesFromUser(ctx context.Context, userId pgtype.UUID) error
BulkDeleteUsersFromRole(ctx context.Context, roleId pgtype.UUID) error
WithTx(tx pgx.Tx) RoleRepository
}
type roleRepository struct {
@@ -42,6 +44,13 @@ func NewRoleRepository(db sqlc.DBTX, c cache.Cache) RoleRepository {
}
}
func (r *roleRepository) WithTx(tx pgx.Tx) RoleRepository {
return &roleRepository{
q: r.q.WithTx(tx),
c: r.c,
}
}
func (r *roleRepository) generateQueryKey(prefix string, params any) string {
b, _ := json.Marshal(params)
hash := fmt.Sprintf("%x", md5.Sum(b))
@@ -140,7 +149,7 @@ func (r *roleRepository) GetByID(ctx context.Context, id pgtype.UUID) (*models.R
return &role, nil
}
func (r *roleRepository) GetByname(ctx context.Context, name string) (*models.RoleEntity, error) {
func (r *roleRepository) GetByName(ctx context.Context, name string) (*models.RoleEntity, error) {
cacheId := fmt.Sprintf("role:name:%s", name)
var role models.RoleEntity
err := r.c.Get(ctx, cacheId, &role)
@@ -183,11 +192,6 @@ func (r *roleRepository) Create(ctx context.Context, name string) (*models.RoleE
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
}
mapCache := map[string]any{
fmt.Sprintf("role:name:%s", name): role,
fmt.Sprintf("role:id:%s", convert.UUIDToString(row.ID)): role,
}
_ = r.c.MSet(ctx, mapCache, constants.NormalCacheDuration)
return &role, nil
}
@@ -204,11 +208,7 @@ func (r *roleRepository) Update(ctx context.Context, params sqlc.UpdateRoleParam
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
}
mapCache := map[string]any{
fmt.Sprintf("role:name:%s", row.Name): role,
fmt.Sprintf("role:id:%s", convert.UUIDToString(row.ID)): role,
}
_ = r.c.MSet(ctx, mapCache, constants.NormalCacheDuration)
_ = r.c.Del(ctx, fmt.Sprintf("role:id:%s", convert.UUIDToString(row.ID)), fmt.Sprintf("role:name:%s", row.Name))
return &role, nil
}

View File

@@ -0,0 +1,313 @@
package repositories
import (
"context"
"crypto/md5"
"encoding/json"
"fmt"
"history-api/internal/gen/sqlc"
"history-api/internal/models"
"history-api/pkg/cache"
"history-api/pkg/constants"
"history-api/pkg/convert"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
)
type SubmissionRepository interface {
GetByID(ctx context.Context, id pgtype.UUID) (*models.SubmissionEntity, error)
GetByIDs(ctx context.Context, ids []string) ([]*models.SubmissionEntity, error)
Search(ctx context.Context, params sqlc.SearchSubmissionsParams) ([]*models.SubmissionEntity, error)
Count(ctx context.Context, params sqlc.CountSubmissionsParams) (int64, error)
Create(ctx context.Context, params sqlc.CreateSubmissionParams) (*models.SubmissionEntity, error)
Update(ctx context.Context, params sqlc.UpdateSubmissionParams) (*models.SubmissionEntity, error)
Delete(ctx context.Context, id pgtype.UUID) error
WithTx(tx pgx.Tx) SubmissionRepository
}
type submissionRepository struct {
q *sqlc.Queries
c cache.Cache
}
func NewSubmissionRepository(db sqlc.DBTX, c cache.Cache) SubmissionRepository {
return &submissionRepository{
q: sqlc.New(db),
c: c,
}
}
func (r *submissionRepository) WithTx(tx pgx.Tx) SubmissionRepository {
return &submissionRepository{
q: r.q.WithTx(tx),
c: r.c,
}
}
func (r *submissionRepository) GetByID(ctx context.Context, id pgtype.UUID) (*models.SubmissionEntity, error) {
cacheId := fmt.Sprintf("submission:id:%s", convert.UUIDToString(id))
var submission models.SubmissionEntity
err := r.c.Get(ctx, cacheId, &submission)
if err == nil {
return &submission, nil
}
row, err := r.q.GetSubmissionById(ctx, id)
if err != nil {
return nil, err
}
entity := &models.SubmissionEntity{
ID: convert.UUIDToString(row.ID),
ProjectID: convert.UUIDToString(row.ProjectID),
CommitID: convert.UUIDToString(row.CommitID),
UserID: convert.UUIDToString(row.UserID),
CreatedAt: convert.TimeToPtr(row.CreatedAt),
Status: constants.ParseStatusType(row.Status),
ReviewedBy: convert.UUIDToStringPtr(row.ReviewedBy),
ReviewedAt: convert.TimeToPtr(row.ReviewedAt),
ReviewNote: convert.TextToPtr(row.ReviewNote),
Content: convert.TextToPtr(row.Content),
IsDeleted: row.IsDeleted,
}
_ = entity.ParseUser(row.User)
_ = entity.ParseReviewer(row.Reviewer)
_ = r.c.Set(ctx, cacheId, entity, constants.NormalCacheDuration)
return entity, nil
}
func (r *submissionRepository) generateQueryKey(prefix string, params any) string {
b, _ := json.Marshal(params)
hash := fmt.Sprintf("%x", md5.Sum(b))
return fmt.Sprintf("%s:%s", prefix, hash)
}
func (r *submissionRepository) getByIDsWithFallback(ctx context.Context, ids []string) ([]*models.SubmissionEntity, error) {
if len(ids) == 0 {
return []*models.SubmissionEntity{}, nil
}
keys := make([]string, len(ids))
for i, id := range ids {
keys[i] = fmt.Sprintf("submission:id:%s", id)
}
raws := r.c.MGet(ctx, keys...)
var submission []*models.SubmissionEntity
missingSubmisstionToCache := make(map[string]any)
var missingPgIds []pgtype.UUID
for i, b := range raws {
if len(b) == 0 {
pgId := pgtype.UUID{}
err := pgId.Scan(ids[i])
if err == nil {
missingPgIds = append(missingPgIds, pgId)
}
}
}
dbMap := make(map[string]*models.SubmissionEntity)
if len(missingPgIds) > 0 {
dbRows, err := r.q.GetSubmissionsByIDs(ctx, missingPgIds)
if err == nil {
for _, row := range dbRows {
entity := &models.SubmissionEntity{
ID: convert.UUIDToString(row.ID),
ProjectID: convert.UUIDToString(row.ProjectID),
CommitID: convert.UUIDToString(row.CommitID),
UserID: convert.UUIDToString(row.UserID),
CreatedAt: convert.TimeToPtr(row.CreatedAt),
Status: constants.ParseStatusType(row.Status),
ReviewedBy: convert.UUIDToStringPtr(row.ReviewedBy),
ReviewedAt: convert.TimeToPtr(row.ReviewedAt),
ReviewNote: convert.TextToPtr(row.ReviewNote),
Content: convert.TextToPtr(row.Content),
IsDeleted: row.IsDeleted,
}
_ = entity.ParseUser(row.User)
_ = entity.ParseReviewer(row.Reviewer)
dbMap[entity.ID] = entity
}
}
}
for i, b := range raws {
if len(b) > 0 {
var u models.SubmissionEntity
if err := json.Unmarshal(b, &u); err == nil {
submission = append(submission, &u)
}
} else {
if item, ok := dbMap[ids[i]]; ok {
submission = append(submission, item)
missingSubmisstionToCache[keys[i]] = item
}
}
}
if len(missingSubmisstionToCache) > 0 {
_ = r.c.MSet(ctx, missingSubmisstionToCache, constants.NormalCacheDuration)
}
return submission, nil
}
func (r *submissionRepository) GetByIDs(ctx context.Context, ids []string) ([]*models.SubmissionEntity, error) {
return r.getByIDsWithFallback(ctx, ids)
}
func (r *submissionRepository) Search(ctx context.Context, params sqlc.SearchSubmissionsParams) ([]*models.SubmissionEntity, error) {
queryKey := r.generateQueryKey("verification:search", params)
var cachedIDs []string
if err := r.c.Get(ctx, queryKey, &cachedIDs); err == nil && len(cachedIDs) > 0 {
listItem, err := r.getByIDsWithFallback(ctx, cachedIDs)
if err != nil {
return nil, err
}
newCachedIDs := make([]string, len(listItem))
for i, media := range listItem {
newCachedIDs[i] = media.ID
}
_ = r.c.Set(ctx, queryKey, newCachedIDs, constants.ListCacheDuration)
return listItem, err
}
rows, err := r.q.SearchSubmissions(ctx, params)
if err != nil {
return nil, err
}
var items []*models.SubmissionEntity
var ids []string
itemToCache := make(map[string]any)
for _, row := range rows {
entity := &models.SubmissionEntity{
ID: convert.UUIDToString(row.ID),
ProjectID: convert.UUIDToString(row.ProjectID),
CommitID: convert.UUIDToString(row.CommitID),
UserID: convert.UUIDToString(row.UserID),
CreatedAt: convert.TimeToPtr(row.CreatedAt),
Status: constants.ParseStatusType(row.Status),
ReviewedBy: convert.UUIDToStringPtr(row.ReviewedBy),
ReviewedAt: convert.TimeToPtr(row.ReviewedAt),
ReviewNote: convert.TextToPtr(row.ReviewNote),
Content: convert.TextToPtr(row.Content),
IsDeleted: row.IsDeleted,
}
_ = entity.ParseUser(row.User)
_ = entity.ParseReviewer(row.Reviewer)
ids = append(ids, entity.ID)
items = append(items, entity)
itemToCache[fmt.Sprintf("submission:id:%s", entity.ID)] = entity
}
if len(itemToCache) > 0 {
_ = r.c.MSet(ctx, itemToCache, constants.NormalCacheDuration)
}
if len(ids) > 0 {
_ = r.c.Set(ctx, queryKey, ids, constants.ListCacheDuration)
}
return items, nil
}
func (r *submissionRepository) Count(ctx context.Context, params sqlc.CountSubmissionsParams) (int64, error) {
queryKey := r.generateQueryKey("submission:count", params)
var count int64
if err := r.c.Get(ctx, queryKey, &count); err == nil {
_ = r.c.Set(ctx, queryKey, count, constants.ListCacheDuration)
return count, nil
}
count, err := r.q.CountSubmissions(ctx, params)
if err != nil {
return 0, err
}
_ = r.c.Set(ctx, queryKey, count, constants.ListCacheDuration)
return count, nil
}
func (r *submissionRepository) Create(ctx context.Context, params sqlc.CreateSubmissionParams) (*models.SubmissionEntity, error) {
row, err := r.q.CreateSubmission(ctx, params)
if err != nil {
return nil, err
}
submission := models.SubmissionEntity{
ID: convert.UUIDToString(row.ID),
ProjectID: convert.UUIDToString(row.ProjectID),
CommitID: convert.UUIDToString(row.CommitID),
UserID: convert.UUIDToString(row.UserID),
CreatedAt: convert.TimeToPtr(row.CreatedAt),
Status: constants.ParseStatusType(row.Status),
ReviewedBy: convert.UUIDToStringPtr(row.ReviewedBy),
ReviewedAt: convert.TimeToPtr(row.ReviewedAt),
ReviewNote: convert.TextToPtr(row.ReviewNote),
Content: convert.TextToPtr(row.Content),
IsDeleted: row.IsDeleted,
}
if err := submission.ParseUser(row.User); err != nil {
return nil, err
}
if err := submission.ParseReviewer(row.Reviewer); err != nil {
return nil, err
}
go func() {
bgCtx := context.Background()
_ = r.c.DelByPattern(bgCtx, "submission:search*")
_ = r.c.DelByPattern(bgCtx, "submission:count*")
}()
return &submission, nil
}
func (r *submissionRepository) Update(ctx context.Context, params sqlc.UpdateSubmissionParams) (*models.SubmissionEntity, error) {
row, err := r.q.UpdateSubmission(ctx, params)
if err != nil {
return nil, err
}
submission := models.SubmissionEntity{
ID: convert.UUIDToString(row.ID),
ProjectID: convert.UUIDToString(row.ProjectID),
CommitID: convert.UUIDToString(row.CommitID),
UserID: convert.UUIDToString(row.UserID),
CreatedAt: convert.TimeToPtr(row.CreatedAt),
Status: constants.ParseStatusType(row.Status),
ReviewedBy: convert.UUIDToStringPtr(row.ReviewedBy),
ReviewedAt: convert.TimeToPtr(row.ReviewedAt),
ReviewNote: convert.TextToPtr(row.ReviewNote),
Content: convert.TextToPtr(row.Content),
IsDeleted: row.IsDeleted,
}
if err := submission.ParseUser(row.User); err != nil {
return nil, err
}
if err := submission.ParseReviewer(row.Reviewer); err != nil {
return nil, err
}
_ = r.c.Del(ctx, fmt.Sprintf("submission:id:%s", submission.ID))
return &submission, nil
}
func (r *submissionRepository) Delete(ctx context.Context, id pgtype.UUID) error {
err := r.q.DeleteSubmission(ctx, id)
if err != nil {
return err
}
_ = r.c.Del(ctx, fmt.Sprintf("submission:id:%s", convert.UUIDToString(id)))
go func() {
_ = r.c.DelByPattern(context.Background(), "submission:count*")
}()
return nil
}

View File

@@ -6,6 +6,7 @@ import (
"encoding/json"
"fmt"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
"history-api/internal/gen/sqlc"
@@ -30,6 +31,7 @@ type UserRepository interface {
UpdateTokenVersion(ctx context.Context, params sqlc.UpdateTokenVersionParams) error
Delete(ctx context.Context, id pgtype.UUID) error
Restore(ctx context.Context, id pgtype.UUID) error
WithTx(tx pgx.Tx) UserRepository
}
type userRepository struct {
@@ -44,6 +46,13 @@ func NewUserRepository(db sqlc.DBTX, c cache.Cache) UserRepository {
}
}
func (r *userRepository) WithTx(tx pgx.Tx) UserRepository {
return &userRepository{
q: r.q.WithTx(tx),
c: r.c,
}
}
func (r *userRepository) generateQueryKey(prefix string, params any) string {
b, _ := json.Marshal(params)
hash := fmt.Sprintf("%x", md5.Sum(b))
@@ -270,11 +279,7 @@ func (r *userRepository) UpdateProfile(ctx context.Context, params sqlc.UpdateUs
}
user.Profile = &profile
mapCache := map[string]any{
fmt.Sprintf("user:email:%s", user.Email): user,
fmt.Sprintf("user:id:%s", user.ID): user,
}
_ = r.c.MSet(ctx, mapCache, constants.NormalCacheDuration)
_ = r.c.Del(ctx, fmt.Sprintf("user:email:%s", user.Email), fmt.Sprintf("user:id:%s", user.ID))
return user, nil
}
@@ -391,11 +396,12 @@ func (r *userRepository) Restore(ctx context.Context, id pgtype.UUID) error {
if err != nil {
return err
}
_ = r.c.Del(ctx, fmt.Sprintf("user:id:%s", convert.UUIDToString(id)))
_ = r.c.Del(ctx, fmt.Sprintf("user:email:%s", convert.UUIDToString(id)))
_ = r.c.Del(ctx, fmt.Sprintf("user:deleted:id:%s", convert.UUIDToString(id)))
_ = r.c.Del(
ctx,
fmt.Sprintf("user:deleted:id:%s", convert.UUIDToString(id)),
fmt.Sprintf("user:email:%s", convert.UUIDToString(id)),
fmt.Sprintf("user:id:%s", convert.UUIDToString(id)),
)
return nil
}
@@ -421,9 +427,7 @@ func (r *userRepository) UpdateTokenVersion(ctx context.Context, params sqlc.Upd
if err != nil {
return err
}
cacheId := fmt.Sprintf("user:token:%s", convert.UUIDToString(params.ID))
_ = r.c.Set(ctx, cacheId, params.TokenVersion, constants.NormalCacheDuration)
_ = r.c.Del(ctx, fmt.Sprintf("user:token:%s", convert.UUIDToString(params.ID)))
return nil
}
@@ -436,23 +440,7 @@ func (r *userRepository) UpdatePassword(ctx context.Context, params sqlc.UpdateU
if err != nil {
return err
}
err = r.UpdateTokenVersion(ctx, sqlc.UpdateTokenVersionParams{
ID: params.ID,
TokenVersion: user.TokenVersion + 1,
})
if err != nil {
return err
}
user.PasswordHash = convert.TextToString(params.PasswordHash)
user.TokenVersion += 1
mapCache := map[string]any{
fmt.Sprintf("user:email:%s", user.Email): user,
fmt.Sprintf("user:id:%s", user.ID): user,
fmt.Sprintf("user:token:%s", user.ID): user.TokenVersion,
}
_ = r.c.MSet(ctx, mapCache, constants.NormalCacheDuration)
_ = r.c.Del(ctx, fmt.Sprintf("user:email:%s", user.Email), fmt.Sprintf("user:id:%s", user.ID))
return nil
}
@@ -467,12 +455,7 @@ func (r *userRepository) UpdateRefreshToken(ctx context.Context, params sqlc.Upd
}
user.RefreshToken = convert.TextToString(params.RefreshToken)
mapCache := map[string]any{
fmt.Sprintf("user:email:%s", user.Email): user,
fmt.Sprintf("user:id:%s", user.ID): user,
fmt.Sprintf("user:token:%s", user.ID): user.TokenVersion,
}
_ = r.c.MSet(ctx, mapCache, constants.NormalCacheDuration)
_ = r.c.Del(ctx, fmt.Sprintf("user:email:%s", user.Email), fmt.Sprintf("user:id:%s", user.ID))
return nil
}

View File

@@ -11,6 +11,7 @@ import (
"history-api/pkg/constants"
"history-api/pkg/convert"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
)
@@ -25,6 +26,7 @@ type VerificationRepository interface {
CreateVerificationMedia(ctx context.Context, params sqlc.CreateVerificationMediaParams) error
DeleteVerificationMedia(ctx context.Context, params sqlc.DeleteVerificationMediaParams) error
BulkVerificationMediaByMediaId(ctx context.Context, mediaId pgtype.UUID) error
WithTx(tx pgx.Tx) VerificationRepository
}
type verificationRepository struct {
@@ -39,6 +41,13 @@ func NewVerificationRepository(db sqlc.DBTX, c cache.Cache) VerificationReposito
}
}
func (v *verificationRepository) WithTx(tx pgx.Tx) VerificationRepository {
return &verificationRepository{
q: v.q.WithTx(tx),
c: v.c,
}
}
func (v *verificationRepository) generateQueryKey(prefix string, params any) string {
b, _ := json.Marshal(params)
hash := fmt.Sprintf("%x", md5.Sum(b))
@@ -174,13 +183,6 @@ func (v *verificationRepository) Create(ctx context.Context, params sqlc.CreateU
if err != nil {
return nil, err
}
go func() {
bgCtx := context.Background()
_ = v.c.DelByPattern(bgCtx, "verification:search*")
_ = v.c.DelByPattern(bgCtx, "verification:count*")
}()
verification := models.UserVerificationEntity{
ID: convert.UUIDToString(row.ID),
VerifyType: constants.ParseVerifyType(row.VerifyType),
@@ -204,9 +206,9 @@ func (v *verificationRepository) Create(ctx context.Context, params sqlc.CreateU
return nil, err
}
_ = v.c.Del(ctx, fmt.Sprintf("verification:userId:%s", convert.UUIDToString(params.UserID)))
go func() {
bgCtx := context.Background()
_ = v.c.DelByPattern(bgCtx, "verification:search*")
_ = v.c.DelByPattern(bgCtx, "verification:count*")
}()
@@ -218,10 +220,7 @@ func (v *verificationRepository) UpdateStatus(ctx context.Context, params sqlc.U
if err != nil {
return err
}
cacheId := fmt.Sprintf("verification:id:%s", convert.UUIDToString(params.ID))
_ = v.c.Del(ctx, cacheId)
_ = v.c.Del(ctx, fmt.Sprintf("verification:id:%s", convert.UUIDToString(params.ID)))
return nil
}
@@ -231,9 +230,10 @@ func (v *verificationRepository) Delete(ctx context.Context, id pgtype.UUID) err
return err
}
cacheId := fmt.Sprintf("verification:id:%s", convert.UUIDToString(id))
_ = v.c.Del(ctx, cacheId)
_ = v.c.Del(ctx, fmt.Sprintf("verification:id:%s", convert.UUIDToString(id)))
go func() {
_ = v.c.DelByPattern(context.Background(), "verification:count*")
}()
return nil
}
@@ -243,6 +243,10 @@ func (v *verificationRepository) BulkVerificationMediaByMediaId(ctx context.Cont
return err
}
if len(ids) == 0 {
return nil
}
listCacheId := make([]string, 0)
for _, it := range ids {
id := convert.UUIDToString(it)

View File

@@ -6,6 +6,7 @@ import (
"encoding/json"
"fmt"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
"history-api/internal/gen/sqlc"
@@ -24,6 +25,7 @@ type WikiRepository interface {
Delete(ctx context.Context, id pgtype.UUID) error
CreateEntityWikis(ctx context.Context, params sqlc.CreateEntityWikisParams) error
BulkDeleteEntityWikisByEntityId(ctx context.Context, entityId pgtype.UUID) error
WithTx(tx pgx.Tx) WikiRepository
}
type wikiRepository struct {
@@ -38,6 +40,13 @@ func NewWikiRepository(db sqlc.DBTX, c cache.Cache) WikiRepository {
}
}
func (r *wikiRepository) WithTx(tx pgx.Tx) WikiRepository {
return &wikiRepository{
q: r.q.WithTx(tx),
c: r.c,
}
}
func (r *wikiRepository) generateQueryKey(prefix string, params any) string {
b, _ := json.Marshal(params)
hash := fmt.Sprintf("%x", md5.Sum(b))
@@ -191,11 +200,9 @@ func (r *wikiRepository) Create(ctx context.Context, params sqlc.CreateWikiParam
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
}
_ = r.c.Set(ctx, fmt.Sprintf("wiki:id:%s", wiki.ID), wiki, constants.NormalCacheDuration)
go func() {
bgCtx := context.Background()
_ = r.c.DelByPattern(bgCtx, "wiki:search*")
_ = r.c.DelByPattern(context.Background(), "wiki:search*")
}()
return &wiki, nil
@@ -214,7 +221,7 @@ func (r *wikiRepository) Update(ctx context.Context, params sqlc.UpdateWikiParam
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
}
_ = r.c.Set(ctx, fmt.Sprintf("wiki:id:%s", wiki.ID), wiki, constants.NormalCacheDuration)
_ = r.c.Del(ctx, fmt.Sprintf("wiki:id:%s", wiki.ID))
return &wiki, nil
}
@@ -224,9 +231,6 @@ func (r *wikiRepository) Delete(ctx context.Context, id pgtype.UUID) error {
return err
}
_ = r.c.Del(ctx, fmt.Sprintf("wiki:id:%s", convert.UUIDToString(id)))
go func() {
_ = r.c.DelByPattern(context.Background(), "wiki:search*")
}()
return nil
}

View File

@@ -8,9 +8,55 @@ import (
"github.com/gofiber/fiber/v3"
)
func ProjectRoutes(app *fiber.App, controller *controllers.ProjectController, userRepo repositories.UserRepository) {
func ProjectRoutes(
app *fiber.App,
controller *controllers.ProjectController,
commitController *controllers.CommitController,
userRepo repositories.UserRepository,
) {
route := app.Group("/projects")
route.Post(
"/:id/commits",
middlewares.JwtAccess(userRepo),
commitController.CreateCommit,
)
route.Post(
"/:id/commits/restore",
middlewares.JwtAccess(userRepo),
commitController.RestoreCommit,
)
route.Get(
"/:id/commits",
commitController.GetProjectCommits,
)
route.Post(
"/:id/members",
middlewares.JwtAccess(userRepo),
controller.AddMember,
)
route.Put(
"/:id/members/:userId",
middlewares.JwtAccess(userRepo),
controller.UpdateMemberRole,
)
route.Delete(
"/:id/members/:userId",
middlewares.JwtAccess(userRepo),
controller.RemoveMember,
)
route.Put(
"/:id/change-owner",
middlewares.JwtAccess(userRepo),
controller.ChangeOwner,
)
route.Get(
"/:id",
controller.GetProjectByID,

View File

@@ -0,0 +1,51 @@
package routes
import (
"history-api/internal/controllers"
"history-api/internal/middlewares"
"history-api/internal/repositories"
"history-api/pkg/constants"
"github.com/gofiber/fiber/v3"
)
func SubmissionRoutes(
app *fiber.App,
controller controllers.SubmissionController,
userRepo repositories.UserRepository,
) {
route := app.Group("/submissions")
route.Patch(
"/:id/status",
middlewares.JwtAccess(userRepo),
middlewares.RequireAnyRole(constants.RoleTypeAdmin, constants.RoleTypeMod),
controller.UpdateSubmissionStatus,
)
route.Get(
"/:id",
middlewares.JwtAccess(userRepo),
controller.GetSubmissionByID,
)
route.Delete(
"/:id",
middlewares.JwtAccess(userRepo),
controller.DeleteSubmission,
)
route.Post(
"/",
middlewares.JwtAccess(userRepo),
controller.CreateSubmission,
)
route.Get(
"/",
middlewares.JwtAccess(userRepo),
middlewares.RequireAnyRole(constants.RoleTypeAdmin, constants.RoleTypeMod),
controller.SearchSubmissions,
)
}

View File

@@ -27,6 +27,7 @@ import (
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgtype"
"github.com/jackc/pgx/v5/pgxpool"
"golang.org/x/crypto/bcrypt"
)
@@ -46,6 +47,7 @@ type authService struct {
roleRepo repositories.RoleRepository
tokenRepo repositories.TokenRepository
c cache.Cache
db *pgxpool.Pool
}
func NewAuthService(
@@ -53,12 +55,14 @@ func NewAuthService(
roleRepo repositories.RoleRepository,
tokenRepo repositories.TokenRepository,
c cache.Cache,
db *pgxpool.Pool,
) AuthService {
return &authService{
userRepo: userRepo,
roleRepo: roleRepo,
tokenRepo: tokenRepo,
c: c,
db: db,
}
}
@@ -113,14 +117,6 @@ func (a *authService) genToken(user *models.UserEntity) (*response.AuthResponse,
return &res, nil
}
func (a *authService) saveNewRefreshToken(ctx context.Context, params sqlc.UpdateUserRefreshTokenParams) error {
err := a.userRepo.UpdateRefreshToken(ctx, params)
if err != nil {
return err
}
return nil
}
func (a *authService) Signin(ctx context.Context, dto *request.SignInDto) (*response.AuthResponse, error) {
if !constants.EMAIL_REGEX.MatchString(dto.Email) {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid email")
@@ -153,7 +149,7 @@ func (a *authService) Signin(ctx context.Context, dto *request.SignInDto) (*resp
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
err = a.saveNewRefreshToken(
err = a.userRepo.UpdateRefreshToken(
ctx,
sqlc.UpdateUserRefreshTokenParams{
ID: pgID,
@@ -172,24 +168,32 @@ func (a *authService) Signin(ctx context.Context, dto *request.SignInDto) (*resp
}
func (a *authService) Logout(ctx context.Context, userId string) error {
tx, err := a.db.Begin(ctx)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to start transaction")
}
defer tx.Rollback(ctx)
uRepoTx := a.userRepo.WithTx(tx)
pgID, err := convert.StringToUUID(userId)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
user , err := a.userRepo.GetByID(ctx, pgID)
user, err := a.userRepo.GetByID(ctx, pgID)
if err != nil || user == nil {
return fiber.NewError(fiber.StatusInternalServerError, "Invalid user data")
}
err = a.userRepo.UpdateTokenVersion(ctx, sqlc.UpdateTokenVersionParams{
ID: pgID,
err = uRepoTx.UpdateTokenVersion(ctx, sqlc.UpdateTokenVersionParams{
ID: pgID,
TokenVersion: user.TokenVersion + 1,
})
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
err = a.userRepo.UpdateRefreshToken(ctx, sqlc.UpdateUserRefreshTokenParams{
err = uRepoTx.UpdateRefreshToken(ctx, sqlc.UpdateUserRefreshTokenParams{
ID: pgID,
RefreshToken: pgtype.Text{
String: "",
@@ -199,6 +203,10 @@ func (a *authService) Logout(ctx context.Context, userId string) error {
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
err = tx.Commit(ctx)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
return nil
}
@@ -228,7 +236,7 @@ func (a *authService) RefreshToken(ctx context.Context, id string, refreshToken
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
err = a.saveNewRefreshToken(
err = a.userRepo.UpdateRefreshToken(
ctx,
sqlc.UpdateUserRefreshTokenParams{
ID: pgID,
@@ -246,10 +254,19 @@ func (a *authService) RefreshToken(ctx context.Context, id string, refreshToken
}
func (a *authService) Signup(ctx context.Context, dto *request.SignUpDto) (*response.AuthResponse, error) {
tx, err := a.db.Begin(ctx)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to start transaction")
}
defer tx.Rollback(ctx)
uRepoTx := a.userRepo.WithTx(tx)
rRepoTx := a.roleRepo.WithTx(tx)
if !constants.EMAIL_REGEX.MatchString(dto.Email) {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid email")
}
err := constants.ValidatePassword(dto.Password)
err = constants.ValidatePassword(dto.Password)
if err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, err.Error())
}
@@ -276,7 +293,7 @@ func (a *authService) Signup(ctx context.Context, dto *request.SignUpDto) (*resp
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
user, err = a.userRepo.UpsertUser(
user, err = uRepoTx.UpsertUser(
ctx,
sqlc.UpsertUserParams{
Email: dto.Email,
@@ -295,7 +312,7 @@ func (a *authService) Signup(ctx context.Context, dto *request.SignUpDto) (*resp
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
_, err = a.userRepo.CreateProfile(
_, err = uRepoTx.CreateProfile(
ctx,
sqlc.CreateUserProfileParams{
UserID: userId,
@@ -308,7 +325,7 @@ func (a *authService) Signup(ctx context.Context, dto *request.SignUpDto) (*resp
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
role, err := a.roleRepo.GetByname(ctx, constants.RoleTypeUser.String())
role, err := a.roleRepo.GetByName(ctx, constants.RoleTypeUser.String())
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
@@ -318,7 +335,7 @@ func (a *authService) Signup(ctx context.Context, dto *request.SignUpDto) (*resp
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
err = a.roleRepo.CreateUserRole(
err = rRepoTx.CreateUserRole(
ctx,
sqlc.CreateUserRoleParams{
UserID: userId,
@@ -334,7 +351,7 @@ func (a *authService) Signup(ctx context.Context, dto *request.SignUpDto) (*resp
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
err = a.saveNewRefreshToken(
err = uRepoTx.UpdateRefreshToken(
ctx,
sqlc.UpdateUserRefreshTokenParams{
ID: userId,
@@ -348,6 +365,11 @@ func (a *authService) Signup(ctx context.Context, dto *request.SignUpDto) (*resp
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
err = tx.Commit(ctx)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
return data, nil
}
@@ -389,6 +411,15 @@ func (a *authService) ForgotPassword(ctx context.Context, dto *request.ForgotPas
}
func (a *authService) SigninWithGoogle(ctx context.Context, dto *request.SigninWithGoogleDto) (*response.AuthResponse, error) {
tx, err := a.db.Begin(ctx)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to start transaction")
}
defer tx.Rollback(ctx)
uRepoTx := a.userRepo.WithTx(tx)
rRepoTx := a.roleRepo.WithTx(tx)
user, err := a.userRepo.GetByEmail(ctx, dto.Email)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
@@ -403,7 +434,7 @@ func (a *authService) SigninWithGoogle(ctx context.Context, dto *request.SigninW
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
err = a.saveNewRefreshToken(
err = uRepoTx.UpdateRefreshToken(
ctx,
sqlc.UpdateUserRefreshTokenParams{
ID: userId,
@@ -419,7 +450,7 @@ func (a *authService) SigninWithGoogle(ctx context.Context, dto *request.SigninW
return data, nil
}
user, err = a.userRepo.UpsertUser(
user, err = uRepoTx.UpsertUser(
ctx,
sqlc.UpsertUserParams{
Email: dto.Email,
@@ -437,7 +468,7 @@ func (a *authService) SigninWithGoogle(ctx context.Context, dto *request.SigninW
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
_, err = a.userRepo.CreateProfile(
_, err = uRepoTx.CreateProfile(
ctx,
sqlc.CreateUserProfileParams{
UserID: userId,
@@ -454,7 +485,7 @@ func (a *authService) SigninWithGoogle(ctx context.Context, dto *request.SigninW
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
role, err := a.roleRepo.GetByname(ctx, constants.RoleTypeUser.String())
role, err := a.roleRepo.GetByName(ctx, constants.RoleTypeUser.String())
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
@@ -464,7 +495,7 @@ func (a *authService) SigninWithGoogle(ctx context.Context, dto *request.SigninW
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
err = a.roleRepo.CreateUserRole(
err = rRepoTx.CreateUserRole(
ctx,
sqlc.CreateUserRoleParams{
UserID: userId,
@@ -479,7 +510,7 @@ func (a *authService) SigninWithGoogle(ctx context.Context, dto *request.SigninW
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
err = a.saveNewRefreshToken(
err = uRepoTx.UpdateRefreshToken(
ctx,
sqlc.UpdateUserRefreshTokenParams{
ID: userId,
@@ -492,6 +523,10 @@ func (a *authService) SigninWithGoogle(ctx context.Context, dto *request.SigninW
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
err = tx.Commit(ctx)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
return data, nil
}

View File

@@ -0,0 +1,168 @@
package services
import (
"context"
"history-api/internal/dtos/request"
"history-api/internal/dtos/response"
"history-api/internal/gen/sqlc"
"history-api/internal/models"
"history-api/internal/repositories"
"history-api/pkg/constants"
"history-api/pkg/convert"
"github.com/gofiber/fiber/v3"
"github.com/jackc/pgx/v5/pgtype"
"github.com/jackc/pgx/v5/pgxpool"
)
type CommitService interface {
CreateCommit(ctx context.Context, userID string, projectID string, dto *request.CreateCommitDto) (*response.CommitResponse, error)
RestoreCommit(ctx context.Context, userID string, projectID string, dto *request.RestoreCommitDto) error
GetProjectCommits(ctx context.Context, projectID string) ([]*response.CommitResponse, error)
}
type commitService struct {
db *pgxpool.Pool
commitRepo repositories.CommitRepository
projectRepo repositories.ProjectRepository
}
func NewCommitService(
db *pgxpool.Pool,
commitRepo repositories.CommitRepository,
projectRepo repositories.ProjectRepository,
) CommitService {
return &commitService{
db: db,
commitRepo: commitRepo,
projectRepo: projectRepo,
}
}
func (s *commitService) checkWritePermission(ctx context.Context, userID string, projectUUID pgtype.UUID) error {
project, err := s.projectRepo.GetByID(ctx, projectUUID)
if err != nil {
return fiber.NewError(fiber.StatusNotFound, "Project not found")
}
if project.UserID == userID {
return nil
}
userUUID, _ := convert.StringToUUID(userID)
roleVal, err := s.projectRepo.CheckPermission(ctx, sqlc.CheckProjectPermissionParams{
ProjectID: projectUUID,
UserID: userUUID,
})
if err != nil {
return fiber.NewError(fiber.StatusForbidden, "You do not have permission to write to this project")
}
role := constants.ParseProjectMemberRole(roleVal)
if !role.CanWrite() {
return fiber.NewError(fiber.StatusForbidden, "You do not have permission to write to this project")
}
return nil
}
func (s *commitService) CreateCommit(ctx context.Context, userID string, projectID string, dto *request.CreateCommitDto) (*response.CommitResponse, error) {
tx, err := s.db.Begin(ctx)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to start transaction")
}
defer tx.Rollback(ctx)
cRepoTx := s.commitRepo.WithTx(tx)
pRepoTx := s.projectRepo.WithTx(tx)
projectUUID, err := convert.StringToUUID(projectID)
if err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project ID")
}
if err := s.checkWritePermission(ctx, userID, projectUUID); err != nil {
return nil, err
}
userUUID, err := convert.StringToUUID(userID)
if err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid user ID")
}
commit, err := cRepoTx.Create(ctx, sqlc.CreateCommitParams{
ProjectID: projectUUID,
SnapshotJson: dto.SnapshotJson,
UserID: userUUID,
EditSummary: convert.StringToText(dto.EditSummary),
})
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create commit")
}
commitUUID, err := convert.StringToUUID(commit.ID)
if err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid commit ID")
}
err = pRepoTx.UpdateLatestCommit(ctx, sqlc.UpdateLatestCommitParams{
ID: projectUUID,
LatestCommitID: commitUUID,
})
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update project latest commit")
}
if err := tx.Commit(ctx); err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to commit transaction")
}
return commit.ToResponse(), nil
}
func (s *commitService) RestoreCommit(ctx context.Context, userID string, projectID string, dto *request.RestoreCommitDto) error {
projectUUID, err := convert.StringToUUID(projectID)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid project ID")
}
if err := s.checkWritePermission(ctx, userID, projectUUID); err != nil {
return err
}
commitUUID, err := convert.StringToUUID(dto.CommitID)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid commit ID")
}
commit, err := s.commitRepo.GetByID(ctx, commitUUID)
if err != nil {
return fiber.NewError(fiber.StatusNotFound, "Commit not found")
}
if commit.ProjectID != projectID {
return fiber.NewError(fiber.StatusBadRequest, "Commit does not belong to this project")
}
err = s.projectRepo.UpdateLatestCommit(ctx, sqlc.UpdateLatestCommitParams{
ID: projectUUID,
LatestCommitID: commitUUID,
})
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to restore commit")
}
return nil
}
func (s *commitService) GetProjectCommits(ctx context.Context, projectID string) ([]*response.CommitResponse, error) {
projectUUID, err := convert.StringToUUID(projectID)
if err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project ID")
}
commits, err := s.commitRepo.GetByProjectID(ctx, projectUUID)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch commits")
}
return models.CommitsEntityToResponse(commits), nil
}

View File

@@ -2,7 +2,6 @@ package services
import (
"context"
"fmt"
"github.com/gofiber/fiber/v3"
"github.com/jackc/pgx/v5/pgtype"
@@ -24,6 +23,10 @@ type ProjectService interface {
DeleteProject(ctx context.Context, id string) error
CreateProject(ctx context.Context, userID string, dto *request.CreateProjectDto) (*response.ProjectResponse, error)
UpdateProject(ctx context.Context, id string, dto *request.UpdateProjectDto) (*response.ProjectResponse, error)
AddMember(ctx context.Context, callerID string, projectID string, dto *request.AddProjectMemberDto) (*response.ProjectResponse, error)
UpdateMemberRole(ctx context.Context, callerID string, projectID string, memberUserID string, dto *request.UpdateProjectMemberDto) (*response.ProjectResponse, error)
RemoveMember(ctx context.Context, callerID string, projectID string, memberUserID string) error
ChangeOwner(ctx context.Context, callerID string, projectID string, newOwnerID string) (*response.ProjectResponse, error)
}
type projectService struct {
@@ -36,6 +39,25 @@ func NewProjectService(projectRepo repositories.ProjectRepository) ProjectServic
}
}
func (s *projectService) checkCallerIsOwner(ctx context.Context, callerID string, projectUUID pgtype.UUID) error {
project, err := s.projectRepo.GetByID(ctx, projectUUID)
if err != nil {
return fiber.NewError(fiber.StatusNotFound, "Project not found")
}
if project.UserID == callerID {
return nil
}
callerUUID, _ := convert.StringToUUID(callerID)
role, err := s.projectRepo.CheckPermission(ctx, sqlc.CheckProjectPermissionParams{
ProjectID: projectUUID,
UserID: callerUUID,
})
if err != nil || constants.ParseProjectMemberRole(role) != constants.ProjectMemberRoleOwner {
return fiber.NewError(fiber.StatusForbidden, "Only project owner can perform this action")
}
return nil
}
func (s *projectService) GetProjectByID(ctx context.Context, id string) (*response.ProjectResponse, error) {
projectUUID, err := convert.StringToUUID(id)
if err != nil {
@@ -96,7 +118,7 @@ func (s *projectService) fillSearchArgs(arg *sqlc.SearchProjectsParams, dto *req
for _, statusStr := range dto.Statuses {
statusType := constants.ParseProjectStatusTypeText(statusStr)
if statusType != constants.ProjectStatusTypeUnknow {
arg.Statuses = append(arg.Statuses, fmt.Sprintf("%d", statusType.Int16()))
arg.Statuses = append(arg.Statuses, statusType.Int16())
}
}
}
@@ -211,15 +233,11 @@ func (s *projectService) UpdateProject(ctx context.Context, id string, dto *requ
}
arg := sqlc.UpdateProjectParams{
ID: projectUUID,
ID: projectUUID,
Title: convert.PtrToText(dto.Title),
Description: convert.PtrToText(dto.Description),
}
if dto.Title != nil {
arg.Title = convert.PtrToText(dto.Title)
}
if dto.Description != nil {
arg.Description = convert.PtrToText(dto.Description)
}
if dto.Status != nil {
statusType := constants.ParseProjectStatusTypeText(*dto.Status)
if statusType == constants.ProjectStatusTypeUnknow {
@@ -254,3 +272,143 @@ func (s *projectService) DeleteProject(ctx context.Context, id string) error {
return nil
}
func (s *projectService) AddMember(ctx context.Context, callerID string, projectID string, dto *request.AddProjectMemberDto) (*response.ProjectResponse, error) {
projectUUID, err := convert.StringToUUID(projectID)
if err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project ID format")
}
if err := s.checkCallerIsOwner(ctx, callerID, projectUUID); err != nil {
return nil, err
}
memberUUID, err := convert.StringToUUID(dto.UserID)
if err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid member user ID format")
}
callerUUID, _ := convert.StringToUUID(callerID)
if dto.UserID == callerID {
return nil, fiber.NewError(fiber.StatusBadRequest, "Cannot add yourself as a member")
}
role := constants.ParseProjectMemberRoleText(dto.Role)
err = s.projectRepo.AddMember(ctx, sqlc.AddProjectMemberParams{
ProjectID: projectUUID,
UserID: memberUUID,
Role: role.Int16(),
InvitedBy: callerUUID,
})
if err != nil {
return nil, fiber.NewError(fiber.StatusConflict, "Member already exists or user not found")
}
project, err := s.projectRepo.GetByID(ctx, projectUUID)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch updated project")
}
return project.ToResponse(), nil
}
func (s *projectService) UpdateMemberRole(ctx context.Context, callerID string, projectID string, memberUserID string, dto *request.UpdateProjectMemberDto) (*response.ProjectResponse, error) {
projectUUID, err := convert.StringToUUID(projectID)
if err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project ID format")
}
if err := s.checkCallerIsOwner(ctx, callerID, projectUUID); err != nil {
return nil, err
}
memberUUID, err := convert.StringToUUID(memberUserID)
if err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid member user ID format")
}
role := constants.ParseProjectMemberRoleText(dto.Role)
err = s.projectRepo.UpdateMemberRole(ctx, sqlc.UpdateProjectMemberRoleParams{
ProjectID: projectUUID,
UserID: memberUUID,
Role: role.Int16(),
})
if err != nil {
return nil, fiber.NewError(fiber.StatusNotFound, "Member not found")
}
project, err := s.projectRepo.GetByID(ctx, projectUUID)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch updated project")
}
return project.ToResponse(), nil
}
func (s *projectService) RemoveMember(ctx context.Context, callerID string, projectID string, memberUserID string) error {
projectUUID, err := convert.StringToUUID(projectID)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid project ID format")
}
if err := s.checkCallerIsOwner(ctx, callerID, projectUUID); err != nil {
return err
}
memberUUID, err := convert.StringToUUID(memberUserID)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid member user ID format")
}
if callerID == memberUserID {
return fiber.NewError(fiber.StatusBadRequest, "Cannot remove yourself from the project")
}
err = s.projectRepo.RemoveMember(ctx, sqlc.RemoveProjectMemberParams{
ProjectID: projectUUID,
UserID: memberUUID,
})
if err != nil {
return fiber.NewError(fiber.StatusNotFound, "Member not found")
}
return nil
}
func (s *projectService) ChangeOwner(ctx context.Context, callerID string, projectID string, newOwnerID string) (*response.ProjectResponse, error) {
projectUUID, err := convert.StringToUUID(projectID)
if err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project ID format")
}
if err := s.checkCallerIsOwner(ctx, callerID, projectUUID); err != nil {
return nil, err
}
if callerID == newOwnerID {
return nil, fiber.NewError(fiber.StatusBadRequest, "You are already the owner")
}
newOwnerUUID, err := convert.StringToUUID(newOwnerID)
if err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid new owner ID format")
}
err = s.projectRepo.ChangeOwner(ctx, sqlc.ChangeProjectOwnerParams{
ID: projectUUID,
UserID: newOwnerUUID,
})
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to change owner")
}
project, err := s.projectRepo.GetByID(ctx, projectUUID)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch updated project")
}
return project.ToResponse(), nil
}

View File

@@ -19,7 +19,6 @@ type roleService struct {
roleRepo repositories.RoleRepository
}
func NewRoleService(
roleRepo repositories.RoleRepository,
) RoleService {
@@ -48,4 +47,4 @@ func (r *roleService) GetRoleByID(ctx context.Context, id string) (*response.Rol
}
return role.ToResponse(), nil
}
}

View File

@@ -0,0 +1,282 @@
package services
import (
"context"
"fmt"
"history-api/internal/dtos/request"
"history-api/internal/dtos/response"
"history-api/internal/gen/sqlc"
"history-api/internal/models"
"history-api/internal/repositories"
"history-api/pkg/cache"
"history-api/pkg/constants"
"history-api/pkg/convert"
"slices"
"github.com/gofiber/fiber/v3"
"github.com/jackc/pgx/v5/pgtype"
"github.com/jackc/pgx/v5/pgxpool"
"golang.org/x/sync/errgroup"
)
type SubmissionService interface {
CreateSubmission(ctx context.Context, userID string, dto *request.CreateSubmissionDto) (*response.SubmissionResponse, error)
UpdateSubmissionStatus(ctx context.Context, reviewerID string, submissionID string, dto *request.UpdateSubmissionStatusDto) (*response.SubmissionResponse, error)
GetSubmissionByID(ctx context.Context, id string) (*response.SubmissionResponse, error)
SearchSubmissions(ctx context.Context, dto *request.SearchSubmissionDto) (*response.PaginatedResponse, error)
DeleteSubmission(ctx context.Context, userID string, id string, claims *response.JWTClaims) error
}
type submissionService struct {
submissionRepo repositories.SubmissionRepository
projectRepo repositories.ProjectRepository
commitRepo repositories.CommitRepository
userRepo repositories.UserRepository
db *pgxpool.Pool
c cache.Cache
}
func NewSubmissionService(
submissionRepo repositories.SubmissionRepository,
projectRepo repositories.ProjectRepository,
commitRepo repositories.CommitRepository,
userRepo repositories.UserRepository,
db *pgxpool.Pool,
c cache.Cache,
) SubmissionService {
return &submissionService{
submissionRepo: submissionRepo,
projectRepo: projectRepo,
commitRepo: commitRepo,
userRepo: userRepo,
db: db,
c: c,
}
}
func (s *submissionService) CreateSubmission(ctx context.Context, userID string, dto *request.CreateSubmissionDto) (*response.SubmissionResponse, error) {
projectUUID, err := convert.StringToUUID(dto.ProjectID)
if err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project ID")
}
commitUUID, err := convert.StringToUUID(dto.CommitID)
if err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid commit ID")
}
userUUID, err := convert.StringToUUID(userID)
if err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid user ID")
}
commit, err := s.commitRepo.GetByID(ctx, commitUUID)
if err != nil {
return nil, fiber.NewError(fiber.StatusNotFound, "Commit not found")
}
if commit.ProjectID != dto.ProjectID {
return nil, fiber.NewError(fiber.StatusBadRequest, "Commit does not belong to project")
}
arg := sqlc.CreateSubmissionParams{
ProjectID: projectUUID,
CommitID: commitUUID,
UserID: userUUID,
Status: constants.StatusTypePending.Int16(),
Content: convert.StringToText(dto.Content),
}
submission, err := s.submissionRepo.Create(ctx, arg)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create submission")
}
return submission.ToResponse(), nil
}
func (s *submissionService) UpdateSubmissionStatus(ctx context.Context, reviewerID string, submissionID string, dto *request.UpdateSubmissionStatusDto) (*response.SubmissionResponse, error) {
submissionUUID, err := convert.StringToUUID(submissionID)
if err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid submission ID")
}
reviewerUUID, err := convert.StringToUUID(reviewerID)
if err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid reviewer ID")
}
status := constants.ParseStatusTypeText(dto.Status)
if status == constants.StatusTypeUnknown {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid status")
}
submission, err := s.submissionRepo.GetByID(ctx, submissionUUID)
if err != nil {
return nil, fiber.NewError(fiber.StatusNotFound, "Submission not found")
}
if submission.Status != constants.StatusTypePending {
return nil, fiber.NewError(fiber.StatusBadRequest, "Submission already processed")
}
arg := sqlc.UpdateSubmissionParams{
ID: submissionUUID,
Status: pgtype.Int2{Int16: status.Int16(), Valid: true},
ReviewedBy: reviewerUUID,
ReviewNote: convert.StringToText(dto.ReviewNote),
}
updatedSubmission, err := s.submissionRepo.Update(ctx, arg)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update submission status")
}
_ = s.c.Del(ctx, fmt.Sprintf("proejct:id:%s", submission.ProjectID))
return updatedSubmission.ToResponse(), nil
}
func (m *submissionService) fillSearchArgs(arg *sqlc.SearchSubmissionsParams, dto *request.SearchSubmissionDto) {
if dto.Sort != "" {
arg.Sort = pgtype.Text{String: dto.Sort, Valid: true}
} else {
arg.Sort = pgtype.Text{String: "id", Valid: true}
}
arg.Order = pgtype.Text{String: "asc", Valid: true}
if dto.Order == "desc" {
arg.Order = pgtype.Text{String: "desc", Valid: true}
}
if len(dto.Statuses) > 0 {
for _, id := range dto.Statuses {
if u := constants.ParseStatusTypeText(id); u != constants.StatusTypeUnknown {
arg.Statuses = append(arg.Statuses, u.Int16())
}
}
}
if len(dto.UserIDs) > 0 {
for _, id := range dto.UserIDs {
if u, err := convert.StringToUUID(id); err == nil {
arg.UserIds = append(arg.UserIds, u)
}
}
}
if dto.ReviewedBy != nil {
if rvID, err := convert.StringToUUID(*dto.ReviewedBy); err == nil {
arg.ReviewedBy = rvID
}
}
if dto.CreatedFrom != nil {
arg.CreatedFrom = pgtype.Timestamptz{Time: *dto.CreatedFrom, Valid: true}
}
if dto.CreatedTo != nil {
arg.CreatedTo = pgtype.Timestamptz{Time: *dto.CreatedTo, Valid: true}
}
if dto.Search != "" {
arg.SearchText = pgtype.Text{String: dto.Search, Valid: true}
}
}
func (s *submissionService) GetSubmissionByID(ctx context.Context, id string) (*response.SubmissionResponse, error) {
submissionUUID, err := convert.StringToUUID(id)
if err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid submission ID")
}
submission, err := s.submissionRepo.GetByID(ctx, submissionUUID)
if err != nil {
return nil, fiber.NewError(fiber.StatusNotFound, "Submission not found")
}
return submission.ToResponse(), nil
}
func (s *submissionService) SearchSubmissions(ctx context.Context, dto *request.SearchSubmissionDto) (*response.PaginatedResponse, error) {
if dto.Page < 1 {
dto.Page = 1
}
if dto.Limit == 0 {
dto.Limit = 20
}
offset := (dto.Page - 1) * dto.Limit
arg := sqlc.SearchSubmissionsParams{
Limit: int32(dto.Limit),
Offset: int32(offset),
}
s.fillSearchArgs(&arg, dto)
var rows []*models.SubmissionEntity
var totalRecords int64
g, gCtx := errgroup.WithContext(ctx)
g.Go(func() error {
var err error
rows, err = s.submissionRepo.Search(gCtx, arg)
return err
})
g.Go(func() error {
countArg := sqlc.CountSubmissionsParams{
UserIds: arg.UserIds,
Statuses: arg.Statuses,
ReviewedBy: arg.ReviewedBy,
CreatedFrom: arg.CreatedFrom,
CreatedTo: arg.CreatedTo,
SearchText: arg.SearchText,
}
var err error
totalRecords, err = s.submissionRepo.Count(gCtx, countArg)
return err
})
if err := g.Wait(); err != nil {
return nil, err
}
submissions := models.SubmissionsEntityToResponse(rows)
return response.BuildPaginatedResponse(submissions, totalRecords, dto.Page, dto.Limit), nil
}
func (s *submissionService) DeleteSubmission(ctx context.Context, userID string, id string, claims *response.JWTClaims) error {
submissionUUID, err := convert.StringToUUID(id)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid submission ID")
}
submission, err := s.submissionRepo.GetByID(ctx, submissionUUID)
if err != nil {
return fiber.NewError(fiber.StatusNotFound, "Submission not found")
}
shoudDelete := false
if slices.Contains(claims.Roles, constants.RoleTypeAdmin) || slices.Contains(claims.Roles, constants.RoleTypeMod) {
shoudDelete = true
}
if submission.UserID == claims.UId && submission.Status == constants.StatusTypePending {
shoudDelete = true
}
if !shoudDelete {
return fiber.NewError(fiber.StatusForbidden, "You don't have permission to delete this submission")
}
err = s.submissionRepo.Delete(ctx, submissionUUID)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete submission")
}
return nil
}

View File

@@ -15,13 +15,13 @@ import (
"github.com/gofiber/fiber/v3"
"github.com/jackc/pgx/v5/pgtype"
"github.com/jackc/pgx/v5/pgxpool"
"golang.org/x/crypto/bcrypt"
"golang.org/x/sync/errgroup"
)
type UserService interface {
//user
GetUserCurrent(ctx context.Context, userId string) (*response.UserResponse, error)
UpdateProfile(ctx context.Context, userId string, dto *request.UpdateProfileDto) (*response.UserResponse, error)
ChangePassword(ctx context.Context, userId string, dto *request.ChangePasswordDto) error
@@ -37,21 +37,33 @@ type userService struct {
userRepo repositories.UserRepository
roleRepo repositories.RoleRepository
c cache.Cache
db *pgxpool.Pool
}
func NewUserService(
userRepo repositories.UserRepository,
roleRepo repositories.RoleRepository,
c cache.Cache,
db *pgxpool.Pool,
) UserService {
return &userService{
userRepo: userRepo,
roleRepo: roleRepo,
c: c,
db: db,
}
}
func (u *userService) ChangePassword(ctx context.Context, userId string, dto *request.ChangePasswordDto) error {
tx, err := u.db.Begin(ctx)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to start transaction")
}
defer tx.Rollback(ctx)
uRepo := u.userRepo.WithTx(tx)
pgID, err := convert.StringToUUID(userId)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
@@ -68,7 +80,6 @@ func (u *userService) ChangePassword(ctx context.Context, userId string, dto *re
if dto.OldPassword == "" {
return fiber.NewError(fiber.StatusBadRequest, "Old password required")
}
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(dto.OldPassword)); err != nil {
return fiber.NewError(fiber.StatusUnauthorized, "Invalid password!")
}
@@ -81,7 +92,7 @@ func (u *userService) ChangePassword(ctx context.Context, userId string, dto *re
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
err = u.userRepo.UpdatePassword(ctx, sqlc.UpdateUserPasswordParams{
err = uRepo.UpdatePassword(ctx, sqlc.UpdateUserPasswordParams{
ID: pgID,
PasswordHash: pgtype.Text{String: string(hashPassword), Valid: true},
})
@@ -89,10 +100,30 @@ func (u *userService) ChangePassword(ctx context.Context, userId string, dto *re
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
err = uRepo.UpdateTokenVersion(ctx, sqlc.UpdateTokenVersionParams{
ID: pgID,
TokenVersion: user.TokenVersion + 1,
})
if err != nil {
return err
}
if err := tx.Commit(ctx); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to commit transaction")
}
return nil
}
func (u *userService) ChangeRoleUser(ctx context.Context, userId string, claims *response.JWTClaims, dto *request.ChangeRoleDto) (*response.UserResponse, error) {
tx, err := u.db.Begin(ctx)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to start transaction")
}
defer tx.Rollback(ctx)
uRepo := u.userRepo.WithTx(tx)
rRepo := u.roleRepo.WithTx(tx)
userUUID, err := convert.StringToUUID(userId)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
@@ -180,12 +211,12 @@ func (u *userService) ChangeRoleUser(ctx context.Context, userId string, claims
user.Roles = append(user.Roles, role.ToRoleSimple())
}
err = u.roleRepo.BulkDeleteRolesFromUser(ctx, userUUID)
err = rRepo.BulkDeleteRolesFromUser(ctx, userUUID)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
err = u.roleRepo.CreateUserRole(ctx, sqlc.CreateUserRoleParams{
err = rRepo.CreateUserRole(ctx, sqlc.CreateUserRoleParams{
UserID: userUUID,
Column2: roleIdList,
})
@@ -193,7 +224,7 @@ func (u *userService) ChangeRoleUser(ctx context.Context, userId string, claims
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
err = u.userRepo.UpdateTokenVersion(ctx, sqlc.UpdateTokenVersionParams{
err = uRepo.UpdateTokenVersion(ctx, sqlc.UpdateTokenVersionParams{
ID: userUUID,
TokenVersion: user.TokenVersion + 1,
})
@@ -202,6 +233,11 @@ func (u *userService) ChangeRoleUser(ctx context.Context, userId string, claims
}
user.TokenVersion += 1
err = tx.Commit(ctx)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
mapCache := map[string]any{
fmt.Sprintf("user:email:%s", user.Email): user,
fmt.Sprintf("user:id:%s", user.ID): user,
@@ -209,7 +245,6 @@ func (u *userService) ChangeRoleUser(ctx context.Context, userId string, claims
_ = u.c.MSet(ctx, mapCache, constants.NormalCacheDuration)
return user.ToResponse(), nil
}
func (u *userService) DeleteUser(ctx context.Context, userId string) error {
@@ -264,18 +299,6 @@ func (u *userService) UpdateProfile(ctx context.Context, userId string, dto *req
return newUser.ToResponse(), nil
}
func (u *userService) GetUserCurrent(ctx context.Context, userId string) (*response.UserResponse, error) {
pgID, err := convert.StringToUUID(userId)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
user, err := u.userRepo.GetByID(ctx, pgID)
if err != nil {
return nil, fiber.NewError(fiber.StatusNotFound, err.Error())
}
return user.ToResponse(), nil
}
func (u *userService) RestoreUser(ctx context.Context, userId string) (*response.UserResponse, error) {
pgID, err := convert.StringToUUID(userId)
if err != nil {

View File

@@ -15,6 +15,7 @@ import (
"github.com/gofiber/fiber/v3"
"github.com/jackc/pgx/v5/pgtype"
"github.com/jackc/pgx/v5/pgxpool"
"golang.org/x/sync/errgroup"
)
@@ -33,6 +34,7 @@ type verificationService struct {
userRepo repositories.UserRepository
roleRepo repositories.RoleRepository
c cache.Cache
db *pgxpool.Pool
}
func NewVerificationService(
@@ -41,6 +43,7 @@ func NewVerificationService(
userRepo repositories.UserRepository,
roleRepo repositories.RoleRepository,
c cache.Cache,
db *pgxpool.Pool,
) VerificationService {
return &verificationService{
verificationRepo: verificationRepo,
@@ -48,6 +51,7 @@ func NewVerificationService(
userRepo: userRepo,
roleRepo: roleRepo,
c: c,
db: db,
}
}
@@ -275,6 +279,12 @@ func (v *verificationService) SearchVerification(ctx context.Context, dto *reque
}
func (v *verificationService) UpdateStatusVerification(ctx context.Context, userId string, verificationId string, dto *request.UpdateVerificationStatusDto) (*response.UserVerificationResponse, error) {
tx, err := v.db.Begin(ctx)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to start transaction")
}
defer tx.Rollback(ctx)
statusType := constants.ParseStatusTypeText(dto.Status)
if statusType == constants.StatusTypeUnknown {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Unknown status type!")
@@ -289,7 +299,7 @@ func (v *verificationService) UpdateStatusVerification(ctx context.Context, user
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
historianRole, err := v.roleRepo.GetByname(ctx, constants.RoleTypeHistorian.String())
historianRole, err := v.roleRepo.GetByName(ctx, constants.RoleTypeHistorian.String())
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
@@ -318,7 +328,11 @@ func (v *verificationService) UpdateStatusVerification(ctx context.Context, user
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
err = v.verificationRepo.UpdateStatus(
vRepoTx := v.verificationRepo.WithTx(tx)
rRepoTx := v.roleRepo.WithTx(tx)
uRepoTx := v.userRepo.WithTx(tx)
err = vRepoTx.UpdateStatus(
ctx,
sqlc.UpdateUserVerificationStatusParams{
ID: verificationUUID,
@@ -354,12 +368,12 @@ func (v *verificationService) UpdateStatusVerification(ctx context.Context, user
roleIdList = append(roleIdList, roleID)
}
err = v.roleRepo.BulkDeleteRolesFromUser(ctx, userVerificationUUID)
err = rRepoTx.BulkDeleteRolesFromUser(ctx, userVerificationUUID)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
err = v.roleRepo.CreateUserRole(ctx, sqlc.CreateUserRoleParams{
err = rRepoTx.CreateUserRole(ctx, sqlc.CreateUserRoleParams{
UserID: userVerificationUUID,
Column2: roleIdList,
})
@@ -367,7 +381,7 @@ func (v *verificationService) UpdateStatusVerification(ctx context.Context, user
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
err = v.userRepo.UpdateTokenVersion(ctx, sqlc.UpdateTokenVersionParams{
err = uRepoTx.UpdateTokenVersion(ctx, sqlc.UpdateTokenVersionParams{
ID: userVerificationUUID,
TokenVersion: userVerification.TokenVersion + 1,
})
@@ -376,11 +390,19 @@ func (v *verificationService) UpdateStatusVerification(ctx context.Context, user
}
userVerification.TokenVersion += 1
if err := tx.Commit(ctx); err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to commit transaction")
}
mapCache := map[string]any{
fmt.Sprintf("user:email:%s", userVerification.Email): userVerification,
fmt.Sprintf("user:id:%s", userVerification.ID): userVerification,
}
_ = v.c.MSet(ctx, mapCache, constants.NormalCacheDuration)
} else {
if err := tx.Commit(ctx); err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to commit transaction")
}
}
v.c.PublishTask(ctx, constants.StreamEmailName, constants.TaskTypeNotifyHistorianReview, data)