UPDATE: Project Module
All checks were successful
Build and Release / release (push) Successful in 1m15s

This commit is contained in:
2026-04-25 14:05:15 +07:00
parent 44a63f29c6
commit ac90236022
71 changed files with 5110 additions and 257 deletions

View File

@@ -0,0 +1,200 @@
package controllers
import (
"context"
"time"
"github.com/gofiber/fiber/v3"
"history-api/internal/dtos/request"
"history-api/internal/dtos/response"
"history-api/internal/services"
"history-api/pkg/validator"
)
type ProjectController struct {
service services.ProjectService
}
func NewProjectController(service services.ProjectService) *ProjectController {
return &ProjectController{
service: service,
}
}
// GetProjectByID godoc
// @Summary Get project by ID
// @Description Retrieve project details by specific ID
// @Tags Projects
// @Accept json
// @Produce json
// @Param id path string true "Project ID"
// @Success 200 {object} response.CommonResponse
// @Failure 400 {object} response.CommonResponse
// @Failure 404 {object} response.CommonResponse
// @Failure 500 {object} response.CommonResponse
// @Router /projects/{id} [get]
func (h *ProjectController) GetProjectByID(c fiber.Ctx) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
projectID := c.Params("id")
res, err := h.service.GetProjectByID(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,
})
}
// SearchProject godoc
// @Summary Search projects
// @Description Search and filter projects with pagination
// @Tags Projects
// @Accept json
// @Produce json
// @Param query query request.SearchProjectDto false "Search Query"
// @Success 200 {object} response.PaginatedResponse
// @Failure 400 {object} response.CommonResponse
// @Failure 500 {object} response.CommonResponse
// @Router /projects [get]
func (h *ProjectController) SearchProject(c fiber.Ctx) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
dto := &request.SearchProjectDto{}
if err := validator.ValidateQueryDto(c, dto); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(response.CommonResponse{
Status: false,
Errors: err,
})
}
res, err := h.service.SearchProject(ctx, dto)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
Status: false,
Message: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(res)
}
// CreateProject godoc
// @Summary Create a new project
// @Description Create a project for the current authenticated user
// @Tags Projects
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param request body request.CreateProjectDto true "Project Data"
// @Success 201 {object} response.CommonResponse
// @Failure 400 {object} response.CommonResponse
// @Failure 500 {object} response.CommonResponse
// @Router /projects [post]
func (h *ProjectController) CreateProject(c fiber.Ctx) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
dto := &request.CreateProjectDto{}
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.CreateProject(ctx, uid, 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,
})
}
// UpdateProject godoc
// @Summary Update a project
// @Description Update project properties (Title, Description, Status)
// @Tags Projects
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path string true "Project ID"
// @Param request body request.UpdateProjectDto true "Project Data"
// @Success 200 {object} response.CommonResponse
// @Failure 400 {object} response.CommonResponse
// @Failure 404 {object} response.CommonResponse
// @Failure 500 {object} response.CommonResponse
// @Router /projects/{id} [put]
func (h *ProjectController) UpdateProject(c fiber.Ctx) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
projectID := c.Params("id")
dto := &request.UpdateProjectDto{}
if err := validator.ValidateBodyDto(c, dto); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(response.CommonResponse{
Status: false,
Errors: err,
})
}
res, err := h.service.UpdateProject(ctx, 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,
Data: res,
})
}
// DeleteProject godoc
// @Summary Delete a project
// @Description Delete project by ID
// @Tags Projects
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path string true "Project ID"
// @Success 200 {object} response.CommonResponse
// @Failure 400 {object} response.CommonResponse
// @Failure 404 {object} response.CommonResponse
// @Failure 500 {object} response.CommonResponse
// @Router /projects/{id} [delete]
func (h *ProjectController) DeleteProject(c fiber.Ctx) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
projectID := c.Params("id")
err := h.service.DeleteProject(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,
Message: "Project deleted successfully",
})
}

View File

@@ -15,17 +15,20 @@ type UserController struct {
service services.UserService
mediaService services.MediaService
verificationService services.VerificationService
projectService services.ProjectService
}
func NewUserController(
svc services.UserService,
mediaSvc services.MediaService,
verificationSvc services.VerificationService,
projectSvc services.ProjectService,
) *UserController {
return &UserController{
service: svc,
mediaService: mediaSvc,
verificationService: verificationSvc,
projectService: projectSvc,
}
}
@@ -408,3 +411,80 @@ func (h *UserController) SearchUser(c fiber.Ctx) error {
}
return c.Status(fiber.StatusOK).JSON(res)
}
// GetUserProject godoc
// @Summary Get current user's projects
// @Description Retrieve project list of the currently authenticated user
// @Tags Users
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param query query request.GetProjectsByUserDto false "Pagination Query"
// @Success 200 {object} response.CommonResponse
// @Failure 400 {object} response.CommonResponse
// @Failure 500 {object} response.CommonResponse
// @Router /users/current/project [get]
func (h *UserController) GetUserProject(c fiber.Ctx) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
dto := &request.GetProjectsByUserDto{}
if err := validator.ValidateQueryDto(c, dto); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(response.CommonResponse{
Status: false,
Errors: err,
})
}
res, err := h.projectService.GetProjectByUserID(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,
})
}
// GetProjectByUserID godoc
// @Summary Get user's projects by user ID
// @Description Retrieve project list by specific user ID
// @Tags Users
// @Accept json
// @Produce json
// @Param id path string true "User ID"
// @Param query query request.GetProjectsByUserDto false "Pagination Query"
// @Success 200 {object} response.CommonResponse
// @Failure 400 {object} response.CommonResponse
// @Failure 500 {object} response.CommonResponse
// @Router /users/{id}/project [get]
func (h *UserController) GetProjectByUserID(c fiber.Ctx) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
userID := c.Params("id")
dto := &request.GetProjectsByUserDto{}
if err := validator.ValidateQueryDto(c, dto); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(response.CommonResponse{
Status: false,
Errors: err,
})
}
res, err := h.projectService.GetProjectByUserID(ctx, userID, 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,
})
}

View File

@@ -0,0 +1,30 @@
package request
import "time"
type SearchProjectDto struct {
PaginationDto
Sort string `json:"sort" query:"sort" validate:"omitempty,oneof=created_at updated_at title"`
Search string `json:"search" query:"search" validate:"omitempty,max=200"`
UserIDs []string `json:"user_ids" query:"user_ids" validate:"omitempty,dive,uuid"`
Statuses []string `json:"statuses" query:"statuses" validate:"omitempty,dive,oneof=PRIVATE PUBLIC ARCHIVE"`
CreatedFrom *time.Time `json:"created_from" query:"created_from"`
CreatedTo *time.Time `json:"created_to" query:"created_to"`
}
type GetProjectsByUserDto struct {
CursorID string `json:"cursor_id" query:"cursor_id" validate:"omitempty,uuid"`
Limit int32 `json:"limit" query:"limit" validate:"omitempty,min=1,max=100"`
}
type CreateProjectDto struct {
Title string `json:"title" validate:"required,max=255"`
Description *string `json:"description" validate:"omitempty"`
Status *string `json:"status" validate:"omitempty,oneof=PRIVATE PUBLIC ARCHIVE"`
}
type UpdateProjectDto struct {
Title *string `json:"title" validate:"omitempty,max=255"`
Description *string `json:"description" validate:"omitempty"`
Status *string `json:"status" validate:"omitempty,oneof=PRIVATE PUBLIC ARCHIVE"`
}

View File

@@ -15,7 +15,7 @@ type CommonResponse struct {
type JWTClaims struct {
UId string `json:"uid"`
Roles []constants.Role `json:"roles"`
Roles []constants.RoleType `json:"roles"`
TokenVersion int32 `json:"token_version"`
jwt.RegisteredClaims
}

View File

@@ -0,0 +1,20 @@
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"`
}

View File

