From 677ae95c8fbc9ddff91fe9e169d9958f58ab1754 Mon Sep 17 00:00:00 2001 From: AzenKain Date: Tue, 7 Apr 2026 17:45:39 +0700 Subject: [PATCH] UPDATE: Something with module search --- db/query/files.sql | 31 ++++- db/query/users.sql | 96 +++++--------- docs/docs.go | 5 +- docs/swagger.json | 5 +- docs/swagger.yaml | 3 +- internal/dtos/request/user.go | 3 +- internal/gen/sqlc/files.sql.go | 43 ++++++- internal/gen/sqlc/users.sql.go | 158 ++++++------------------ internal/repositories/userRepository.go | 52 -------- internal/services/mediaService.go | 12 ++ internal/services/userService.go | 17 ++- 11 files changed, 177 insertions(+), 248 deletions(-) diff --git a/db/query/files.sql b/db/query/files.sql index 1dbfebd..d44b121 100644 --- a/db/query/files.sql +++ b/db/query/files.sql @@ -15,12 +15,41 @@ SELECT * FROM medias WHERE (sqlc.narg('cursor')::uuid IS NULL OR id > sqlc.narg('cursor')::uuid) + AND ( sqlc.narg('search_text')::text IS NULL OR original_name ILIKE '%' || sqlc.narg('search_text')::text || '%' OR storage_key ILIKE '%' || sqlc.narg('search_text')::text || '%' ) -ORDER BY id ASC + +ORDER BY + -- id + CASE + WHEN sqlc.narg('sort') = 'id' AND sqlc.narg('order') = 'asc' THEN id + END ASC, + CASE + WHEN sqlc.narg('sort') = 'id' AND sqlc.narg('order') = 'desc' THEN id + END DESC, + + -- created_at + CASE + WHEN sqlc.narg('sort') = 'created_at' AND sqlc.narg('order') = 'asc' THEN created_at + END ASC, + CASE + WHEN sqlc.narg('sort') = 'created_at' AND sqlc.narg('order') = 'desc' THEN created_at + END DESC, + + -- updated_at + CASE + WHEN sqlc.narg('sort') = 'updated_at' AND sqlc.narg('order') = 'asc' THEN updated_at + END ASC, + CASE + WHEN sqlc.narg('sort') = 'updated_at' AND sqlc.narg('order') = 'desc' THEN updated_at + END DESC, + + -- fallback + id ASC + LIMIT sqlc.arg('limit'); -- name: GetMediasByUserID :many diff --git a/db/query/users.sql b/db/query/users.sql index cee4375..c4b1b1b 100644 --- a/db/query/users.sql +++ b/db/query/users.sql @@ -195,59 +195,6 @@ SELECT FROM users u WHERE u.email = $1 AND u.is_deleted = false; --- name: GetUsers :many -SELECT - u.id, - u.email, - u.password_hash, - u.token_version, - u.refresh_token, - u.is_deleted, - u.created_at, - u.updated_at, - - -- profile JSON - ( - 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, - - -- roles JSON - ( - 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 - (sqlc.narg('cursor')::uuid IS NULL OR u.id > sqlc.narg('cursor')::uuid) - AND (sqlc.narg('is_deleted')::boolean IS NULL OR u.is_deleted = sqlc.narg('is_deleted')::boolean) - AND ( - sqlc.narg('role_ids')::uuid[] IS NULL OR - EXISTS ( - SELECT 1 FROM user_roles ur2 - WHERE ur2.user_id = u.id AND ur2.role_id = ANY(sqlc.narg('role_ids')::uuid[]) - ) - ) -ORDER BY u.id ASC -LIMIT sqlc.arg('limit'); - - -- name: SearchUsers :many SELECT u.id, @@ -285,26 +232,51 @@ SELECT ) AS roles FROM users u + WHERE (sqlc.narg('cursor')::uuid IS NULL OR u.id > sqlc.narg('cursor')::uuid) - + AND (sqlc.narg('is_deleted')::boolean IS NULL OR u.is_deleted = sqlc.narg('is_deleted')::boolean) + AND ( sqlc.narg('role_ids')::uuid[] IS NULL OR EXISTS ( SELECT 1 FROM user_roles ur2 - WHERE ur2.user_id = u.id AND ur2.role_id = ANY(sqlc.narg('role_ids')::uuid[]) + WHERE ur2.user_id = u.id + AND ur2.role_id = ANY(sqlc.narg('role_ids')::uuid[]) ) ) - + AND (sqlc.narg('search_id')::uuid IS NULL OR u.id = sqlc.narg('search_id')::uuid) + AND ( sqlc.narg('search_text')::text IS NULL OR - u.email ILIKE '%' || sqlc.narg('search_text')::text || '%' OR - EXISTS ( - SELECT 1 FROM user_profiles p - WHERE p.user_id = u.id AND p.display_name ILIKE '%' || sqlc.narg('search_text')::text || '%' - ) + u.email ILIKE '%' || sqlc.narg('search_text')::text || '%' ) -ORDER BY u.id ASC + +ORDER BY + -- id + CASE + WHEN sqlc.narg('sort') = 'id' AND sqlc.narg('order') = 'asc' THEN id + END ASC, + CASE + WHEN sqlc.narg('sort') = 'id' AND sqlc.narg('order') = 'desc' THEN id + END DESC, + -- created_at + CASE + WHEN sqlc.narg('sort') = 'created_at' AND sqlc.narg('order') = 'asc' THEN u.created_at + END ASC, + CASE + WHEN sqlc.narg('sort') = 'created_at' AND sqlc.narg('order') = 'desc' THEN u.created_at + END DESC, + -- updated_at + CASE + WHEN sqlc.narg('sort') = 'updated_at' AND sqlc.narg('order') = 'asc' THEN u.updated_at + END ASC, + CASE + WHEN sqlc.narg('sort') = 'updated_at' AND sqlc.narg('order') = 'desc' THEN u.updated_at + END DESC, + -- fallback + u.id ASC + LIMIT sqlc.arg('limit'); \ No newline at end of file diff --git a/docs/docs.go b/docs/docs.go index b1ab611..c58d8d0 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -875,10 +875,9 @@ const docTemplate = `{ }, { "enum": [ + "id", "created_at", - "updated_at", - "email", - "display_name" + "updated_at" ], "type": "string", "name": "sort", diff --git a/docs/swagger.json b/docs/swagger.json index f1455d6..df85aec 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -868,10 +868,9 @@ }, { "enum": [ + "id", "created_at", - "updated_at", - "email", - "display_name" + "updated_at" ], "type": "string", "name": "sort", diff --git a/docs/swagger.yaml b/docs/swagger.yaml index c78dd85..01d43aa 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -738,10 +738,9 @@ paths: name: search type: string - enum: + - id - created_at - updated_at - - email - - display_name in: query name: sort type: string diff --git a/internal/dtos/request/user.go b/internal/dtos/request/user.go index 8d5e769..fa43764 100644 --- a/internal/dtos/request/user.go +++ b/internal/dtos/request/user.go @@ -30,10 +30,9 @@ type GetAllUserDto struct { type CursorPaginationDto struct { Cursor string `json:"cursor" query:"cursor" validate:"omitempty,uuid"` Limit int `json:"limit" query:"limit" validate:"required,min=1,max=100"` - Sort string `json:"sort" query:"sort" validate:"omitempty,oneof=created_at updated_at email display_name"` + Sort string `json:"sort" query:"sort" validate:"omitempty,oneof=id created_at updated_at"` Order string `json:"order" query:"order" validate:"omitempty,oneof=asc desc"` } - type SearchUserDto struct { CursorPaginationDto Search string `json:"search" query:"search" validate:"omitempty,min=2,max=200"` diff --git a/internal/gen/sqlc/files.sql.go b/internal/gen/sqlc/files.sql.go index c569fbb..58bad24 100644 --- a/internal/gen/sqlc/files.sql.go +++ b/internal/gen/sqlc/files.sql.go @@ -126,23 +126,60 @@ SELECT id, user_id, storage_key, original_name, mime_type, size, file_metadata, FROM medias WHERE ($1::uuid IS NULL OR id > $1::uuid) + AND ( $2::text IS NULL OR original_name ILIKE '%' || $2::text || '%' OR storage_key ILIKE '%' || $2::text || '%' ) -ORDER BY id ASC -LIMIT $3 + +ORDER BY + -- id + CASE + WHEN $3 = 'id' AND $4 = 'asc' THEN id + END ASC, + CASE + WHEN $3 = 'id' AND $4 = 'desc' THEN id + END DESC, + + -- created_at + CASE + WHEN $3 = 'created_at' AND $4 = 'asc' THEN created_at + END ASC, + CASE + WHEN $3 = 'created_at' AND $4 = 'desc' THEN created_at + END DESC, + + -- updated_at + CASE + WHEN $3 = 'updated_at' AND $4 = 'asc' THEN updated_at + END ASC, + CASE + WHEN $3 = 'updated_at' AND $4 = 'desc' THEN updated_at + END DESC, + + -- fallback + id ASC + +LIMIT $5 ` type SearchMediasParams struct { Cursor pgtype.UUID `json:"cursor"` SearchText pgtype.Text `json:"search_text"` + Sort interface{} `json:"sort"` + Order interface{} `json:"order"` Limit int32 `json:"limit"` } func (q *Queries) SearchMedias(ctx context.Context, arg SearchMediasParams) ([]Media, error) { - rows, err := q.db.Query(ctx, searchMedias, arg.Cursor, arg.SearchText, arg.Limit) + rows, err := q.db.Query(ctx, searchMedias, + arg.Cursor, + arg.SearchText, + arg.Sort, + arg.Order, + arg.Limit, + ) if err != nil { return nil, err } diff --git a/internal/gen/sqlc/users.sql.go b/internal/gen/sqlc/users.sql.go index 36be411..5845cf2 100644 --- a/internal/gen/sqlc/users.sql.go +++ b/internal/gen/sqlc/users.sql.go @@ -286,115 +286,6 @@ func (q *Queries) GetUserByIDWithoutDeleted(ctx context.Context, id pgtype.UUID) return i, err } -const getUsers = `-- name: GetUsers :many -SELECT - u.id, - u.email, - u.password_hash, - u.token_version, - u.refresh_token, - u.is_deleted, - u.created_at, - u.updated_at, - - -- profile JSON - ( - 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, - - -- roles JSON - ( - 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 - ($1::uuid IS NULL OR u.id > $1::uuid) - AND ($2::boolean IS NULL OR u.is_deleted = $2::boolean) - AND ( - $3::uuid[] IS NULL OR - EXISTS ( - SELECT 1 FROM user_roles ur2 - WHERE ur2.user_id = u.id AND ur2.role_id = ANY($3::uuid[]) - ) - ) -ORDER BY u.id ASC -LIMIT $4 -` - -type GetUsersParams struct { - Cursor pgtype.UUID `json:"cursor"` - IsDeleted pgtype.Bool `json:"is_deleted"` - RoleIds []pgtype.UUID `json:"role_ids"` - Limit int32 `json:"limit"` -} - -type GetUsersRow struct { - ID pgtype.UUID `json:"id"` - Email string `json:"email"` - PasswordHash pgtype.Text `json:"password_hash"` - TokenVersion int32 `json:"token_version"` - RefreshToken pgtype.Text `json:"refresh_token"` - IsDeleted bool `json:"is_deleted"` - CreatedAt pgtype.Timestamptz `json:"created_at"` - UpdatedAt pgtype.Timestamptz `json:"updated_at"` - Profile []byte `json:"profile"` - Roles []byte `json:"roles"` -} - -func (q *Queries) GetUsers(ctx context.Context, arg GetUsersParams) ([]GetUsersRow, error) { - rows, err := q.db.Query(ctx, getUsers, - arg.Cursor, - arg.IsDeleted, - arg.RoleIds, - arg.Limit, - ) - if err != nil { - return nil, err - } - defer rows.Close() - items := []GetUsersRow{} - for rows.Next() { - var i GetUsersRow - if err := rows.Scan( - &i.ID, - &i.Email, - &i.PasswordHash, - &i.TokenVersion, - &i.RefreshToken, - &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 @@ -444,29 +335,54 @@ SELECT ) AS roles FROM users u + WHERE ($1::uuid IS NULL OR u.id > $1::uuid) - + AND ($2::boolean IS NULL OR u.is_deleted = $2::boolean) + AND ( $3::uuid[] IS NULL OR EXISTS ( SELECT 1 FROM user_roles ur2 - WHERE ur2.user_id = u.id AND ur2.role_id = ANY($3::uuid[]) + WHERE ur2.user_id = u.id + AND ur2.role_id = ANY($3::uuid[]) ) ) - + AND ($4::uuid IS NULL OR u.id = $4::uuid) + AND ( $5::text IS NULL OR - u.email ILIKE '%' || $5::text || '%' OR - EXISTS ( - SELECT 1 FROM user_profiles p - WHERE p.user_id = u.id AND p.display_name ILIKE '%' || $5::text || '%' - ) + u.email ILIKE '%' || $5::text || '%' ) -ORDER BY u.id ASC -LIMIT $6 + +ORDER BY + -- id + CASE + WHEN $6 = 'id' AND $7 = 'asc' THEN id + END ASC, + CASE + WHEN $6 = 'id' AND $7 = 'desc' THEN id + END DESC, + -- created_at + CASE + WHEN $6 = 'created_at' AND $7 = 'asc' THEN u.created_at + END ASC, + CASE + WHEN $6 = 'created_at' AND $7 = 'desc' THEN u.created_at + END DESC, + -- updated_at + CASE + WHEN $6 = 'updated_at' AND $7 = 'asc' THEN u.updated_at + END ASC, + CASE + WHEN $6 = 'updated_at' AND $7 = 'desc' THEN u.updated_at + END DESC, + -- fallback + u.id ASC + +LIMIT $8 ` type SearchUsersParams struct { @@ -475,6 +391,8 @@ type SearchUsersParams struct { RoleIds []pgtype.UUID `json:"role_ids"` SearchID pgtype.UUID `json:"search_id"` SearchText pgtype.Text `json:"search_text"` + Sort interface{} `json:"sort"` + Order interface{} `json:"order"` Limit int32 `json:"limit"` } @@ -498,6 +416,8 @@ func (q *Queries) SearchUsers(ctx context.Context, arg SearchUsersParams) ([]Sea arg.RoleIds, arg.SearchID, arg.SearchText, + arg.Sort, + arg.Order, arg.Limit, ) if err != nil { diff --git a/internal/repositories/userRepository.go b/internal/repositories/userRepository.go index 0793088..ce165f5 100644 --- a/internal/repositories/userRepository.go +++ b/internal/repositories/userRepository.go @@ -19,7 +19,6 @@ type UserRepository interface { GetByID(ctx context.Context, id pgtype.UUID) (*models.UserEntity, error) GetByIDWithoutDeleted(ctx context.Context, id pgtype.UUID) (*models.UserEntity, error) GetByEmail(ctx context.Context, email string) (*models.UserEntity, error) - All(ctx context.Context, params sqlc.GetUsersParams) ([]*models.UserEntity, error) Search(ctx context.Context, params sqlc.SearchUsersParams) ([]*models.UserEntity, error) UpsertUser(ctx context.Context, params sqlc.UpsertUserParams) (*models.UserEntity, error) CreateProfile(ctx context.Context, params sqlc.CreateUserProfileParams) (*models.UserProfileSimple, error) @@ -271,57 +270,6 @@ func (r *userRepository) CreateProfile(ctx context.Context, params sqlc.CreateUs }, nil } -func (r *userRepository) All(ctx context.Context, params sqlc.GetUsersParams) ([]*models.UserEntity, error) { - queryKey := r.generateQueryKey("user:all", 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.GetUsers(ctx, params) - if err != nil { - return nil, err - } - - var users []*models.UserEntity - var ids []string - usersToCache := make(map[string]any) - - for _, row := range rows { - user := &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), - } - - if err := user.ParseRoles(row.Roles); err != nil { - return nil, err - } - if err := user.ParseProfile(row.Profile); err != nil { - return nil, err - } - - users = append(users, user) - ids = append(ids, user.ID) - - usersToCache[fmt.Sprintf("user:id:%s", user.ID)] = user - } - - if len(usersToCache) > 0 { - _ = r.c.MSet(ctx, usersToCache, constants.NormalCacheDuration) - } - - if len(ids) > 0 { - _ = r.c.Set(ctx, queryKey, ids, constants.ListCacheDuration) - } - - return users, nil -} - func (r *userRepository) Search(ctx context.Context, params sqlc.SearchUsersParams) ([]*models.UserEntity, error) { queryKey := r.generateQueryKey("user:search", params) diff --git a/internal/services/mediaService.go b/internal/services/mediaService.go index 0ba577e..66f3ccc 100644 --- a/internal/services/mediaService.go +++ b/internal/services/mediaService.go @@ -115,6 +115,18 @@ func (m *mediaService) SearchMedia(ctx context.Context, dto *request.SearchMedia arg := sqlc.SearchMediasParams{ Limit: int32(dto.Limit + 1), } + + if dto.Sort != "" { + arg.Sort = pgtype.Text{String: dto.Sort, Valid: true} + } else { + arg.Sort = pgtype.Text{String: "id", Valid: true} + } + + if dto.Order != "" { + arg.Order = pgtype.Text{String: dto.Order, Valid: true} + } else { + arg.Order = pgtype.Text{String: "asc", Valid: true} + } if dto.Cursor != "" { pgID, err := convert.StringToUUID(dto.Cursor) diff --git a/internal/services/userService.go b/internal/services/userService.go index cbe1869..53f190b 100644 --- a/internal/services/userService.go +++ b/internal/services/userService.go @@ -213,6 +213,18 @@ func (u *userService) SearchUser(ctx context.Context, dto *request.SearchUserDto Limit: int32(dto.Limit + 1), } + if dto.Sort != "" { + arg.Sort = pgtype.Text{String: dto.Sort, Valid: true} + } else { + arg.Sort = pgtype.Text{String: "id", Valid: true} + } + + if dto.Order != "" { + arg.Order = pgtype.Text{String: dto.Order, Valid: true} + } else { + arg.Order = pgtype.Text{String: "asc", Valid: true} + } + if dto.Cursor != "" { pgID, err := convert.StringToUUID(dto.Cursor) if err != nil { @@ -259,11 +271,14 @@ func (u *userService) SearchUser(ctx context.Context, dto *request.SearchUserDto rows = rows[:dto.Limit] } + users := models.UsersEntityToResponse(rows) + res := &response.PaginatedResponse{ - Data: rows, + Data: users, Status: true, Message: "", } + res.Pagination.HasMore = hasMore res.Pagination.NextCursor = nextCursor