From 11ab06a50d3f18d3bd30ff8e628880a47bd545aa Mon Sep 17 00:00:00 2001 From: Aleksandr Soloshenko Date: Mon, 27 Apr 2026 12:20:33 +0700 Subject: [PATCH] [server] fill user info in references --- internal/server/docs/docs.go | 2 +- internal/server/dto/response.go | 17 +++ internal/server/tasks/dto.go | 44 +++---- internal/server/tasks/dto_attachments.go | 27 ++--- internal/server/tasks/dto_comments.go | 18 ++- internal/server/tasks/handler.go | 115 ++++++++++++++++--- internal/server/tasks/handler_attachments.go | 39 ++++++- internal/server/tasks/handler_comments.go | 49 +++++++- internal/users/models.go | 21 ++-- internal/users/repository.go | 38 +++++- internal/users/service.go | 10 ++ requests.http | 2 +- 12 files changed, 293 insertions(+), 89 deletions(-) diff --git a/internal/server/docs/docs.go b/internal/server/docs/docs.go index 657ef4b..6b681b3 100644 --- a/internal/server/docs/docs.go +++ b/internal/server/docs/docs.go @@ -813,7 +813,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/tasks.TaskResponse" + "$ref": "#/definitions/tasks.TaskDetailsResponse" } }, "400": { diff --git a/internal/server/dto/response.go b/internal/server/dto/response.go index 8fb5a3d..8bdb01d 100644 --- a/internal/server/dto/response.go +++ b/internal/server/dto/response.go @@ -26,6 +26,23 @@ func ToUserBrief(u *users.User) UserBrief { } } +func ResolveUserBrief(id *int64, lookup map[int64]users.User) *UserBrief { + if id == nil { + return nil + } + + if u, ok := lookup[*id]; ok { + return lo.ToPtr(ToUserBrief(&u)) + } + + return &UserBrief{ + ID: *id, + Name: "", + Role: "", + CreatedAt: "", + } +} + type UserBriefList struct { Items []UserBrief `json:"items"` Total int `json:"total"` diff --git a/internal/server/tasks/dto.go b/internal/server/tasks/dto.go index a23d719..9e4c85f 100644 --- a/internal/server/tasks/dto.go +++ b/internal/server/tasks/dto.go @@ -8,6 +8,7 @@ import ( "github.com/bit-issues/backend/internal/comments" "github.com/bit-issues/backend/internal/server/dto" "github.com/bit-issues/backend/internal/tasks" + "github.com/bit-issues/backend/internal/users" "github.com/samber/lo" ) @@ -104,18 +105,9 @@ type TaskResponse struct { } // newTaskResponse converts a domain Task to a TaskResponse DTO. -// It requires fetching author and assignee from the users service. -func newTaskResponse(task *tasks.Task) TaskResponse { - var assignee *dto.UserBrief - if task.AssigneeID != nil { - assignee = &dto.UserBrief{ - ID: *task.AssigneeID, - Name: "", - Role: "", - CreatedAt: "", - } - } - +// It enriches author and assignee fields from the pre-fetched users map. +// If a user is not found in the map, only the ID is populated (stub). +func newTaskResponse(task *tasks.Task, usersMap map[int64]users.User) TaskResponse { return TaskResponse{ ID: task.ID, ProjectSlug: task.ProjectSlug, @@ -124,16 +116,11 @@ func newTaskResponse(task *tasks.Task) TaskResponse { Description: task.Description, Priority: string(task.Priority), Status: string(task.Status), - Author: dto.UserBrief{ - ID: task.AuthorID, - Name: "", - Role: "", - CreatedAt: "", - }, - Assignee: assignee, - DueDate: task.DueDate, - CreatedAt: task.CreatedAt.Format(time.RFC3339), - UpdatedAt: task.UpdatedAt.Format(time.RFC3339), + Author: *dto.ResolveUserBrief(&task.AuthorID, usersMap), + Assignee: dto.ResolveUserBrief(task.AssigneeID, usersMap), + DueDate: task.DueDate, + CreatedAt: task.CreatedAt.Format(time.RFC3339), + UpdatedAt: task.UpdatedAt.Format(time.RFC3339), } } @@ -146,13 +133,14 @@ type TaskDetailsResponse struct { func newTaskDetailsResponse( task *tasks.Task, + usersMap map[int64]users.User, comments []comments.Comment, attachmentList []attachments.AttachmentWithURL, ) *TaskDetailsResponse { return &TaskDetailsResponse{ - TaskResponse: newTaskResponse(task), - Comments: toCommentsList(comments), - Attachments: toAttachmentsList(attachmentList), + TaskResponse: newTaskResponse(task, usersMap), + Comments: toCommentsList(comments, usersMap), + Attachments: toAttachmentsList(attachmentList, usersMap), } } @@ -165,12 +153,12 @@ type TaskListResponse struct { } // toTaskListResponse converts a list of domain Tasks to TaskListResponse DTO. -func toTaskListResponse(items []tasks.Task, total int) TaskListResponse { - return TaskListResponse{ +func toTaskListResponse(items []tasks.Task, usersMap map[int64]users.User, total int) *TaskListResponse { + return &TaskListResponse{ Items: lo.Map( items, func(t tasks.Task, _ int) TaskResponse { - return newTaskResponse(&t) + return newTaskResponse(&t, usersMap) }, ), Total: total, diff --git a/internal/server/tasks/dto_attachments.go b/internal/server/tasks/dto_attachments.go index 9f5dc98..e01aa1b 100644 --- a/internal/server/tasks/dto_attachments.go +++ b/internal/server/tasks/dto_attachments.go @@ -5,6 +5,7 @@ import ( "github.com/bit-issues/backend/internal/attachments" "github.com/bit-issues/backend/internal/server/dto" + "github.com/bit-issues/backend/internal/users" ) type AttachmentUploadRequest struct { @@ -50,42 +51,36 @@ func toUploadResponse(result *attachments.UploadResult) AttachmentUploadResponse } } -func toConfirmResponse(attachment *attachments.Attachment, downloadURL string) AttachmentConfirmResponse { +func toConfirmResponse( + attachment *attachments.Attachment, + downloadURL string, + usersMap map[int64]users.User, +) AttachmentConfirmResponse { return AttachmentConfirmResponse{ ID: attachment.ID, FileName: attachment.FileName, SizeBytes: attachment.SizeBytes, DownloadURL: downloadURL, UploadedAt: attachment.UploadedAt.UTC().Format(time.RFC3339), - UploadedBy: dto.UserBrief{ - ID: attachment.UploadedBy, - Name: "", - Role: "", - CreatedAt: "", - }, + UploadedBy: *dto.ResolveUserBrief(&attachment.UploadedBy, usersMap), } } -func toAttachmentResponse(item attachments.AttachmentWithURL) AttachmentResponse { +func toAttachmentResponse(item attachments.AttachmentWithURL, usersMap map[int64]users.User) AttachmentResponse { return AttachmentResponse{ ID: item.ID, FileName: item.FileName, SizeBytes: item.SizeBytes, UploadedAt: item.UploadedAt.UTC().Format(time.RFC3339), DownloadURL: item.DownloadURL, - UploadedBy: dto.UserBrief{ - ID: item.UploadedBy, - Name: "", - Role: "", - CreatedAt: "", - }, + UploadedBy: *dto.ResolveUserBrief(&item.UploadedBy, usersMap), } } -func toAttachmentsList(items []attachments.AttachmentWithURL) []AttachmentResponse { +func toAttachmentsList(items []attachments.AttachmentWithURL, usersMap map[int64]users.User) []AttachmentResponse { result := make([]AttachmentResponse, 0, len(items)) for _, item := range items { - result = append(result, toAttachmentResponse(item)) + result = append(result, toAttachmentResponse(item, usersMap)) } return result diff --git a/internal/server/tasks/dto_comments.go b/internal/server/tasks/dto_comments.go index 4a5889c..b66b3dc 100644 --- a/internal/server/tasks/dto_comments.go +++ b/internal/server/tasks/dto_comments.go @@ -5,6 +5,7 @@ import ( "github.com/bit-issues/backend/internal/comments" "github.com/bit-issues/backend/internal/server/dto" + "github.com/bit-issues/backend/internal/users" ) // CommentCreateRequest represents the request body for creating a new comment. @@ -33,15 +34,12 @@ type CommentResponse struct { } // toCommentResponse converts a domain Comment to an API response. -func toCommentResponse(comment *comments.Comment) CommentResponse { +// It enriches the author field from the pre-fetched users map. +// If the user is not found, only the ID is populated (stub). +func toCommentResponse(comment *comments.Comment, usersMap map[int64]users.User) CommentResponse { return CommentResponse{ - ID: comment.ID, - Author: dto.UserBrief{ - ID: comment.AuthorID, - Name: "", - Role: "", - CreatedAt: "", - }, + ID: comment.ID, + Author: *dto.ResolveUserBrief(&comment.AuthorID, usersMap), Content: comment.Content, CreatedAt: comment.CreatedAt.UTC().Format(time.RFC3339), UpdatedAt: comment.UpdatedAt.UTC().Format(time.RFC3339), @@ -49,10 +47,10 @@ func toCommentResponse(comment *comments.Comment) CommentResponse { } // toCommentsList converts a list of comments to an API response. -func toCommentsList(items []comments.Comment) []CommentResponse { +func toCommentsList(items []comments.Comment, usersMap map[int64]users.User) []CommentResponse { comments := make([]CommentResponse, 0, len(items)) for _, item := range items { - comments = append(comments, toCommentResponse(&item)) + comments = append(comments, toCommentResponse(&item, usersMap)) } return comments diff --git a/internal/server/tasks/handler.go b/internal/server/tasks/handler.go index 6f7e41d..e6d4434 100644 --- a/internal/server/tasks/handler.go +++ b/internal/server/tasks/handler.go @@ -1,6 +1,7 @@ package tasks import ( + "context" "errors" "fmt" "strconv" @@ -14,6 +15,7 @@ import ( "github.com/go-core-fx/fiberfx/validation" "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" + "go.uber.org/zap" ) // Handler handles HTTP requests for task-related endpoints. @@ -24,6 +26,8 @@ type Handler struct { commentsSvc *comments.Service attachmentsSvc *attachments.Service usersSvc *users.Service + + logger *zap.Logger } // NewHandler creates a new Handler instance with the given dependencies. @@ -32,6 +36,7 @@ func NewHandler( commentsSvc *comments.Service, attachmentsSvc *attachments.Service, usersSvc *users.Service, + logger *zap.Logger, validate *validator.Validate, ) handler.Handler { return &Handler{ @@ -41,6 +46,8 @@ func NewHandler( commentsSvc: commentsSvc, attachmentsSvc: attachmentsSvc, usersSvc: usersSvc, + + logger: logger, } } @@ -114,7 +121,12 @@ func (h *Handler) list(c *fiber.Ctx) error { return fmt.Errorf("failed to list tasks: %w", err) } - return c.JSON(toTaskListResponse(taskList, total)) + response, err := h.prepareListResponse(c.Context(), taskList, total) + if err != nil { + return fmt.Errorf("failed to prepare list response: %w", err) + } + + return c.JSON(response) } // @Summary Get task by ID @@ -143,17 +155,12 @@ func (h *Handler) get(c *fiber.Ctx) error { return fmt.Errorf("failed to get task: %w", err) } - comments, err := h.commentsSvc.ListByTask(c.Context(), task.ID) + response, err := h.prepareDetailsResponse(c.Context(), task) if err != nil { - return fmt.Errorf("failed to get comments: %w", err) + return fmt.Errorf("failed to prepare details response: %w", err) } - attachmentList, err := h.attachmentsSvc.ListByTask(c.Context(), task.ID) - if err != nil { - return fmt.Errorf("failed to get attachments: %w", err) - } - - return c.JSON(newTaskDetailsResponse(task, comments, attachmentList)) + return c.JSON(response) } // @Summary Create a new task @@ -186,7 +193,12 @@ func (h *Handler) post(c *fiber.Ctx, req *TaskCreateRequest) error { return fmt.Errorf("failed to create task: %w", err) } - return c.Status(fiber.StatusCreated).JSON(newTaskDetailsResponse(task, nil, nil)) + response, err := h.prepareDetailsResponse(c.Context(), task) + if err != nil { + return fmt.Errorf("failed to prepare details response: %w", err) + } + + return c.Status(fiber.StatusCreated).JSON(response) } // @Summary Update a task @@ -197,7 +209,7 @@ func (h *Handler) post(c *fiber.Ctx, req *TaskCreateRequest) error { // @Security BearerAuth // @Param id path int64 true "Task ID" // @Param request body TaskUpdateRequest true "Task update data" -// @Success 200 {object} TaskResponse +// @Success 200 {object} TaskDetailsResponse // @Failure 400 {object} fiberfx.ErrorResponse // @Failure 401 {object} fiberfx.ErrorResponse // @Failure 404 {object} fiberfx.ErrorResponse @@ -219,7 +231,12 @@ func (h *Handler) patch(c *fiber.Ctx, req *TaskUpdateRequest) error { return fmt.Errorf("failed to update task: %w", err) } - return c.JSON(newTaskResponse(task)) + response, err := h.prepareDetailsResponse(c.Context(), task) + if err != nil { + return fmt.Errorf("failed to prepare details response: %w", err) + } + + return c.JSON(response) } // @Summary Delete a task @@ -283,7 +300,79 @@ func (h *Handler) myTasks(c *fiber.Ctx) error { return fmt.Errorf("failed to list tasks: %w", err) } - return c.JSON(toTaskListResponse(taskList, total)) + response, err := h.prepareListResponse(c.Context(), taskList, total) + if err != nil { + return fmt.Errorf("failed to prepare list response: %w", err) + } + + return c.JSON(response) +} + +// fetchUsersForTasks collects unique user IDs from tasks and fetches them in a single batch call. +func (h *Handler) fetchUsersForTasks(ctx context.Context, tasks []tasks.Task) (map[int64]users.User, error) { + // Collect unique user IDs + idSet := make(map[int64]struct{}) + for _, t := range tasks { + idSet[t.AuthorID] = struct{}{} + if t.AssigneeID != nil { + idSet[*t.AssigneeID] = struct{}{} + } + } + + if len(idSet) == 0 { + return make(map[int64]users.User), nil + } + + // Convert set to slice + ids := make([]int64, 0, len(idSet)) + for id := range idSet { + ids = append(ids, id) + } + + // Batch fetch users + usersMap, err := h.usersSvc.LookupByIDs(ctx, ids) + if err != nil { + return nil, fmt.Errorf("failed to fetch users: %w", err) + } + + return usersMap, nil +} + +func (h *Handler) prepareListResponse( + ctx context.Context, + tasks []tasks.Task, + total int, +) (*TaskListResponse, error) { + // Fetch users for author and assignee enrichment + usersMap, err := h.fetchUsersForTasks(ctx, tasks) + if err != nil { + return nil, err + } + + return toTaskListResponse(tasks, usersMap, total), nil +} + +func (h *Handler) prepareDetailsResponse( + ctx context.Context, + task *tasks.Task, +) (*TaskDetailsResponse, error) { + comments, err := h.commentsSvc.ListByTask(ctx, task.ID) + if err != nil { + return nil, fmt.Errorf("failed to get comments: %w", err) + } + + attachmentList, err := h.attachmentsSvc.ListByTask(ctx, task.ID) + if err != nil { + return nil, fmt.Errorf("failed to get attachments: %w", err) + } + + // Fetch users for author and assignee enrichment + usersMap, err := h.fetchUsersForTasks(ctx, []tasks.Task{*task}) + if err != nil { + return nil, err + } + + return newTaskDetailsResponse(task, usersMap, comments, attachmentList), nil } // errorsHandler is a middleware that converts service errors to appropriate HTTP responses. diff --git a/internal/server/tasks/handler_attachments.go b/internal/server/tasks/handler_attachments.go index df0bcaa..db6d2d8 100644 --- a/internal/server/tasks/handler_attachments.go +++ b/internal/server/tasks/handler_attachments.go @@ -1,12 +1,15 @@ package tasks import ( + "context" "fmt" "strconv" "github.com/bit-issues/backend/internal/attachments" "github.com/bit-issues/backend/internal/server/middlewares/jwtauth" + "github.com/bit-issues/backend/internal/users" "github.com/gofiber/fiber/v2" + "go.uber.org/zap" ) func (h *Handler) attachmentInitUpload(c *fiber.Ctx, req *AttachmentUploadRequest) error { @@ -54,7 +57,14 @@ func (h *Handler) attachmentConfirmUpload(c *fiber.Ctx) error { return fmt.Errorf("failed to create download url: %w", err) } - return c.JSON(toConfirmResponse(attachment, downloadURL)) + // Fetch user for uploaded_by enrichment (best effort; confirm already succeeded) + usersMap, err := h.fetchUsersForAttachments(c.Context(), []attachments.Attachment{*attachment}) + if err != nil { + h.logger.Error("failed to fetch users for attachments", zap.Int64("attachment_id", id), zap.Error(err)) + usersMap = make(map[int64]users.User) + } + + return c.JSON(toConfirmResponse(attachment, downloadURL, usersMap)) } func (h *Handler) attachmentGetDownloadURL(c *fiber.Ctx) error { @@ -92,3 +102,30 @@ func (h *Handler) attachmentDelete(c *fiber.Ctx) error { return c.SendStatus(fiber.StatusNoContent) } + +// fetchUsersForAttachments collects unique user IDs from attachments and fetches them in a single batch call. +func (h *Handler) fetchUsersForAttachments( + ctx context.Context, + items []attachments.Attachment, +) (map[int64]users.User, error) { + idSet := make(map[int64]struct{}) + for _, a := range items { + idSet[a.UploadedBy] = struct{}{} + } + + if len(idSet) == 0 { + return make(map[int64]users.User), nil + } + + ids := make([]int64, 0, len(idSet)) + for id := range idSet { + ids = append(ids, id) + } + + usersMap, err := h.usersSvc.LookupByIDs(ctx, ids) + if err != nil { + return nil, fmt.Errorf("failed to fetch users: %w", err) + } + + return usersMap, nil +} diff --git a/internal/server/tasks/handler_comments.go b/internal/server/tasks/handler_comments.go index 660414d..0803c48 100644 --- a/internal/server/tasks/handler_comments.go +++ b/internal/server/tasks/handler_comments.go @@ -1,12 +1,15 @@ package tasks import ( + "context" "fmt" "strconv" "github.com/bit-issues/backend/internal/comments" "github.com/bit-issues/backend/internal/server/middlewares/jwtauth" + "github.com/bit-issues/backend/internal/users" "github.com/gofiber/fiber/v2" + "go.uber.org/zap" ) // @Summary Create a new comment on a task @@ -47,7 +50,14 @@ func (h *Handler) createComment(c *fiber.Ctx, req *CommentCreateRequest) error { return fmt.Errorf("failed to create comment: %w", err) } - return c.Status(fiber.StatusCreated).JSON(toCommentResponse(comment)) + // Fetch user for author enrichment (best-effort) + usersMap, err := h.fetchUsersForComments(c.Context(), []comments.Comment{*comment}) + if err != nil { + h.logger.Error("failed to fetch users for comment", zap.Int64("comment_id", comment.ID), zap.Error(err)) + usersMap = make(map[int64]users.User) + } + + return c.Status(fiber.StatusCreated).JSON(toCommentResponse(comment, usersMap)) } // @Summary Update a comment @@ -94,7 +104,15 @@ func (h *Handler) updateComment(c *fiber.Ctx, req *CommentUpdateRequest) error { return fmt.Errorf("failed to update comment: %w", err) } - return c.JSON(toCommentResponse(comment)) + // Fetch user for author enrichment (best-effort) + usersMap, err := h.fetchUsersForComments(c.Context(), []comments.Comment{*comment}) + if err != nil { + // Log error but continue with ID-only author stub + // The comment was successfully updated, don't fail the request due to user lookup failure + usersMap = make(map[int64]users.User) + } + + return c.JSON(toCommentResponse(comment, usersMap)) } // @Summary Delete a comment @@ -137,3 +155,30 @@ func (h *Handler) deleteComment(c *fiber.Ctx) error { return c.SendStatus(fiber.StatusNoContent) } + +// fetchUsersForComments collects unique user IDs from comments and fetches them in a single batch call. +func (h *Handler) fetchUsersForComments( + ctx context.Context, + items []comments.Comment, +) (map[int64]users.User, error) { + idSet := make(map[int64]struct{}) + for _, c := range items { + idSet[c.AuthorID] = struct{}{} + } + + if len(idSet) == 0 { + return make(map[int64]users.User), nil + } + + ids := make([]int64, 0, len(idSet)) + for id := range idSet { + ids = append(ids, id) + } + + usersMap, err := h.usersSvc.LookupByIDs(ctx, ids) + if err != nil { + return nil, fmt.Errorf("failed to fetch users: %w", err) + } + + return usersMap, nil +} diff --git a/internal/users/models.go b/internal/users/models.go index bfb957b..30a2226 100644 --- a/internal/users/models.go +++ b/internal/users/models.go @@ -41,22 +41,19 @@ func newUserModel(u UserInput, passwordHash string) *userModel { } } -func (m *userModel) toDomain() *UserWithPasswordHash { +func (m *userModel) toDomain() *User { if m == nil { return nil } - return &UserWithPasswordHash{ - User: User{ - ID: m.ID, - Email: m.Email, - Name: m.Name, - Role: m.Role, - Status: m.Status, - CreatedAt: m.CreatedAt, - UpdatedAt: m.UpdatedAt, - }, - PasswordHash: m.PasswordHash, + return &User{ + ID: m.ID, + Email: m.Email, + Name: m.Name, + Role: m.Role, + Status: m.Status, + CreatedAt: m.CreatedAt, + UpdatedAt: m.UpdatedAt, } } diff --git a/internal/users/repository.go b/internal/users/repository.go index ce300ee..a1ed182 100644 --- a/internal/users/repository.go +++ b/internal/users/repository.go @@ -29,7 +29,7 @@ func (r *Repository) Create(ctx context.Context, input UserInput, passwordHash s return nil, fmt.Errorf("failed to create user: %w", err) } - return &model.toDomain().User, nil + return model.toDomain(), nil } func (r *Repository) GetByEmail(ctx context.Context, email string) (*UserWithPasswordHash, error) { @@ -42,7 +42,10 @@ func (r *Repository) GetByEmail(ctx context.Context, email string) (*UserWithPas return nil, fmt.Errorf("failed to get user by email: %w", err) } - return model.toDomain(), nil + return &UserWithPasswordHash{ + User: *model.toDomain(), + PasswordHash: model.PasswordHash, + }, nil } func (r *Repository) GetByID(ctx context.Context, id int64) (*UserWithPasswordHash, error) { @@ -55,7 +58,10 @@ func (r *Repository) GetByID(ctx context.Context, id int64) (*UserWithPasswordHa return nil, fmt.Errorf("failed to get user by id: %w", err) } - return model.toDomain(), nil + return &UserWithPasswordHash{ + User: *model.toDomain(), + PasswordHash: model.PasswordHash, + }, nil } func (r *Repository) IsAdminByID(ctx context.Context, id int64) (bool, error) { @@ -96,7 +102,7 @@ func (r *Repository) List( users := make([]User, 0, len(models)) for _, model := range models { - users = append(users, model.toDomain().User) + users = append(users, *model.toDomain()) } return users, total, nil @@ -126,12 +132,34 @@ func (r *Repository) Search( users := make([]User, 0, len(models)) for _, model := range models { - users = append(users, model.toDomain().User) + users = append(users, *model.toDomain()) } return users, total, nil } +// GetByIDs retrieves multiple users by their IDs in a single query. +// Returns a map of user ID to User. Users that are not found are omitted from the map. +func (r *Repository) GetByIDs(ctx context.Context, ids []int64) (map[int64]User, error) { + if len(ids) == 0 { + return make(map[int64]User), nil + } + + models := make([]userModel, 0, len(ids)) + query := r.db.NewSelect().Model(&models).Where("id IN (?)", bun.List(ids)) + + if err := query.Scan(ctx); err != nil { + return nil, fmt.Errorf("failed to get users by IDs: %w", err) + } + + result := make(map[int64]User, len(models)) + for _, model := range models { + result[model.ID] = *model.toDomain() + } + + return result, nil +} + func (r *Repository) Update(ctx context.Context, id int64, update UserUpdate) error { if update.IsEmpty() { return nil diff --git a/internal/users/service.go b/internal/users/service.go index e9b5303..c9df152 100644 --- a/internal/users/service.go +++ b/internal/users/service.go @@ -79,6 +79,16 @@ func (s *Service) GetByID(ctx context.Context, id int64) (*User, error) { return &user.User, nil } +// LookupByIDs retrieves multiple users by their IDs in a single query. +// Returns a map of user ID to User. Users that are not found are omitted from the map. +func (s *Service) LookupByIDs(ctx context.Context, ids []int64) (map[int64]User, error) { + if len(ids) == 0 { + return make(map[int64]User), nil + } + + return s.repo.GetByIDs(ctx, ids) +} + func (s *Service) IsAdmin(ctx context.Context, id int64) (bool, error) { isAdmin, err := s.repo.IsAdminByID(ctx, id) if err != nil { diff --git a/requests.http b/requests.http index 07c8765..7573285 100644 --- a/requests.http +++ b/requests.http @@ -25,7 +25,7 @@ Content-Type: application/json ### # Register a new user # @name register -@email={{register.response.body.$.email}} +@email={{register.request.body.$.email}} @userID={{register.response.body.$.id}} POST {{baseURL}}/auth/register Content-Type: application/json