@@ -53,6 +53,38 @@ func (q *Queries) DeleteEntity(ctx context.Context, id pgtype.UUID) error {
return err
}
const getEntitiesByIDs = `-- name: GetEntitiesByIDs :many
SELECT id, name, description, thumbnail_url, is_deleted, created_at, updated_at FROM entities WHERE id = ANY($1::uuid[]) AND is_deleted = false
`
func (q *Queries) GetEntitiesByIDs(ctx context.Context, dollar_1 []pgtype.UUID) ([]Entity, error) {
rows, err := q.db.Query(ctx, getEntitiesByIDs, dollar_1)
if err != nil {
return nil, err
}
defer rows.Close()
items := []Entity{}
for rows.Next() {
var i Entity
if err := rows.Scan(
&i.ID,
&i.Name,
&i.Description,
&i.ThumbnailUrl,
&i.IsDeleted,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getEntityById = `-- name: GetEntityById :one
SELECT id, name, description, thumbnail_url, is_deleted, created_at, updated_at
FROM entities

View File

@@ -132,6 +132,41 @@ func (q *Queries) GetMediaByID(ctx context.Context, id pgtype.UUID) (Media, erro
return i, err
}
const getMediaByIDs = `-- name: GetMediaByIDs :many
SELECT id, user_id, storage_key, original_name, mime_type, size, file_metadata, created_at, updated_at FROM medias
WHERE id = ANY($1::uuid[])
`
func (q *Queries) GetMediaByIDs(ctx context.Context, dollar_1 []pgtype.UUID) ([]Media, error) {
rows, err := q.db.Query(ctx, getMediaByIDs, dollar_1)
if err != nil {
return nil, err
}
defer rows.Close()
items := []Media{}
for rows.Next() {
var i Media
if err := rows.Scan(
&i.ID,
&i.UserID,
&i.StorageKey,
&i.OriginalName,
&i.MimeType,
&i.Size,
&i.FileMetadata,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getMediasByUserID = `-- name: GetMediasByUserID :many
SELECT id, user_id, storage_key, original_name, mime_type, size, file_metadata, created_at, updated_at FROM medias
WHERE user_id = $1

View File

@@ -67,7 +67,7 @@ RETURNING id, geo_type, draw_geometry, binding, time_start, time_end,
`
type CreateGeometryParams struct {
GeoType string `json:"geo_type"`
GeoType int16 `json:"geo_type"`
DrawGeometry json.RawMessage `json:"draw_geometry"`
Binding []byte `json:"binding"`
TimeStart pgtype.Int4 `json:"time_start"`
@@ -80,7 +80,7 @@ type CreateGeometryParams struct {
type CreateGeometryRow struct {
ID pgtype.UUID `json:"id"`
GeoType string `json:"geo_type"`
GeoType int16 `json:"geo_type"`
DrawGeometry json.RawMessage `json:"draw_geometry"`
Binding []byte `json:"binding"`
TimeStart pgtype.Int4 `json:"time_start"`
@@ -137,6 +137,68 @@ func (q *Queries) DeleteGeometry(ctx context.Context, id pgtype.UUID) error {
return err
}
const getGeometriesByIDs = `-- name: GetGeometriesByIDs :many
SELECT
id, geo_type, draw_geometry, binding, time_start, time_end,
ST_XMin(bbox)::float8 as min_lng,
ST_YMin(bbox)::float8 as min_lat,
ST_XMax(bbox)::float8 as max_lng,
ST_YMax(bbox)::float8 as max_lat,
is_deleted, created_at, updated_at
FROM geometries
WHERE id = ANY($1::uuid[]) AND is_deleted = false
`
type GetGeometriesByIDsRow struct {
ID pgtype.UUID `json:"id"`
GeoType int16 `json:"geo_type"`
DrawGeometry json.RawMessage `json:"draw_geometry"`
Binding []byte `json:"binding"`
TimeStart pgtype.Int4 `json:"time_start"`
TimeEnd pgtype.Int4 `json:"time_end"`
MinLng float64 `json:"min_lng"`
MinLat float64 `json:"min_lat"`
MaxLng float64 `json:"max_lng"`
MaxLat float64 `json:"max_lat"`
IsDeleted bool `json:"is_deleted"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
func (q *Queries) GetGeometriesByIDs(ctx context.Context, dollar_1 []pgtype.UUID) ([]GetGeometriesByIDsRow, error) {
rows, err := q.db.Query(ctx, getGeometriesByIDs, dollar_1)
if err != nil {
return nil, err
}
defer rows.Close()
items := []GetGeometriesByIDsRow{}
for rows.Next() {
var i GetGeometriesByIDsRow
if err := rows.Scan(
&i.ID,
&i.GeoType,
&i.DrawGeometry,
&i.Binding,
&i.TimeStart,
&i.TimeEnd,
&i.MinLng,
&i.MinLat,
&i.MaxLng,
&i.MaxLat,
&i.IsDeleted,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getGeometryById = `-- name: GetGeometryById :one
SELECT id, geo_type, draw_geometry, binding, time_start, time_end,
ST_XMin(bbox)::float8 as min_lng, ST_YMin(bbox)::float8 as min_lat, ST_XMax(bbox)::float8 as max_lng, ST_YMax(bbox)::float8 as max_lat,
@@ -147,7 +209,7 @@ WHERE id = $1 AND is_deleted = false
type GetGeometryByIdRow struct {
ID pgtype.UUID `json:"id"`
GeoType string `json:"geo_type"`
GeoType int16 `json:"geo_type"`
DrawGeometry json.RawMessage `json:"draw_geometry"`
Binding []byte `json:"binding"`
TimeStart pgtype.Int4 `json:"time_start"`
@@ -232,7 +294,7 @@ type SearchGeometriesParams struct {
type SearchGeometriesRow struct {
ID pgtype.UUID `json:"id"`
GeoType string `json:"geo_type"`
GeoType int16 `json:"geo_type"`
DrawGeometry json.RawMessage `json:"draw_geometry"`
Binding []byte `json:"binding"`
TimeStart pgtype.Int4 `json:"time_start"`
@@ -308,7 +370,7 @@ RETURNING id, geo_type, draw_geometry, binding, time_start, time_end,
`
type UpdateGeometryParams struct {
GeoType pgtype.Text `json:"geo_type"`
GeoType pgtype.Int2 `json:"geo_type"`
DrawGeometry []byte `json:"draw_geometry"`
Binding []byte `json:"binding"`
TimeStart pgtype.Int4 `json:"time_start"`
@@ -323,7 +385,7 @@ type UpdateGeometryParams struct {
type UpdateGeometryRow struct {
ID pgtype.UUID `json:"id"`
GeoType string `json:"geo_type"`
GeoType int16 `json:"geo_type"`
DrawGeometry json.RawMessage `json:"draw_geometry"`
Binding []byte `json:"binding"`
TimeStart pgtype.Int4 `json:"time_start"`

View File

@@ -32,7 +32,7 @@ type EntityWiki struct {
type Geometry struct {
ID pgtype.UUID `json:"id"`
GeoType string `json:"geo_type"`
GeoType int16 `json:"geo_type"`
DrawGeometry json.RawMessage `json:"draw_geometry"`
Binding []byte `json:"binding"`
TimeStart pgtype.Int4 `json:"time_start"`
@@ -55,6 +55,33 @@ type Media struct {
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
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"`
}
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 Role struct {
ID pgtype.UUID `json:"id"`
Name string `json:"name"`
@@ -63,6 +90,19 @@ type Role struct {
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
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"`
}
type User struct {
ID pgtype.UUID `json:"id"`
Email string `json:"email"`

View File

@@ -0,0 +1,556 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: project.sql
package sqlc
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
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[])
)
AND ($2::uuid[] IS NULL OR p.user_id = ANY($2::uuid[]))
AND (
$3::text IS NULL OR
p.title ILIKE '%' || $3::text || '%' OR
p.description ILIKE '%' || $3::text || '%'
)
AND ($4::timestamptz IS NULL OR p.created_at >= $4::timestamptz)
AND ($5::timestamptz IS NULL OR p.created_at <= $5::timestamptz)
`
type CountProjectsParams struct {
Statuses []string `json:"statuses"`
UserIds []pgtype.UUID `json:"user_ids"`
SearchText pgtype.Text `json:"search_text"`
CreatedFrom pgtype.Timestamptz `json:"created_from"`
CreatedTo pgtype.Timestamptz `json:"created_to"`
}
func (q *Queries) CountProjects(ctx context.Context, arg CountProjectsParams) (int64, error) {
row := q.db.QueryRow(ctx, countProjects,
arg.Statuses,
arg.UserIds,
arg.SearchText,
arg.CreatedFrom,
arg.CreatedTo,
)
var count int64
err := row.Scan(&count)
return count, err
}
const createProject = `-- name: CreateProject :one
INSERT INTO projects (
title, description, project_status, user_id
) VALUES (
$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,
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 user,
'{}'::uuid[] AS commit_ids,
'{}'::uuid[] AS submission_ids
`
type CreateProjectParams struct {
Title string `json:"title"`
Description pgtype.Text `json:"description"`
ProjectStatus int16 `json:"project_status"`
UserID pgtype.UUID `json:"user_id"`
}
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"`
}
func (q *Queries) CreateProject(ctx context.Context, arg CreateProjectParams) (CreateProjectRow, error) {
row := q.db.QueryRow(ctx, createProject,
arg.Title,
arg.Description,
arg.ProjectStatus,
arg.UserID,
)
var i CreateProjectRow
err := row.Scan(
&i.ID,
&i.Title,
&i.Description,
&i.LatestRevisionID,
&i.VersionCount,
&i.ProjectStatus,
&i.LockedBy,
&i.IsDeleted,
&i.UserID,
&i.CreatedAt,
&i.UpdatedAt,
&i.User,
&i.CommitIds,
&i.SubmissionIds,
)
return i, err
}
const deleteProject = `-- name: DeleteProject :exec
UPDATE projects
SET
is_deleted = true
WHERE id = $1
`
func (q *Queries) DeleteProject(ctx context.Context, id pgtype.UUID) error {
_, err := q.db.Exec(ctx, deleteProject, id)
return err
}
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,
COALESCE(
(SELECT array_agg(id) FROM revisions WHERE project_id = p.id),
'{}'
)::uuid[] AS commit_ids,
COALESCE(
(SELECT array_agg(id) FROM submissions WHERE project_id = p.id),
'{}'
)::uuid[] AS submission_ids,
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 user
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 = $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"`
}
func (q *Queries) GetProjectById(ctx context.Context, id pgtype.UUID) (GetProjectByIdRow, error) {
row := q.db.QueryRow(ctx, getProjectById, id)
var i GetProjectByIdRow
err := row.Scan(
&i.ID,
&i.Title,
&i.Description,
&i.LatestRevisionID,
&i.VersionCount,
&i.ProjectStatus,
&i.LockedBy,
&i.IsDeleted,
&i.UserID,
&i.CreatedAt,
&i.UpdatedAt,
&i.CommitIds,
&i.SubmissionIds,
&i.User,
)
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,
COALESCE((SELECT array_agg(id) FROM submissions WHERE project_id = p.id), '{}')::uuid[] AS submission_ids,
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 user
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 = 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"`
}
func (q *Queries) GetProjectsByIDs(ctx context.Context, dollar_1 []pgtype.UUID) ([]GetProjectsByIDsRow, error) {
rows, err := q.db.Query(ctx, getProjectsByIDs, dollar_1)
if err != nil {
return nil, err
}
defer rows.Close()
items := []GetProjectsByIDsRow{}
for rows.Next() {
var i GetProjectsByIDsRow
if err := rows.Scan(
&i.ID,
&i.Title,
&i.Description,
&i.LatestRevisionID,
&i.VersionCount,
&i.ProjectStatus,
&i.LockedBy,
&i.IsDeleted,
&i.UserID,
&i.CreatedAt,
&i.UpdatedAt,
&i.CommitIds,
&i.SubmissionIds,
&i.User,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
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,
COALESCE(
(SELECT array_agg(id) FROM revisions WHERE project_id = p.id),
'{}'
)::uuid[] AS commit_ids,
COALESCE(
(SELECT array_agg(id) FROM submissions WHERE project_id = p.id),
'{}'
)::uuid[] AS submission_ids,
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 user
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
AND p.is_deleted = false
AND ($2::uuid IS NULL OR p.id < $2::uuid)
ORDER BY p.updated_at DESC
LIMIT $3
`
type GetProjectsByUserIdParams struct {
UserID pgtype.UUID `json:"user_id"`
CursorID pgtype.UUID `json:"cursor_id"`
Limit int32 `json:"limit"`
}
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"`
}
func (q *Queries) GetProjectsByUserId(ctx context.Context, arg GetProjectsByUserIdParams) ([]GetProjectsByUserIdRow, error) {
rows, err := q.db.Query(ctx, getProjectsByUserId, arg.UserID, arg.CursorID, arg.Limit)
if err != nil {
return nil, err
}
defer rows.Close()
items := []GetProjectsByUserIdRow{}
for rows.Next() {
var i GetProjectsByUserIdRow
if err := rows.Scan(
&i.ID,
&i.Title,
&i.Description,
&i.LatestRevisionID,
&i.VersionCount,
&i.ProjectStatus,
&i.LockedBy,
&i.IsDeleted,
&i.UserID,
&i.CreatedAt,
&i.UpdatedAt,
&i.CommitIds,
&i.SubmissionIds,
&i.User,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
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,
COALESCE(
(SELECT array_agg(id) FROM revisions WHERE project_id = p.id),
'{}'
)::uuid[] AS commit_ids,
COALESCE(
(SELECT array_agg(id) FROM submissions WHERE project_id = p.id),
'{}'
)::uuid[] AS submission_ids,
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 user
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[])
)
AND ($2::uuid[] IS NULL OR p.user_id = ANY($2::uuid[]))
AND (
$3::text IS NULL OR
p.title ILIKE '%' || $3::text || '%' OR
p.description ILIKE '%' || $3::text || '%'
)
AND ($4::timestamptz IS NULL OR p.created_at >= $4::timestamptz)
AND ($5::timestamptz IS NULL OR p.created_at <= $5::timestamptz)
ORDER BY
CASE WHEN $6 = 'created_at' AND $7 = 'asc' THEN p.created_at END ASC,
CASE WHEN $6 = 'created_at' AND $7 = 'desc' THEN p.created_at END DESC,
CASE WHEN $6 = 'updated_at' AND $7 = 'asc' THEN p.updated_at END ASC,
CASE WHEN $6 = 'updated_at' AND $7 = 'desc' THEN p.updated_at END DESC,
CASE WHEN $6 = 'title' AND $7 = 'asc' THEN p.title END ASC,
CASE WHEN $6 = 'title' AND $7 = 'desc' THEN p.title END DESC,
CASE WHEN $6 IS NULL THEN p.updated_at END DESC
LIMIT $9
OFFSET $8
`
type SearchProjectsParams struct {
Statuses []string `json:"statuses"`
UserIds []pgtype.UUID `json:"user_ids"`
SearchText pgtype.Text `json:"search_text"`
CreatedFrom pgtype.Timestamptz `json:"created_from"`
CreatedTo pgtype.Timestamptz `json:"created_to"`
Sort interface{} `json:"sort"`
Order interface{} `json:"order"`
Offset int32 `json:"offset"`
Limit int32 `json:"limit"`
}
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"`
}
func (q *Queries) SearchProjects(ctx context.Context, arg SearchProjectsParams) ([]SearchProjectsRow, error) {
rows, err := q.db.Query(ctx, searchProjects,
arg.Statuses,
arg.UserIds,
arg.SearchText,
arg.CreatedFrom,
arg.CreatedTo,
arg.Sort,
arg.Order,
arg.Offset,
arg.Limit,
)
if err != nil {
return nil, err
}
defer rows.Close()
items := []SearchProjectsRow{}
for rows.Next() {
var i SearchProjectsRow
if err := rows.Scan(
&i.ID,
&i.Title,
&i.Description,
&i.LatestRevisionID,
&i.VersionCount,
&i.ProjectStatus,
&i.LockedBy,
&i.IsDeleted,
&i.UserID,
&i.CreatedAt,
&i.UpdatedAt,
&i.CommitIds,
&i.SubmissionIds,
&i.User,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
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),
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
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,
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 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
`
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"`
}
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"`
}
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.Status,
arg.LockedBy,
arg.ID,
)
var i UpdateProjectRow
err := row.Scan(
&i.ID,
&i.Title,
&i.Description,
&i.LatestRevisionID,
&i.VersionCount,
&i.ProjectStatus,
&i.LockedBy,
&i.IsDeleted,
&i.UserID,
&i.CreatedAt,
&i.UpdatedAt,
&i.User,
&i.CommitIds,
&i.SubmissionIds,
)
return i, err
}

View File

@@ -0,0 +1,182 @@
// 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

@@ -0,0 +1,493 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: submission.sql
package sqlc
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const countSubmissions = `-- name: CountSubmissions :one
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 ($3::uuid IS NULL OR s.reviewed_by = $3)
AND (
$4::text[] IS NULL
OR s.status = ANY($4::text[])
)
AND ($5::timestamptz IS NULL OR s.submitted_at >= $5::timestamptz)
AND ($6::timestamptz IS NULL OR s.submitted_at <= $6::timestamptz)
AND (
$7::text IS NULL OR
s.id::text ILIKE '%' || $7::text || '%' OR
s.review_note ILIKE '%' || $7::text || '%'
)
`
type CountSubmissionsParams struct {
ProjectID pgtype.UUID `json:"project_id"`
SubmittedBy pgtype.UUID `json:"submitted_by"`
ReviewedBy pgtype.UUID `json:"reviewed_by"`
Statuses []string `json:"statuses"`
CreatedFrom pgtype.Timestamptz `json:"created_from"`
CreatedTo pgtype.Timestamptz `json:"created_to"`
SearchText pgtype.Text `json:"search_text"`
}
func (q *Queries) CountSubmissions(ctx context.Context, arg CountSubmissionsParams) (int64, error) {
row := q.db.QueryRow(ctx, countSubmissions,
arg.ProjectID,
arg.SubmittedBy,
arg.ReviewedBy,
arg.Statuses,
arg.CreatedFrom,
arg.CreatedTo,
arg.SearchText,
)
var count int64
err := row.Scan(&count)
return count, err
}
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
)
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(
'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
`
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"`
}
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"`
}
func (q *Queries) CreateSubmission(ctx context.Context, arg CreateSubmissionParams) (CreateSubmissionRow, error) {
row := q.db.QueryRow(ctx, createSubmission,
arg.ProjectID,
arg.RevisionID,
arg.SubmittedBy,
arg.Status,
)
var i CreateSubmissionRow
err := row.Scan(
&i.ID,
&i.ProjectID,
&i.RevisionID,
&i.SubmittedBy,
&i.SubmittedAt,
&i.Status,
&i.ReviewedBy,
&i.ReviewedAt,
&i.ReviewNote,
&i.IsDeleted,
&i.Submitter,
&i.Reviewer,
)
return i, err
}
const deleteSubmission = `-- name: DeleteSubmission :exec
UPDATE submissions
SET is_deleted = true
WHERE id = $1
`
func (q *Queries) DeleteSubmission(ctx context.Context, id pgtype.UUID) error {
_, err := q.db.Exec(ctx, deleteSubmission, id)
return err
}
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,
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(
'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 submissions 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
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"`
}
func (q *Queries) GetSubmissionById(ctx context.Context, id pgtype.UUID) (GetSubmissionByIdRow, error) {
row := q.db.QueryRow(ctx, getSubmissionById, id)
var i GetSubmissionByIdRow
err := row.Scan(
&i.ID,
&i.ProjectID,
&i.RevisionID,
&i.SubmittedBy,
&i.SubmittedAt,
&i.Status,
&i.ReviewedBy,
&i.ReviewedAt,
&i.ReviewNote,
&i.IsDeleted,
&i.Submitter,
&i.Reviewer,
)
return i, err
}
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,
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(
'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 submissions 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
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"`
}
func (q *Queries) GetSubmissionsByIDs(ctx context.Context, dollar_1 []pgtype.UUID) ([]GetSubmissionsByIDsRow, error) {
rows, err := q.db.Query(ctx, getSubmissionsByIDs, dollar_1)
if err != nil {
return nil, err
}
defer rows.Close()
items := []GetSubmissionsByIDsRow{}
for rows.Next() {
var i GetSubmissionsByIDsRow
if err := rows.Scan(
&i.ID,
&i.ProjectID,
&i.RevisionID,
&i.SubmittedBy,
&i.SubmittedAt,
&i.Status,
&i.ReviewedBy,
&i.ReviewedAt,
&i.ReviewNote,
&i.IsDeleted,
&i.Submitter,
&i.Reviewer,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
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,
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(
'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 submissions 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
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 ($3::uuid IS NULL OR s.reviewed_by = $3)
AND (
$4::text[] IS NULL
OR s.status = ANY($4::text[])
)
AND ($5::timestamptz IS NULL OR s.submitted_at >= $5::timestamptz)
AND ($6::timestamptz IS NULL OR s.submitted_at <= $6::timestamptz)
AND (
$7::text IS NULL OR
s.id::text ILIKE '%' || $7::text || '%' OR
s.review_note 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 = '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
LIMIT $11
OFFSET $10
`
type SearchSubmissionsParams struct {
ProjectID pgtype.UUID `json:"project_id"`
SubmittedBy pgtype.UUID `json:"submitted_by"`
ReviewedBy pgtype.UUID `json:"reviewed_by"`
Statuses []string `json:"statuses"`
CreatedFrom pgtype.Timestamptz `json:"created_from"`
CreatedTo pgtype.Timestamptz `json:"created_to"`
SearchText pgtype.Text `json:"search_text"`
Sort interface{} `json:"sort"`
Order interface{} `json:"order"`
Offset int32 `json:"offset"`
Limit int32 `json:"limit"`
}
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"`
}
func (q *Queries) SearchSubmissions(ctx context.Context, arg SearchSubmissionsParams) ([]SearchSubmissionsRow, error) {
rows, err := q.db.Query(ctx, searchSubmissions,
arg.ProjectID,
arg.SubmittedBy,
arg.ReviewedBy,
arg.Statuses,
arg.CreatedFrom,
arg.CreatedTo,
arg.SearchText,
arg.Sort,
arg.Order,
arg.Offset,
arg.Limit,
)
if err != nil {
return nil, err
}
defer rows.Close()
items := []SearchSubmissionsRow{}
for rows.Next() {
var i SearchSubmissionsRow
if err := rows.Scan(
&i.ID,
&i.ProjectID,
&i.RevisionID,
&i.SubmittedBy,
&i.SubmittedAt,
&i.Status,
&i.ReviewedBy,
&i.ReviewedAt,
&i.ReviewNote,
&i.IsDeleted,
&i.Submitter,
&i.Reviewer,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const updateSubmission = `-- name: UpdateSubmission :one
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)
FROM submissions 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
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,
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(
'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
`
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"`
}
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"`
}
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.ID,
)
var i UpdateSubmissionRow
err := row.Scan(
&i.ID,
&i.ProjectID,
&i.RevisionID,
&i.SubmittedBy,
&i.SubmittedAt,
&i.Status,
&i.ReviewedBy,
&i.ReviewedAt,
&i.ReviewNote,
&i.IsDeleted,
&i.Submitter,
&i.Reviewer,
)
return i, err
}

View File

@@ -341,6 +341,84 @@ func (q *Queries) GetUserByIDWithoutDeleted(ctx context.Context, id pgtype.UUID)
return i, err
}
const getUsersByIDs = `-- name: GetUsersByIDs :many
SELECT
u.id,
u.email,
u.password_hash,
u.token_version,
u.is_deleted,
u.created_at,
u.updated_at,
(
SELECT json_build_object(
'display_name', p.display_name,
'full_name', p.full_name,
'avatar_url', p.avatar_url,
'bio', p.bio,
'location', p.location,
'website', p.website,
'country_code', p.country_code,
'phone', p.phone
)
FROM user_profiles p
WHERE p.user_id = u.id
) AS profile,
(
SELECT COALESCE(
json_agg(json_build_object('id', r.id, 'name', r.name)),
'[]'
)::json
FROM user_roles ur
JOIN roles r ON ur.role_id = r.id
WHERE ur.user_id = u.id
) AS roles
FROM users u
WHERE u.id = ANY($1::uuid[]) AND u.is_deleted = false
`
type GetUsersByIDsRow struct {
ID pgtype.UUID `json:"id"`
Email string `json:"email"`
PasswordHash pgtype.Text `json:"password_hash"`
TokenVersion int32 `json:"token_version"`
IsDeleted bool `json:"is_deleted"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
Profile json.RawMessage `json:"profile"`
Roles []byte `json:"roles"`
}
func (q *Queries) GetUsersByIDs(ctx context.Context, dollar_1 []pgtype.UUID) ([]GetUsersByIDsRow, error) {
rows, err := q.db.Query(ctx, getUsersByIDs, dollar_1)
if err != nil {
return nil, err
}
defer rows.Close()
items := []GetUsersByIDsRow{}
for rows.Next() {
var i GetUsersByIDsRow
if err := rows.Scan(
&i.ID,
&i.Email,
&i.PasswordHash,
&i.TokenVersion,
&i.IsDeleted,
&i.CreatedAt,
&i.UpdatedAt,
&i.Profile,
&i.Roles,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const restoreUser = `-- name: RestoreUser :exec
UPDATE users
SET

View File

@@ -111,7 +111,7 @@ SELECT
'full_name', up.full_name,
'avatar_url', up.avatar_url
)::json AS user,
NULL::json AS reviewer, -- Khi mới tạo thì reviewer luôn null
NULL::json AS reviewer,
'[]'::json AS medias
FROM inserted_uv i
JOIN users u ON i.user_id = u.id
@@ -387,6 +387,101 @@ func (q *Queries) GetUserVerifications(ctx context.Context, userID pgtype.UUID)
return items, nil
}
const getUserVerificationsByIDs = `-- name: GetUserVerificationsByIDs :many
SELECT
uv.id, uv.verify_type, uv.content,
uv.is_deleted, uv.status, uv.review_note,
uv.reviewed_at, uv.created_at,
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 user,
CASE WHEN uv.reviewed_by IS NOT NULL THEN
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,
(
SELECT COALESCE(
json_agg(
json_build_object(
'id', m.id,
'storage_key', m.storage_key,
'original_name', m.original_name,
'mime_type', m.mime_type,
'size', m.size,
'file_metadata', m.file_metadata,
'created_at', m.created_at
)
),
'[]'
)::json
FROM verification_medias vm
JOIN medias m ON vm.media_id = m.id
WHERE vm.verification_id = uv.id
) AS medias
FROM user_verifications uv
JOIN users u ON uv.user_id = u.id
LEFT JOIN user_profiles up ON u.id = up.user_id
LEFT JOIN users ru ON uv.reviewed_by = ru.id
LEFT JOIN user_profiles rup ON ru.id = rup.user_id
WHERE uv.id = ANY($1::uuid[])
AND uv.is_deleted = false
`
type GetUserVerificationsByIDsRow struct {
ID pgtype.UUID `json:"id"`
VerifyType int16 `json:"verify_type"`
Content pgtype.Text `json:"content"`
IsDeleted bool `json:"is_deleted"`
Status int16 `json:"status"`
ReviewNote pgtype.Text `json:"review_note"`
ReviewedAt pgtype.Timestamptz `json:"reviewed_at"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
User []byte `json:"user"`
Reviewer []byte `json:"reviewer"`
Medias []byte `json:"medias"`
}
func (q *Queries) GetUserVerificationsByIDs(ctx context.Context, dollar_1 []pgtype.UUID) ([]GetUserVerificationsByIDsRow, error) {
rows, err := q.db.Query(ctx, getUserVerificationsByIDs, dollar_1)
if err != nil {
return nil, err
}
defer rows.Close()
items := []GetUserVerificationsByIDsRow{}
for rows.Next() {
var i GetUserVerificationsByIDsRow
if err := rows.Scan(
&i.ID,
&i.VerifyType,
&i.Content,
&i.IsDeleted,
&i.Status,
&i.ReviewNote,
&i.ReviewedAt,
&i.CreatedAt,
&i.User,
&i.Reviewer,
&i.Medias,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const searchUserVerifications = `-- name: SearchUserVerifications :many
SELECT
uv.id,

View File

@@ -114,6 +114,37 @@ func (q *Queries) GetWikiById(ctx context.Context, id pgtype.UUID) (Wiki, error)
return i, err
}
const getWikisByIDs = `-- name: GetWikisByIDs :many
SELECT id, title, content, is_deleted, created_at, updated_at FROM wikis WHERE id = ANY($1::uuid[]) AND is_deleted = false
`
func (q *Queries) GetWikisByIDs(ctx context.Context, dollar_1 []pgtype.UUID) ([]Wiki, error) {
rows, err := q.db.Query(ctx, getWikisByIDs, dollar_1)
if err != nil {
return nil, err
}
defer rows.Close()
items := []Wiki{}
for rows.Next() {
var i Wiki
if err := rows.Scan(
&i.ID,
&i.Title,
&i.Content,
&i.IsDeleted,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const searchWikis = `-- name: SearchWikis :many
SELECT w.id, w.title, w.content, w.is_deleted, w.created_at, w.updated_at
FROM wikis w

View File

@@ -68,7 +68,7 @@ func jwtSuccess(userRepo repositories.UserRepository) fiber.Handler {
return unauthorized()
}
if slices.Contains(claims.Roles, constants.BANNED) {
if slices.Contains(claims.Roles, constants.RoleTypeBanned) {
return c.Status(fiber.StatusForbidden).JSON(response.CommonResponse{
Status: false,
Message: "User account is banned",
@@ -119,7 +119,7 @@ func jwtSuccessRefresh() fiber.Handler {
return unauthorized()
}
if slices.Contains(claims.Roles, constants.BANNED) {
if slices.Contains(claims.Roles, constants.RoleTypeBanned) {
return c.Status(fiber.StatusForbidden).JSON(response.CommonResponse{
Status: false,
Message: "User account is banned",

View File

@@ -8,7 +8,7 @@ import (
"github.com/gofiber/fiber/v3"
)
func getRoles(c fiber.Ctx) ([]constants.Role, error) {
func getRoles(c fiber.Ctx) ([]constants.RoleType, error) {
claimsVal := c.Locals("user_claims")
if claimsVal == nil {
return nil, fiber.ErrUnauthorized
@@ -22,7 +22,7 @@ func getRoles(c fiber.Ctx) ([]constants.Role, error) {
return claims.Roles, nil
}
func RequireAnyRole(required ...constants.Role) fiber.Handler {
func RequireAnyRole(required ...constants.RoleType) fiber.Handler {
return func(c fiber.Ctx) error {
userRoles, err := getRoles(c)
if err != nil {
@@ -43,7 +43,7 @@ func RequireAnyRole(required ...constants.Role) fiber.Handler {
}
}
func RequireAllRoles(required ...constants.Role) fiber.Handler {
func RequireAllRoles(required ...constants.RoleType) fiber.Handler {
return func(c fiber.Ctx) error {
userRoles, err := getRoles(c)
if err != nil {
@@ -61,7 +61,7 @@ func RequireAllRoles(required ...constants.Role) fiber.Handler {
}
}
func ForbidRoles(forbidden ...constants.Role) fiber.Handler {
func ForbidRoles(forbidden ...constants.RoleType) fiber.Handler {
return func(c fiber.Ctx) error {
userRoles, err := getRoles(c)
if err != nil {

View File

@@ -3,20 +3,21 @@ package models
import (
"encoding/json"
"history-api/internal/dtos/response"
"history-api/pkg/constants"
"time"
)
type GeometryEntity struct {
ID string `json:"id"`
GeoType string `json:"geo_type"`
DrawGeometry json.RawMessage `json:"draw_geometry"`
Binding json.RawMessage `json:"binding"`
TimeStart int32 `json:"time_start"`
TimeEnd int32 `json:"time_end"`
Bbox *response.Bbox `json:"bbox"`
IsDeleted bool `json:"is_deleted"`
CreatedAt *time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at"`
ID string `json:"id"`
GeoType constants.GeoType `json:"geo_type"`
DrawGeometry json.RawMessage `json:"draw_geometry"`
Binding json.RawMessage `json:"binding"`
TimeStart int32 `json:"time_start"`
TimeEnd int32 `json:"time_end"`
Bbox *response.Bbox `json:"bbox"`
IsDeleted bool `json:"is_deleted"`
CreatedAt *time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at"`
}
func (g *GeometryEntity) ToResponse() *response.GeometryResponse {
@@ -25,7 +26,7 @@ func (g *GeometryEntity) ToResponse() *response.GeometryResponse {
}
return &response.GeometryResponse{
ID: g.ID,
GeoType: g.GeoType,
GeoType: g.GeoType.String(),
DrawGeometry: g.DrawGeometry,
Binding: g.Binding,
TimeStart: g.TimeStart,

View File

@@ -0,0 +1,74 @@
package models
import (
"encoding/json"
"history-api/internal/dtos/response"
"history-api/pkg/constants"
"time"
)
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"`
}
func (p *ProjectEntity) ParseUser(data []byte) error {
if len(data) == 0 || string(data) == "null" {
p.User = nil
return nil
}
return json.Unmarshal(data, &p.User)
}
func (p *ProjectEntity) ToResponse() *response.ProjectResponse {
if p == nil {
return nil
}
var userResponse *response.UserSimpleResponse
if p.User != nil {
userResponse = p.User.ToResponse()
}
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,
}
}
func ProjectsEntityToResponse(projects []*ProjectEntity) []*response.ProjectResponse {
out := make([]*response.ProjectResponse, 0)
if projects == nil {
return out
}
for _, project := range projects {
if project == nil {
continue
}
out = append(out, project.ToResponse())
}
return out
}

View File

@@ -74,8 +74,8 @@ func RolesEntityToResponse(rs []*RoleEntity) []*response.RoleResponse {
return out
}
func RolesEntityToRoleConstant(rs []*RoleSimple) []constants.Role {
out := make([]constants.Role, 0)
func RolesEntityToRoleConstant(rs []*RoleSimple) []constants.RoleType {
out := make([]constants.RoleType, 0)
if rs == nil {
return out
}

View File

@@ -55,22 +55,46 @@ func (r *entityRepository) getByIDsWithFallback(ctx context.Context, ids []strin
var entities []*models.EntityEntity
missingToCache := make(map[string]any)
var missingPgIds []pgtype.UUID
for i, b := range raws {
if len(b) > 0 {
var e models.EntityEntity
if err := json.Unmarshal(b, &e); err == nil {
entities = append(entities, &e)
}
} else {
if len(b) == 0 {
pgId := pgtype.UUID{}
err := pgId.Scan(ids[i])
if err != nil {
continue
if err == nil {
missingPgIds = append(missingPgIds, pgId)
}
dbEntity, err := r.GetByID(ctx, pgId)
if err == nil && dbEntity != nil {
entities = append(entities, dbEntity)
missingToCache[keys[i]] = dbEntity
}
}
dbMap := make(map[string]*models.EntityEntity)
if len(missingPgIds) > 0 {
dbRows, err := r.q.GetEntitiesByIDs(ctx, missingPgIds)
if err == nil {
for _, row := range dbRows {
item := models.EntityEntity{
ID: convert.UUIDToString(row.ID),
Name: row.Name,
Description: convert.TextToString(row.Description),
ThumbnailUrl: convert.TextToString(row.ThumbnailUrl),
IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
}
dbMap[item.ID] = &item
}
}
}
for i, b := range raws {
if len(b) > 0 {
var u models.EntityEntity
if err := json.Unmarshal(b, &u); err == nil {
entities = append(entities, &u)
}
} else {
if item, ok := dbMap[ids[i]]; ok {
entities = append(entities, item)
missingToCache[keys[i]] = item
}
}
}

View File

@@ -58,22 +58,54 @@ func (r *geometryRepository) getByIDsWithFallback(ctx context.Context, ids []str
var geometries []*models.GeometryEntity
missingToCache := make(map[string]any)
var missingPgIds []pgtype.UUID
for i, b := range raws {
if len(b) > 0 {
var g models.GeometryEntity
if err := json.Unmarshal(b, &g); err == nil {
geometries = append(geometries, &g)
}
} else {
if len(b) == 0 {
pgId := pgtype.UUID{}
err := pgId.Scan(ids[i])
if err != nil {
continue
if err == nil {
missingPgIds = append(missingPgIds, pgId)
}
dbGeometry, err := r.GetByID(ctx, pgId)
if err == nil && dbGeometry != nil {
geometries = append(geometries, dbGeometry)
missingToCache[keys[i]] = dbGeometry
}
}
dbMap := make(map[string]*models.GeometryEntity)
if len(missingPgIds) > 0 {
dbRows, err := r.q.GetGeometriesByIDs(ctx, missingPgIds)
if err == nil {
for _, row := range dbRows {
item := models.GeometryEntity{
ID: convert.UUIDToString(row.ID),
GeoType: constants.ParseGeoType(row.GeoType),
DrawGeometry: row.DrawGeometry,
Binding: row.Binding,
TimeStart: convert.Int4ToInt32(row.TimeStart),
TimeEnd: convert.Int4ToInt32(row.TimeEnd),
Bbox: &response.Bbox{
MinLng: row.MinLng,
MinLat: row.MinLat,
MaxLng: row.MaxLng,
MaxLat: row.MaxLat,
},
IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
}
dbMap[item.ID] = &item
}
}
}
for i, b := range raws {
if len(b) > 0 {
var u models.GeometryEntity
if err := json.Unmarshal(b, &u); err == nil {
geometries = append(geometries, &u)
}
} else {
if item, ok := dbMap[ids[i]]; ok {
geometries = append(geometries, item)
missingToCache[keys[i]] = item
}
}
}
@@ -105,7 +137,7 @@ func (r *geometryRepository) GetByID(ctx context.Context, id pgtype.UUID) (*mode
geometry = models.GeometryEntity{
ID: convert.UUIDToString(row.ID),
GeoType: row.GeoType,
GeoType: constants.ParseGeoType(row.GeoType),
DrawGeometry: row.DrawGeometry,
Binding: row.Binding,
TimeStart: convert.Int4ToInt32(row.TimeStart),
@@ -143,7 +175,7 @@ func (r *geometryRepository) Search(ctx context.Context, params sqlc.SearchGeome
for _, row := range rows {
geometry := &models.GeometryEntity{
ID: convert.UUIDToString(row.ID),
GeoType: row.GeoType,
GeoType: constants.ParseGeoType(row.GeoType),
DrawGeometry: row.DrawGeometry,
Binding: row.Binding,
TimeStart: convert.Int4ToInt32(row.TimeStart),
@@ -181,7 +213,7 @@ func (r *geometryRepository) Create(ctx context.Context, params sqlc.CreateGeome
geometry := models.GeometryEntity{
ID: convert.UUIDToString(row.ID),
GeoType: row.GeoType,
GeoType: constants.ParseGeoType(row.GeoType),
DrawGeometry: row.DrawGeometry,
Binding: row.Binding,
TimeStart: convert.Int4ToInt32(row.TimeStart),
@@ -212,7 +244,7 @@ func (r *geometryRepository) Update(ctx context.Context, params sqlc.UpdateGeome
}
geometry := models.GeometryEntity{
ID: convert.UUIDToString(row.ID),
GeoType: row.GeoType,
GeoType: constants.ParseGeoType(row.GeoType),
DrawGeometry: row.DrawGeometry,
Binding: row.Binding,
TimeStart: convert.Int4ToInt32(row.TimeStart),

View File

@@ -56,22 +56,48 @@ func (r *mediaRepository) getByIDsWithFallback(ctx context.Context, ids []string
var medias []*models.MediaEntity
missingMediasToCache := make(map[string]any)
var missingPgIds []pgtype.UUID
for i, b := range raws {
if len(b) > 0 {
var m models.MediaEntity
if err := json.Unmarshal(b, &m); err == nil {
medias = append(medias, &m)
}
} else {
if len(b) == 0 {
pgId := pgtype.UUID{}
err := pgId.Scan(ids[i])
if err != nil {
continue
if err == nil {
missingPgIds = append(missingPgIds, pgId)
}
dbMedia, err := r.GetByID(ctx, pgId)
if err == nil && dbMedia != nil {
medias = append(medias, dbMedia)
missingMediasToCache[keys[i]] = dbMedia
}
}
dbMap := make(map[string]*models.MediaEntity)
if len(missingPgIds) > 0 {
dbRows, err := r.q.GetMediaByIDs(ctx, missingPgIds)
if err == nil {
for _, row := range dbRows {
item := models.MediaEntity{
ID: convert.UUIDToString(row.ID),
UserID: convert.UUIDToString(row.UserID),
StorageKey: row.StorageKey,
OriginalName: row.OriginalName,
MimeType: row.MimeType,
Size: row.Size,
FileMetadata: row.FileMetadata,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
}
dbMap[item.ID] = &item
}
}
}
for i, b := range raws {
if len(b) > 0 {
var u models.MediaEntity
if err := json.Unmarshal(b, &u); err == nil {
medias = append(medias, &u)
}
} else {
if item, ok := dbMap[ids[i]]; ok {
medias = append(medias, item)
missingMediasToCache[keys[i]] = item
}
}
}

View File

@@ -0,0 +1,345 @@
package repositories
import (
"context"
"crypto/md5"
"encoding/json"
"fmt"
"github.com/jackc/pgx/v5/pgtype"
"history-api/internal/gen/sqlc"
"history-api/internal/models"
"history-api/pkg/cache"
"history-api/pkg/constants"
"history-api/pkg/convert"
)
type ProjectRepository interface {
GetByID(ctx context.Context, id pgtype.UUID) (*models.ProjectEntity, error)
GetByIDs(ctx context.Context, ids []string) ([]*models.ProjectEntity, error)
GetByUserID(ctx context.Context, params sqlc.GetProjectsByUserIdParams) ([]*models.ProjectEntity, error)
Search(ctx context.Context, params sqlc.SearchProjectsParams) ([]*models.ProjectEntity, error)
Count(ctx context.Context, params sqlc.CountProjectsParams) (int64, error)
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
}
type projectRepository struct {
q *sqlc.Queries
c cache.Cache
}
func NewProjectRepository(db sqlc.DBTX, c cache.Cache) ProjectRepository {
return &projectRepository{
q: sqlc.New(db),
c: 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
}
keys := make([]string, len(ids))
for i, id := range ids {
keys[i] = fmt.Sprintf("project:id:%s", id)
}
raws := r.c.MGet(ctx, keys...)
var projects []*models.ProjectEntity
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.ProjectEntity)
if len(missingPgIds) > 0 {
dbRows, err := r.q.GetProjectsByIDs(ctx, missingPgIds)
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),
}
_ = item.ParseUser(row.User)
dbMap[item.ID] = &item
}
}
}
for i, b := range raws {
if len(b) > 0 {
var p models.ProjectEntity
if err := json.Unmarshal(b, &p); err == nil {
projects = append(projects, &p)
}
} else {
if item, ok := dbMap[ids[i]]; ok {
projects = append(projects, item)
missingToCache[keys[i]] = item
}
}
}
if len(missingToCache) > 0 {
_ = r.c.MSet(ctx, missingToCache, constants.NormalCacheDuration)
}
return projects, nil
}
func (r *projectRepository) GetByIDs(ctx context.Context, ids []string) ([]*models.ProjectEntity, error) {
return r.getByIDsWithFallback(ctx, ids)
}
func (r *projectRepository) GetByID(ctx context.Context, id pgtype.UUID) (*models.ProjectEntity, error) {
cacheId := fmt.Sprintf("project:id:%s", convert.UUIDToString(id))
var project models.ProjectEntity
err := r.c.Get(ctx, cacheId, &project)
if err == nil {
_ = r.c.Set(ctx, cacheId, project, constants.NormalCacheDuration)
return &project, nil
}
row, err := r.q.GetProjectById(ctx, id)
if err != nil {
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),
}
_ = project.ParseUser(row.User)
_ = r.c.Set(ctx, cacheId, project, constants.NormalCacheDuration)
return &project, nil
}
func (r *projectRepository) GetByUserID(ctx context.Context, params sqlc.GetProjectsByUserIdParams) ([]*models.ProjectEntity, error) {
queryKey := r.generateQueryKey("project:user", 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.GetProjectsByUserId(ctx, params)
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),
}
_ = project.ParseUser(row.User)
ids = append(ids, project.ID)
projects = append(projects, project)
projectToCache[fmt.Sprintf("project:id:%s", project.ID)] = project
}
if len(projectToCache) > 0 {
_ = r.c.MSet(ctx, projectToCache, constants.NormalCacheDuration)
}
if len(ids) > 0 {
_ = r.c.Set(ctx, queryKey, ids, constants.ListCacheDuration)
}
return projects, nil
}
func (r *projectRepository) Search(ctx context.Context, params sqlc.SearchProjectsParams) ([]*models.ProjectEntity, error) {
queryKey := r.generateQueryKey("project: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.SearchProjects(ctx, params)
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),
}
_ = project.ParseUser(row.User)
ids = append(ids, project.ID)
projects = append(projects, project)
projectToCache[fmt.Sprintf("project:id:%s", project.ID)] = project
}
if len(projectToCache) > 0 {
_ = r.c.MSet(ctx, projectToCache, constants.NormalCacheDuration)
}
if len(ids) > 0 {
_ = r.c.Set(ctx, queryKey, ids, constants.ListCacheDuration)
}
return projects, nil
}
func (r *projectRepository) Count(ctx context.Context, params sqlc.CountProjectsParams) (int64, error) {
queryKey := r.generateQueryKey("project:count", params)
var count int64
if err := r.c.Get(ctx, queryKey, &count); err == nil {
return count, nil
}
count, err := r.q.CountProjects(ctx, params)
if err != nil {
return 0, err
}
_ = r.c.Set(ctx, queryKey, count, constants.NormalCacheDuration)
return count, nil
}
func (r *projectRepository) Create(ctx context.Context, params sqlc.CreateProjectParams) (*models.ProjectEntity, error) {
row, err := r.q.CreateProject(ctx, params)
if err != nil {
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),
}
_ = project.ParseUser(row.User)
_ = r.c.Set(ctx, fmt.Sprintf("project:id:%s", project.ID), project, constants.NormalCacheDuration)
go func() {
bgCtx := context.Background()
_ = r.c.DelByPattern(bgCtx, "project:search*")
_ = r.c.DelByPattern(bgCtx, "project:user*")
_ = r.c.DelByPattern(bgCtx, "project:count*")
}()
return &project, nil
}
func (r *projectRepository) Update(ctx context.Context, params sqlc.UpdateProjectParams) (*models.ProjectEntity, error) {
row, err := r.q.UpdateProject(ctx, params)
if err != nil {
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),
}
_ = project.ParseUser(row.User)
_ = r.c.Set(ctx, fmt.Sprintf("project:id:%s", project.ID), project, constants.NormalCacheDuration)
return &project, nil
}
func (r *projectRepository) Delete(ctx context.Context, id pgtype.UUID) error {
err := r.q.DeleteProject(ctx, id)
if err != nil {
return err
}
_ = 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
}

View File

@@ -61,6 +61,34 @@ func (r *roleRepository) getByIDsWithFallback(ctx context.Context, ids []string)
var roles []*models.RoleEntity
missingRolesToCache := 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.RoleEntity)
if len(missingPgIds) > 0 {
dbRows, err := r.q.GetRolesByIDs(ctx, missingPgIds)
if err == nil {
for _, row := range dbRows {
item := models.RoleEntity{
ID: convert.UUIDToString(row.ID),
Name: row.Name,
IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
}
dbMap[item.ID] = &item
}
}
}
for i, b := range raws {
if len(b) > 0 {
var u models.RoleEntity
@@ -68,15 +96,9 @@ func (r *roleRepository) getByIDsWithFallback(ctx context.Context, ids []string)
roles = append(roles, &u)
}
} else {
pgId := pgtype.UUID{}
err := pgId.Scan(ids[i])
if err != nil {
continue
}
dbRole, err := r.GetByID(ctx, pgId)
if err == nil && dbRole != nil {
roles = append(roles, dbRole)
missingRolesToCache[keys[i]] = dbRole
if item, ok := dbMap[ids[i]]; ok {
roles = append(roles, item)
missingRolesToCache[keys[i]] = item
}
}
}

View File

@@ -49,7 +49,7 @@ func (t *tokenRepository) CheckVerified(ctx context.Context, email string, token
}
func (t *tokenRepository) CreateUploadToken(ctx context.Context, userId string, token *models.TokenUploadEntity) error {
cacheKey := fmt.Sprintf("token:%d:%s:%s", constants.TokenUpload.Value(), userId, token.ID)
cacheKey := fmt.Sprintf("token:%d:%s:%s", constants.TokenTypeUpload.Value(), userId, token.ID)
err := t.c.Set(ctx, cacheKey, token, constants.TokenUploadDuration)
if err != nil {
return err
@@ -58,7 +58,7 @@ func (t *tokenRepository) CreateUploadToken(ctx context.Context, userId string,
}
func (t *tokenRepository) GetUploadToken(ctx context.Context, userId string, id string) (*models.TokenUploadEntity, error) {
cacheKey := fmt.Sprintf("token:%d:%s:%s", constants.TokenUpload.Value(), userId, id)
cacheKey := fmt.Sprintf("token:%d:%s:%s", constants.TokenTypeUpload.Value(), userId, id)
var token models.TokenUploadEntity
err := t.c.Get(ctx, cacheKey, &token)
if err != nil {
@@ -68,7 +68,7 @@ func (t *tokenRepository) GetUploadToken(ctx context.Context, userId string, id
}
func (t *tokenRepository) DeleteUploadToken(ctx context.Context, userId string, id string) error {
cacheKey := fmt.Sprintf("token:%d:%s:%s", constants.TokenUpload.Value(), userId, id)
cacheKey := fmt.Sprintf("token:%d:%s:%s", constants.TokenTypeUpload.Value(), userId, id)
return t.c.Del(ctx, cacheKey)
}

View File

@@ -63,6 +63,38 @@ func (r *userRepository) getByIDsWithFallback(ctx context.Context, ids []string)
var users []*models.UserEntity
missingUsersToCache := 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.UserEntity)
if len(missingPgIds) > 0 {
dbRows, err := r.q.GetUsersByIDs(ctx, missingPgIds)
if err == nil {
for _, row := range dbRows {
item := models.UserEntity{
ID: convert.UUIDToString(row.ID),
Email: row.Email,
PasswordHash: convert.TextToString(row.PasswordHash),
TokenVersion: row.TokenVersion,
IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
}
_ = item.ParseRoles(row.Roles)
_ = item.ParseProfile(row.Profile)
dbMap[item.ID] = &item
}
}
}
for i, b := range raws {
if len(b) > 0 {
var u models.UserEntity
@@ -70,15 +102,9 @@ func (r *userRepository) getByIDsWithFallback(ctx context.Context, ids []string)
users = append(users, &u)
}
} else {
pgId := pgtype.UUID{}
err := pgId.Scan(ids[i])
if err != nil {
continue
}
dbUser, err := r.GetByID(ctx, pgId)
if err == nil && dbUser != nil {
users = append(users, dbUser)
missingUsersToCache[keys[i]] = dbUser
if item, ok := dbMap[ids[i]]; ok {
users = append(users, item)
missingUsersToCache[keys[i]] = item
}
}
}

View File

@@ -99,6 +99,40 @@ func (v *verificationRepository) getByIDsWithFallback(ctx context.Context, ids [
var verification []*models.UserVerificationEntity
missingVerificationToCache := 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.UserVerificationEntity)
if len(missingPgIds) > 0 {
dbRows, err := v.q.GetUserVerificationsByIDs(ctx, missingPgIds)
if err == nil {
for _, row := range dbRows {
item := models.UserVerificationEntity{
ID: convert.UUIDToString(row.ID),
VerifyType: constants.ParseVerifyType(row.VerifyType),
Content: convert.TextToString(row.Content),
IsDeleted: row.IsDeleted,
Status: constants.ParseStatusType(row.Status),
ReviewNote: convert.TextToString(row.ReviewNote),
ReviewedAt: convert.TimeToPtr(row.ReviewedAt),
CreatedAt: convert.TimeToPtr(row.CreatedAt),
}
_ = item.ParseMedia(row.Medias)
_ = item.ParseUser(row.User)
_ = item.ParseReviewer(row.Reviewer)
dbMap[item.ID] = &item
}
}
}
for i, b := range raws {
if len(b) > 0 {
var u models.UserVerificationEntity
@@ -106,15 +140,9 @@ func (v *verificationRepository) getByIDsWithFallback(ctx context.Context, ids [
verification = append(verification, &u)
}
} else {
pgId := pgtype.UUID{}
err := pgId.Scan(ids[i])
if err != nil {
continue
}
dbUser, err := v.GetByID(ctx, pgId)
if err == nil && dbUser != nil {
verification = append(verification, dbUser)
missingVerificationToCache[keys[i]] = dbUser
if item, ok := dbMap[ids[i]]; ok {
verification = append(verification, item)
missingVerificationToCache[keys[i]] = item
}
}
}

View File

@@ -57,22 +57,45 @@ func (r *wikiRepository) getByIDsWithFallback(ctx context.Context, ids []string)
var wikis []*models.WikiEntity
missingToCache := make(map[string]any)
var missingPgIds []pgtype.UUID
for i, b := range raws {
if len(b) > 0 {
var w models.WikiEntity
if err := json.Unmarshal(b, &w); err == nil {
wikis = append(wikis, &w)
}
} else {
if len(b) == 0 {
pgId := pgtype.UUID{}
err := pgId.Scan(ids[i])
if err != nil {
continue
if err == nil {
missingPgIds = append(missingPgIds, pgId)
}
dbWiki, err := r.GetByID(ctx, pgId)
if err == nil && dbWiki != nil {
wikis = append(wikis, dbWiki)
missingToCache[keys[i]] = dbWiki
}
}
dbMap := make(map[string]*models.WikiEntity)
if len(missingPgIds) > 0 {
dbRows, err := r.q.GetWikisByIDs(ctx, missingPgIds)
if err == nil {
for _, row := range dbRows {
item := models.WikiEntity{
ID: convert.UUIDToString(row.ID),
Title: convert.TextToString(row.Title),
Content: convert.TextToString(row.Content),
IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
}
dbMap[item.ID] = &item
}
}
}
for i, b := range raws {
if len(b) > 0 {
var u models.WikiEntity
if err := json.Unmarshal(b, &u); err == nil {
wikis = append(wikis, &u)
}
} else {
if item, ok := dbMap[ids[i]]; ok {
wikis = append(wikis, item)
missingToCache[keys[i]] = item
}
}
}

View File

@@ -15,7 +15,7 @@ func MediaRoutes(app *fiber.App, controller *controllers.MediaController, userRe
route.Post(
"/upload",
middlewares.JwtAccess(userRepo),
middlewares.RequireAnyRole(constants.ADMIN, constants.MOD),
middlewares.RequireAnyRole(constants.RoleTypeAdmin, constants.RoleTypeMod),
controller.UploadServerSide,
)
@@ -34,7 +34,7 @@ func MediaRoutes(app *fiber.App, controller *controllers.MediaController, userRe
route.Get(
"/:id",
middlewares.JwtAccess(userRepo),
middlewares.RequireAnyRole(constants.ADMIN, constants.MOD),
middlewares.RequireAnyRole(constants.RoleTypeAdmin, constants.RoleTypeMod),
controller.GetMediaByID,
)
@@ -47,7 +47,7 @@ func MediaRoutes(app *fiber.App, controller *controllers.MediaController, userRe
route.Get(
"/",
middlewares.JwtAccess(userRepo),
middlewares.RequireAnyRole(constants.ADMIN, constants.MOD),
middlewares.RequireAnyRole(constants.RoleTypeAdmin, constants.RoleTypeMod),
controller.SearchMedia,
)
route.Delete(

View File

@@ -0,0 +1,41 @@
package routes
import (
"history-api/internal/controllers"
"history-api/internal/middlewares"
"history-api/internal/repositories"
"github.com/gofiber/fiber/v3"
)
func ProjectRoutes(app *fiber.App, controller *controllers.ProjectController, userRepo repositories.UserRepository) {
route := app.Group("/projects")
route.Get(
"/:id",
controller.GetProjectByID,
)
route.Put(
"/:id",
middlewares.JwtAccess(userRepo),
controller.UpdateProject,
)
route.Delete(
"/:id",
middlewares.JwtAccess(userRepo),
controller.DeleteProject,
)
route.Get(
"/",
controller.SearchProject,
)
route.Post(
"/",
middlewares.JwtAccess(userRepo),
controller.CreateProject,
)
}

View File

@@ -36,6 +36,12 @@ func UserRoutes(app *fiber.App, controller *controllers.UserController, userRepo
controller.GetUserApplication,
)
route.Get(
"/current/project",
middlewares.JwtAccess(userRepo),
controller.GetUserProject,
)
route.Patch(
"/current/password",
middlewares.JwtAccess(userRepo),
@@ -45,49 +51,56 @@ func UserRoutes(app *fiber.App, controller *controllers.UserController, userRepo
route.Get(
"/:id",
middlewares.JwtAccess(userRepo),
middlewares.RequireAnyRole(constants.ADMIN, constants.MOD),
middlewares.RequireAnyRole(constants.RoleTypeAdmin, constants.RoleTypeMod),
controller.GetUserById,
)
route.Delete(
"/:id",
middlewares.JwtAccess(userRepo),
middlewares.RequireAnyRole(constants.ADMIN, constants.MOD),
middlewares.RequireAnyRole(constants.RoleTypeAdmin, constants.RoleTypeMod),
controller.DeleteUser,
)
route.Get(
"/:id/media",
middlewares.JwtAccess(userRepo),
middlewares.RequireAnyRole(constants.ADMIN, constants.MOD),
middlewares.RequireAnyRole(constants.RoleTypeAdmin, constants.RoleTypeMod),
controller.GetMediaByUserID,
)
route.Get(
"/:id/application",
middlewares.JwtAccess(userRepo),
middlewares.RequireAnyRole(constants.ADMIN, constants.MOD),
middlewares.RequireAnyRole(constants.RoleTypeAdmin, constants.RoleTypeMod),
controller.GetVerificationByUserID,
)
route.Get(
"/:id/project",
middlewares.JwtAccess(userRepo),
middlewares.RequireAnyRole(constants.RoleTypeAdmin, constants.RoleTypeMod),
controller.GetProjectByUserID,
)
route.Patch(
"/:id/restore",
middlewares.JwtAccess(userRepo),
middlewares.RequireAnyRole(constants.ADMIN, constants.MOD),
middlewares.RequireAnyRole(constants.RoleTypeAdmin, constants.RoleTypeMod),
controller.RestoreUser,
)
route.Patch(
"/:id/role",
middlewares.JwtAccess(userRepo),
middlewares.RequireAnyRole(constants.ADMIN, constants.MOD),
middlewares.RequireAnyRole(constants.RoleTypeAdmin, constants.RoleTypeMod),
controller.ChangeRoleUser,
)
route.Get(
"/",
middlewares.JwtAccess(userRepo),
middlewares.RequireAnyRole(constants.ADMIN, constants.MOD),
middlewares.RequireAnyRole(constants.RoleTypeAdmin, constants.RoleTypeMod),
controller.SearchUser,
)

View File

@@ -15,7 +15,7 @@ func VerificationRoutes(app *fiber.App, controller *controllers.VerificationCont
route.Get(
"/:id",
middlewares.JwtAccess(userRepo),
middlewares.RequireAnyRole(constants.ADMIN, constants.MOD),
middlewares.RequireAnyRole(constants.RoleTypeAdmin, constants.RoleTypeMod),
controller.GetVerificationByID,
)
@@ -28,21 +28,21 @@ func VerificationRoutes(app *fiber.App, controller *controllers.VerificationCont
route.Put(
"/:id/status",
middlewares.JwtAccess(userRepo),
middlewares.RequireAnyRole(constants.ADMIN, constants.MOD),
middlewares.RequireAnyRole(constants.RoleTypeAdmin, constants.RoleTypeMod),
controller.UpdateVerificationStatus,
)
route.Get(
"/",
middlewares.JwtAccess(userRepo),
middlewares.RequireAnyRole(constants.ADMIN, constants.MOD),
middlewares.RequireAnyRole(constants.RoleTypeAdmin, constants.RoleTypeMod),
controller.SearchVerification,
)
route.Post(
"/",
middlewares.JwtAccess(userRepo),
middlewares.ForbidRoles(constants.HISTORIAN),
middlewares.ForbidRoles(constants.RoleTypeHistorian),
controller.CreateVerification,
)

View File

@@ -136,7 +136,7 @@ func (a *authService) Signin(ctx context.Context, dto *request.SignInDto) (*resp
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
if user.AuthProvider != constants.LocalProvider.String() && user.PasswordHash == "" {
if user.AuthProvider != constants.ProviderTypeLocal.String() && user.PasswordHash == "" {
return nil, fiber.NewError(fiber.StatusUnauthorized, "Please sign in with "+user.AuthProvider)
}
@@ -219,7 +219,7 @@ func (a *authService) RefreshToken(ctx context.Context, id string, refreshToken
roles := models.RolesEntityToRoleConstant(user.Roles)
if slices.Contains(roles, constants.BANNED) {
if slices.Contains(roles, constants.RoleTypeBanned) {
return nil, fiber.NewError(fiber.StatusUnauthorized, "User is banned!")
}
@@ -254,7 +254,7 @@ func (a *authService) Signup(ctx context.Context, dto *request.SignUpDto) (*resp
return nil, fiber.NewError(fiber.StatusBadRequest, err.Error())
}
ok, err := a.tokenRepo.CheckVerified(ctx, dto.Email, constants.TokenEmailVerify, dto.TokenID)
ok, err := a.tokenRepo.CheckVerified(ctx, dto.Email, constants.TokenTypeEmailVerify, dto.TokenID)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
@@ -284,7 +284,7 @@ func (a *authService) Signup(ctx context.Context, dto *request.SignUpDto) (*resp
String: string(hashed),
Valid: len(hashed) != 0,
},
AuthProvider: constants.LocalProvider.String(),
AuthProvider: constants.ProviderTypeLocal.String(),
},
)
if err != nil {
@@ -308,7 +308,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.USER.String())
role, err := a.roleRepo.GetByname(ctx, constants.RoleTypeUser.String())
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
@@ -352,7 +352,7 @@ func (a *authService) Signup(ctx context.Context, dto *request.SignUpDto) (*resp
}
func (a *authService) ForgotPassword(ctx context.Context, dto *request.ForgotPasswordDto) error {
ok, err := a.tokenRepo.CheckVerified(ctx, dto.Email, constants.TokenPasswordReset, dto.TokenID)
ok, err := a.tokenRepo.CheckVerified(ctx, dto.Email, constants.TokenTypePasswordReset, dto.TokenID)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
@@ -423,7 +423,7 @@ func (a *authService) SigninWithGoogle(ctx context.Context, dto *request.SigninW
ctx,
sqlc.UpsertUserParams{
Email: dto.Email,
AuthProvider: constants.GoogleProvider.String(),
AuthProvider: constants.ProviderTypeGoogle.String(),
GoogleID: pgtype.Text{
String: dto.Sub,
Valid: dto.Sub != "",
@@ -454,7 +454,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.USER.String())
role, err := a.roleRepo.GetByname(ctx, constants.RoleTypeUser.String())
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
@@ -521,8 +521,8 @@ func (a *authService) CreateToken(ctx context.Context, dto *request.CreateTokenD
}
shouldSend := true
if (dto.TokenType == constants.TokenEmailVerify && user != nil) ||
(dto.TokenType == constants.TokenPasswordReset && user == nil) {
if (dto.TokenType == constants.TokenTypeEmailVerify && user != nil) ||
(dto.TokenType == constants.TokenTypePasswordReset && user == nil) {
shouldSend = false
}
@@ -575,8 +575,8 @@ func (a *authService) VerifyToken(ctx context.Context, dto *request.VerifyTokenD
return nil, fiber.NewError(fiber.StatusInternalServerError, "Internal Server Error")
}
if (dto.TokenType == constants.TokenEmailVerify && user != nil) ||
(dto.TokenType == constants.TokenPasswordReset && user == nil) {
if (dto.TokenType == constants.TokenTypeEmailVerify && user != nil) ||
(dto.TokenType == constants.TokenTypePasswordReset && user == nil) {
return nil, genericError
}

View File

@@ -71,7 +71,7 @@ func (m *mediaService) DeleteMedia(ctx context.Context, claims *response.JWTClai
}
shoudDelete := false
if slices.Contains(claims.Roles, constants.ADMIN) || slices.Contains(claims.Roles, constants.MOD) || media.UserID == claims.UId {
if slices.Contains(claims.Roles, constants.RoleTypeAdmin) || slices.Contains(claims.Roles, constants.RoleTypeMod) || media.UserID == claims.UId {
shoudDelete = true
}
@@ -95,7 +95,7 @@ func (m *mediaService) BulkDeleteMedia(ctx context.Context, claims *response.JWT
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
shoudDelete := false
if slices.Contains(claims.Roles, constants.ADMIN) || slices.Contains(claims.Roles, constants.MOD) {
if slices.Contains(claims.Roles, constants.RoleTypeAdmin) || slices.Contains(claims.Roles, constants.RoleTypeMod) {
shoudDelete = true
}
listMediaIds := make([]pgtype.UUID, len(listMedia))

View File

@@ -0,0 +1,256 @@
package services
import (
"context"
"fmt"
"github.com/gofiber/fiber/v3"
"github.com/jackc/pgx/v5/pgtype"
"golang.org/x/sync/errgroup"
"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"
)
type ProjectService interface {
GetProjectByID(ctx context.Context, id string) (*response.ProjectResponse, error)
GetProjectByUserID(ctx context.Context, userID string, dto *request.GetProjectsByUserDto) ([]*response.ProjectResponse, error)
SearchProject(ctx context.Context, dto *request.SearchProjectDto) (*response.PaginatedResponse, error)
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)
}
type projectService struct {
projectRepo repositories.ProjectRepository
}
func NewProjectService(projectRepo repositories.ProjectRepository) ProjectService {
return &projectService{
projectRepo: projectRepo,
}
}
func (s *projectService) GetProjectByID(ctx context.Context, id string) (*response.ProjectResponse, error) {
projectUUID, err := convert.StringToUUID(id)
if err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project ID format")
}
project, err := s.projectRepo.GetByID(ctx, projectUUID)
if err != nil {
return nil, fiber.NewError(fiber.StatusNotFound, "Project not found")
}
return project.ToResponse(), nil
}
func (s *projectService) GetProjectByUserID(ctx context.Context, userID string, dto *request.GetProjectsByUserDto) ([]*response.ProjectResponse, error) {
userUUID, err := convert.StringToUUID(userID)
if err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid user ID format")
}
limit := int32(20)
if dto.Limit > 0 {
limit = dto.Limit
}
arg := sqlc.GetProjectsByUserIdParams{
UserID: userUUID,
Limit: limit,
}
if dto.CursorID != "" {
if cursorID, err := convert.StringToUUID(dto.CursorID); err == nil {
arg.CursorID = cursorID
}
}
projects, err := s.projectRepo.GetByUserID(ctx, arg)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
return models.ProjectsEntityToResponse(projects), nil
}
func (s *projectService) fillSearchArgs(arg *sqlc.SearchProjectsParams, dto *request.SearchProjectDto) {
if dto.Sort != "" {
arg.Sort = dto.Sort
} else {
arg.Sort = "updated_at"
}
arg.Order = "desc"
if dto.Order == "asc" {
arg.Order = "asc"
}
if len(dto.Statuses) > 0 {
for _, statusStr := range dto.Statuses {
statusType := constants.ParseProjectStatusTypeText(statusStr)
if statusType != constants.ProjectStatusTypeUnknow {
arg.Statuses = append(arg.Statuses, fmt.Sprintf("%d", statusType.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.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 *projectService) SearchProject(ctx context.Context, dto *request.SearchProjectDto) (*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.SearchProjectsParams{
Limit: int32(dto.Limit),
Offset: int32(offset),
}
s.fillSearchArgs(&arg, dto)
var rows []*models.ProjectEntity
var totalRecords int64
g, gCtx := errgroup.WithContext(ctx)
g.Go(func() error {
var err error
rows, err = s.projectRepo.Search(gCtx, arg)
return err
})
g.Go(func() error {
countArg := sqlc.CountProjectsParams{
Statuses: arg.Statuses,
UserIds: arg.UserIds,
SearchText: arg.SearchText,
CreatedFrom: arg.CreatedFrom,
CreatedTo: arg.CreatedTo,
}
var err error
totalRecords, err = s.projectRepo.Count(gCtx, countArg)
return err
})
if err := g.Wait(); err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
projects := models.ProjectsEntityToResponse(rows)
return response.BuildPaginatedResponse(projects, totalRecords, dto.Page, dto.Limit), nil
}
func (s *projectService) CreateProject(ctx context.Context, userID string, dto *request.CreateProjectDto) (*response.ProjectResponse, error) {
userUUID, err := convert.StringToUUID(userID)
if err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid user ID format")
}
arg := sqlc.CreateProjectParams{
Title: dto.Title,
Description: convert.PtrToText(dto.Description),
ProjectStatus: constants.ProjectStatusTypePrivate.Int16(),
UserID: userUUID,
}
if dto.Status != nil {
statusType := constants.ParseProjectStatusTypeText(*dto.Status)
if statusType == constants.ProjectStatusTypeUnknow {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid status type")
}
arg.ProjectStatus = statusType.Int16()
}
project, err := s.projectRepo.Create(ctx, arg)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create project")
}
return project.ToResponse(), nil
}
func (s *projectService) UpdateProject(ctx context.Context, id string, dto *request.UpdateProjectDto) (*response.ProjectResponse, error) {
projectUUID, err := convert.StringToUUID(id)
if err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project ID format")
}
_, err = s.projectRepo.GetByID(ctx, projectUUID)
if err != nil {
return nil, fiber.NewError(fiber.StatusNotFound, "Project not found")
}
arg := sqlc.UpdateProjectParams{
ID: projectUUID,
}
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 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid status type")
}
arg.Status = pgtype.Int2{Int16: statusType.Int16(), Valid: true}
}
project, err := s.projectRepo.Update(ctx, arg)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update project")
}
return project.ToResponse(), nil
}
func (s *projectService) DeleteProject(ctx context.Context, id string) error {
projectUUID, err := convert.StringToUUID(id)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid project ID format")
}
_, err = s.projectRepo.GetByID(ctx, projectUUID)
if err != nil {
return fiber.NewError(fiber.StatusNotFound, "Project not found")
}
err = s.projectRepo.Delete(ctx, projectUUID)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete project")
}
return nil
}

View File

@@ -117,16 +117,16 @@ func (u *userService) ChangeRoleUser(ctx context.Context, userId string, claims
hasModRole := false
for _, r := range newListRole {
if r.Name == constants.USER.String() {
if r.Name == constants.RoleTypeUser.String() {
hasUserRole = true
}
if r.Name == constants.ADMIN.String() {
if r.Name == constants.RoleTypeAdmin.String() {
hasAdminRole = true
}
if r.Name == constants.BANNED.String() {
if r.Name == constants.RoleTypeBanned.String() {
hasBannedRole = true
}
if r.Name == constants.MOD.String() {
if r.Name == constants.RoleTypeMod.String() {
hasModRole = true
}
}
@@ -135,7 +135,7 @@ func (u *userService) ChangeRoleUser(ctx context.Context, userId string, claims
return nil, fiber.NewError(fiber.StatusNotFound, "User must have the USER role")
}
if slices.Contains(claims.Roles, constants.MOD) && !slices.Contains(claims.Roles, constants.ADMIN) {
if slices.Contains(claims.Roles, constants.RoleTypeMod) && !slices.Contains(claims.Roles, constants.RoleTypeAdmin) {
if hasAdminRole {
return nil, fiber.NewError(fiber.StatusForbidden, "MOD cannot assign ADMIN role to any user")
}
@@ -149,7 +149,7 @@ func (u *userService) ChangeRoleUser(ctx context.Context, userId string, claims
}
isTargetAdminOrMod := false
for _, r := range user.Roles {
if r.Name == constants.ADMIN.String() || r.Name == constants.MOD.String() {
if r.Name == constants.RoleTypeAdmin.String() || r.Name == constants.RoleTypeMod.String() {
isTargetAdminOrMod = true
break
}
@@ -159,7 +159,7 @@ func (u *userService) ChangeRoleUser(ctx context.Context, userId string, claims
}
}
if slices.Contains(claims.Roles, constants.ADMIN) {
if slices.Contains(claims.Roles, constants.RoleTypeAdmin) {
if userId == claims.UId && hasBannedRole {
return nil, fiber.NewError(fiber.StatusForbidden, "You can't assign BANNED role to yourself")
}

View File

@@ -53,7 +53,7 @@ func NewVerificationService(
func (v *verificationService) CreateVerification(ctx context.Context, userId string, dto *request.CreateUserVerificationDto) (*response.UserVerificationResponse, error) {
verifyType := constants.ParseVerifyTypeText(dto.VerifyType)
if verifyType == constants.VerifyUnknown {
if verifyType == constants.VerifyTypeUnknown {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Unknown verify type!")
}
@@ -124,11 +124,11 @@ func (v *verificationService) DeleteVerification(ctx context.Context, claims *re
}
shoudDelete := false
if slices.Contains(claims.Roles, constants.ADMIN) || slices.Contains(claims.Roles, constants.MOD) {
if slices.Contains(claims.Roles, constants.RoleTypeAdmin) || slices.Contains(claims.Roles, constants.RoleTypeMod) {
shoudDelete = true
}
if verification.User.ID == claims.UId && verification.Status == constants.StatusPending {
if verification.User.ID == claims.UId && verification.Status == constants.StatusTypePending {
shoudDelete = true
}
@@ -182,7 +182,7 @@ func (m *verificationService) fillSearchArgs(arg *sqlc.SearchUserVerificationsPa
if len(dto.Statuses) > 0 {
for _, id := range dto.Statuses {
if u := constants.ParseStatusTypeText(id); u != constants.StatusUnknown {
if u := constants.ParseStatusTypeText(id); u != constants.StatusTypeUnknown {
arg.Statuses = append(arg.Statuses, u.Int16())
}
}
@@ -190,7 +190,7 @@ func (m *verificationService) fillSearchArgs(arg *sqlc.SearchUserVerificationsPa
if len(dto.VerifyTypes) > 0 {
for _, id := range dto.VerifyTypes {
if u := constants.ParseVerifyTypeText(id); u != constants.VerifyUnknown {
if u := constants.ParseVerifyTypeText(id); u != constants.VerifyTypeUnknown {
arg.VerifyTypes = append(arg.VerifyTypes, u.Int16())
}
}
@@ -276,7 +276,7 @@ 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) {
statusType := constants.ParseStatusTypeText(dto.Status)
if statusType == constants.StatusUnknown {
if statusType == constants.StatusTypeUnknown {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Unknown status type!")
}
verificationUUID, err := convert.StringToUUID(verificationId)
@@ -289,7 +289,7 @@ func (v *verificationService) UpdateStatusVerification(ctx context.Context, user
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
historianRole, err := v.roleRepo.GetByname(ctx, constants.HISTORIAN.String())
historianRole, err := v.roleRepo.GetByname(ctx, constants.RoleTypeHistorian.String())
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
@@ -304,7 +304,7 @@ func (v *verificationService) UpdateStatusVerification(ctx context.Context, user
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
if verification.Status != constants.StatusPending {
if verification.Status != constants.StatusTypePending {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid status!")
}
@@ -340,7 +340,7 @@ func (v *verificationService) UpdateStatusVerification(ctx context.Context, user
Status: statusType,
}
if statusType == constants.StatusApproved {
if statusType == constants.StatusTypeApproved {
roleIdList := make([]pgtype.UUID, 0)
userVerification.Roles = append(userVerification.Roles, historianRole.ToRoleSimple())