Added activities module
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
DO
|
||||
$$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM cron.job WHERE jobname = 'cleanup_activity_logs_hourly') THEN
|
||||
PERFORM cron.unschedule((SELECT jobid FROM cron.job WHERE jobname = 'cleanup_activity_logs_hourly'));
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
DROP TABLE IF EXISTS activity_logs;
|
||||
@@ -0,0 +1,30 @@
|
||||
CREATE UNLOGGED TABLE activity_logs
|
||||
(
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
family_id BIGINT REFERENCES families (id) ON DELETE CASCADE,
|
||||
user_id BIGINT NOT NULL REFERENCES users (id) ON DELETE CASCADE,
|
||||
action TEXT NOT NULL,
|
||||
entity_type TEXT NOT NULL,
|
||||
entity_id BIGINT,
|
||||
description TEXT NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_activity_logs_created_at ON activity_logs (created_at DESC);
|
||||
CREATE INDEX idx_activity_logs_user_id ON activity_logs (user_id);
|
||||
CREATE INDEX idx_activity_logs_family_id ON activity_logs (family_id);
|
||||
|
||||
DO
|
||||
$$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM cron.job WHERE jobname = 'cleanup_activity_logs_hourly') THEN
|
||||
PERFORM cron.unschedule((SELECT jobid FROM cron.job WHERE jobname = 'cleanup_activity_logs_hourly'));
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
SELECT cron.schedule(
|
||||
'cleanup_activity_logs_hourly',
|
||||
'0 * * * *',
|
||||
$$DELETE FROM activity_logs WHERE created_at < NOW() - INTERVAL '1 day'$$
|
||||
);
|
||||
@@ -15,6 +15,67 @@ const docTemplate = `{
|
||||
"host": "{{.Host}}",
|
||||
"basePath": "{{.BasePath}}",
|
||||
"paths": {
|
||||
"/activities": {
|
||||
"get": {
|
||||
"description": "Возвращает список действий пользователей с пагинацией",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Activities"
|
||||
],
|
||||
"summary": "Получить активность пользователей",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Family ID",
|
||||
"name": "family_id",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "User ID",
|
||||
"name": "user_id",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Limit, default 10",
|
||||
"name": "limit",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Offset",
|
||||
"name": "offset",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.ActivityListResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/families": {
|
||||
"post": {
|
||||
"description": "Создает новую семью",
|
||||
@@ -1158,6 +1219,52 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.ActivityListResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/dto.ActivityResponse"
|
||||
}
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer"
|
||||
},
|
||||
"offset": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.ActivityResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": {
|
||||
"type": "string"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"entity_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"entity_type": {
|
||||
"type": "string"
|
||||
},
|
||||
"family_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"user_id": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.CreateTransactionRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
||||
@@ -4,6 +4,67 @@
|
||||
"contact": {}
|
||||
},
|
||||
"paths": {
|
||||
"/activities": {
|
||||
"get": {
|
||||
"description": "Возвращает список действий пользователей с пагинацией",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Activities"
|
||||
],
|
||||
"summary": "Получить активность пользователей",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Family ID",
|
||||
"name": "family_id",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "User ID",
|
||||
"name": "user_id",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Limit, default 10",
|
||||
"name": "limit",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Offset",
|
||||
"name": "offset",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.ActivityListResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/families": {
|
||||
"post": {
|
||||
"description": "Создает новую семью",
|
||||
@@ -1147,6 +1208,52 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.ActivityListResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/dto.ActivityResponse"
|
||||
}
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer"
|
||||
},
|
||||
"offset": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.ActivityResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": {
|
||||
"type": "string"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"entity_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"entity_type": {
|
||||
"type": "string"
|
||||
},
|
||||
"family_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"user_id": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.CreateTransactionRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
||||
@@ -120,6 +120,36 @@ definitions:
|
||||
username:
|
||||
type: string
|
||||
type: object
|
||||
dto.ActivityListResponse:
|
||||
properties:
|
||||
items:
|
||||
items:
|
||||
$ref: '#/definitions/dto.ActivityResponse'
|
||||
type: array
|
||||
limit:
|
||||
type: integer
|
||||
offset:
|
||||
type: integer
|
||||
type: object
|
||||
dto.ActivityResponse:
|
||||
properties:
|
||||
action:
|
||||
type: string
|
||||
created_at:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
entity_id:
|
||||
type: integer
|
||||
entity_type:
|
||||
type: string
|
||||
family_id:
|
||||
type: integer
|
||||
id:
|
||||
type: integer
|
||||
user_id:
|
||||
type: integer
|
||||
type: object
|
||||
dto.CreateTransactionRequest:
|
||||
properties:
|
||||
amount:
|
||||
@@ -210,6 +240,46 @@ definitions:
|
||||
info:
|
||||
contact: {}
|
||||
paths:
|
||||
/activities:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Возвращает список действий пользователей с пагинацией
|
||||
parameters:
|
||||
- description: Family ID
|
||||
in: query
|
||||
name: family_id
|
||||
type: integer
|
||||
- description: User ID
|
||||
in: query
|
||||
name: user_id
|
||||
type: integer
|
||||
- description: Limit, default 10
|
||||
in: query
|
||||
name: limit
|
||||
type: integer
|
||||
- description: Offset
|
||||
in: query
|
||||
name: offset
|
||||
type: integer
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/dto.ActivityListResponse'
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/dto.ErrorResponse'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/dto.ErrorResponse'
|
||||
summary: Получить активность пользователей
|
||||
tags:
|
||||
- Activities
|
||||
/families:
|
||||
post:
|
||||
consumes:
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"FamilyHub/src/domain"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ActivityListQuery struct {
|
||||
FamilyID *int64 `form:"family_id"`
|
||||
UserID *int64 `form:"user_id"`
|
||||
Limit int `form:"limit"`
|
||||
Offset int `form:"offset"`
|
||||
}
|
||||
|
||||
type ActivityResponse struct {
|
||||
ID int64 `json:"id"`
|
||||
FamilyID *int64 `json:"family_id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
Action string `json:"action"`
|
||||
EntityType string `json:"entity_type"`
|
||||
EntityID *int64 `json:"entity_id"`
|
||||
Description string `json:"description"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
type ActivityListResponse struct {
|
||||
Items []ActivityResponse `json:"items"`
|
||||
Limit int `json:"limit"`
|
||||
Offset int `json:"offset"`
|
||||
}
|
||||
|
||||
func ActivityToResponse(activity *domain.ActivityLog) ActivityResponse {
|
||||
return ActivityResponse{
|
||||
ID: activity.ID,
|
||||
FamilyID: activity.FamilyID,
|
||||
UserID: activity.UserID,
|
||||
Action: activity.Action,
|
||||
EntityType: activity.EntityType,
|
||||
EntityID: activity.EntityID,
|
||||
Description: activity.Description,
|
||||
CreatedAt: activity.CreatedAt.Format(time.RFC3339),
|
||||
}
|
||||
}
|
||||
|
||||
func ActivitiesToListResponse(activities []*domain.ActivityLog, limit, offset int) ActivityListResponse {
|
||||
items := make([]ActivityResponse, 0, len(activities))
|
||||
for _, activity := range activities {
|
||||
items = append(items, ActivityToResponse(activity))
|
||||
}
|
||||
|
||||
return ActivityListResponse{
|
||||
Items: items,
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package routers
|
||||
|
||||
import (
|
||||
"FamilyHub/src/api/dto"
|
||||
"FamilyHub/src/api/services"
|
||||
"FamilyHub/src/domain"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type ActivitiesRouter struct {
|
||||
service services.ActivityService
|
||||
}
|
||||
|
||||
func NewActivitiesRouter(s services.ActivityService) *ActivitiesRouter {
|
||||
return &ActivitiesRouter{service: s}
|
||||
}
|
||||
|
||||
func (router *ActivitiesRouter) RegisterRoutes(r *gin.RouterGroup) {
|
||||
activities := r.Group("/activities")
|
||||
{
|
||||
activities.GET("", router.List)
|
||||
}
|
||||
}
|
||||
|
||||
// List GoDoc
|
||||
// @Summary Получить активность пользователей
|
||||
// @Description Возвращает список действий пользователей с пагинацией
|
||||
// @Tags Activities
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param family_id query int false "Family ID"
|
||||
// @Param user_id query int false "User ID"
|
||||
// @Param limit query int false "Limit, default 10"
|
||||
// @Param offset query int false "Offset"
|
||||
// @Success 200 {object} dto.ActivityListResponse
|
||||
// @Failure 400 {object} dto.ErrorResponse
|
||||
// @Failure 500 {object} dto.ErrorResponse
|
||||
// @Router /activities [get]
|
||||
func (router *ActivitiesRouter) List(c *gin.Context) {
|
||||
var query dto.ActivityListQuery
|
||||
if err := c.ShouldBindQuery(&query); err != nil {
|
||||
c.JSON(http.StatusBadRequest, dto.ErrorResponse{Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
activities, filter, err := router.service.List(c.Request.Context(), domain.ActivityLogListFilter{
|
||||
FamilyID: query.FamilyID,
|
||||
UserID: query.UserID,
|
||||
Limit: query.Limit,
|
||||
Offset: query.Offset,
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, dto.ErrorResponse{Message: "internal server error"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, dto.ActivitiesToListResponse(activities, filter.Limit, filter.Offset))
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package routers
|
||||
|
||||
import (
|
||||
"FamilyHub/src/api/services"
|
||||
"FamilyHub/src/domain"
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type activityServiceMock struct {
|
||||
listFn func(ctx context.Context, filter domain.ActivityLogListFilter) ([]*domain.ActivityLog, domain.ActivityLogListFilter, error)
|
||||
}
|
||||
|
||||
func (m *activityServiceMock) List(ctx context.Context, filter domain.ActivityLogListFilter) ([]*domain.ActivityLog, domain.ActivityLogListFilter, error) {
|
||||
if m.listFn != nil {
|
||||
return m.listFn(ctx, filter)
|
||||
}
|
||||
return nil, filter, errors.New("mock list is not configured")
|
||||
}
|
||||
|
||||
func setupActivitiesRouter(mock services.ActivityService) *gin.Engine {
|
||||
gin.SetMode(gin.TestMode)
|
||||
r := gin.New()
|
||||
apiV1 := r.Group("/api/v1")
|
||||
router := NewActivitiesRouter(mock)
|
||||
router.RegisterRoutes(apiV1)
|
||||
return r
|
||||
}
|
||||
|
||||
func TestActivitiesRouter_List(t *testing.T) {
|
||||
t.Run("uses default pagination", func(t *testing.T) {
|
||||
r := setupActivitiesRouter(&activityServiceMock{listFn: func(ctx context.Context, filter domain.ActivityLogListFilter) ([]*domain.ActivityLog, domain.ActivityLogListFilter, error) {
|
||||
assert.Equal(t, 0, filter.Limit)
|
||||
assert.Equal(t, 0, filter.Offset)
|
||||
|
||||
activity := &domain.ActivityLog{
|
||||
ID: 1,
|
||||
UserID: 2,
|
||||
Action: "create",
|
||||
EntityType: "transaction",
|
||||
Description: "Created transaction 1",
|
||||
CreatedAt: time.Date(2026, time.April, 11, 12, 0, 0, 0, time.UTC),
|
||||
}
|
||||
filter.Limit = 10
|
||||
return []*domain.ActivityLog{activity}, filter, nil
|
||||
}})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/activities", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Contains(t, w.Body.String(), "\"limit\":10")
|
||||
assert.Contains(t, w.Body.String(), "Created transaction 1")
|
||||
})
|
||||
}
|
||||
@@ -90,6 +90,7 @@ func NewServer(cfg config.Config) *Server {
|
||||
apiV1 := router.Group("/api/v1")
|
||||
|
||||
transactionRepo := repositories.NewTransactionsSQLRepository(dbConn)
|
||||
activityRepo := repositories.NewActivitySQLRepository(dbConn)
|
||||
|
||||
ocrSvc, err := ocr.NewGoogleOCR(context.Background())
|
||||
if err != nil {
|
||||
@@ -101,10 +102,14 @@ func NewServer(cfg config.Config) *Server {
|
||||
receiptRouter := routers.NewReceiptRouter(receiptService_, ocrSvc)
|
||||
receiptRouter.RegisterRoutes(apiV1)
|
||||
|
||||
transactionService := services.NewTransactionService(transactionRepo)
|
||||
transactionService := services.NewTransactionService(transactionRepo, activityRepo)
|
||||
transactionRouter := routers.NewTransactionsRouter(transactionService)
|
||||
transactionRouter.RegisterRoutes(apiV1)
|
||||
|
||||
activityService := services.NewActivityService(activityRepo)
|
||||
activityRouter := routers.NewActivitiesRouter(activityService)
|
||||
activityRouter.RegisterRoutes(apiV1)
|
||||
|
||||
usersRepo := repositories.NewUsersSQLRepository(dbConn)
|
||||
usersService := services.NewUserService(usersRepo)
|
||||
usersRouter := routers.NewUsersRouter(usersService)
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"FamilyHub/src/domain"
|
||||
"FamilyHub/src/repositories"
|
||||
"context"
|
||||
)
|
||||
|
||||
type ActivityService interface {
|
||||
List(ctx context.Context, filter domain.ActivityLogListFilter) ([]*domain.ActivityLog, domain.ActivityLogListFilter, error)
|
||||
}
|
||||
|
||||
type activityService struct {
|
||||
repo repositories.ActivityRepository
|
||||
}
|
||||
|
||||
func NewActivityService(repo repositories.ActivityRepository) ActivityService {
|
||||
return &activityService{repo: repo}
|
||||
}
|
||||
|
||||
func (s *activityService) List(ctx context.Context, filter domain.ActivityLogListFilter) ([]*domain.ActivityLog, domain.ActivityLogListFilter, error) {
|
||||
if filter.Limit <= 0 {
|
||||
filter.Limit = 10
|
||||
}
|
||||
if filter.Limit > 100 {
|
||||
filter.Limit = 100
|
||||
}
|
||||
if filter.Offset < 0 {
|
||||
filter.Offset = 0
|
||||
}
|
||||
|
||||
activities, err := s.repo.List(ctx, filter)
|
||||
if err != nil {
|
||||
return nil, filter, err
|
||||
}
|
||||
|
||||
return activities, filter, nil
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -19,11 +20,12 @@ type TransactionService interface {
|
||||
}
|
||||
|
||||
type transactionService struct {
|
||||
repo repositories.TransactionRepository
|
||||
repo repositories.TransactionRepository
|
||||
activityRepo repositories.ActivityRepository
|
||||
}
|
||||
|
||||
func NewTransactionService(repo repositories.TransactionRepository) TransactionService {
|
||||
return &transactionService{repo: repo}
|
||||
func NewTransactionService(repo repositories.TransactionRepository, activityRepo repositories.ActivityRepository) TransactionService {
|
||||
return &transactionService{repo: repo, activityRepo: activityRepo}
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -58,6 +60,18 @@ func (s *transactionService) Create(ctx context.Context, req domain.CreateTransa
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if s.activityRepo != nil {
|
||||
description := fmt.Sprintf("Created transaction %d", transaction.ID)
|
||||
_ = s.activityRepo.Create(ctx, &domain.ActivityLog{
|
||||
FamilyID: &transaction.FamilyID,
|
||||
UserID: transaction.CreatedBy,
|
||||
Action: "create",
|
||||
EntityType: "transaction",
|
||||
EntityID: &transaction.ID,
|
||||
Description: description,
|
||||
})
|
||||
}
|
||||
|
||||
return transaction, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package domain
|
||||
|
||||
import "time"
|
||||
|
||||
type ActivityLog struct {
|
||||
ID int64
|
||||
FamilyID *int64
|
||||
UserID int64
|
||||
Action string
|
||||
EntityType string
|
||||
EntityID *int64
|
||||
Description string
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
type ActivityLogCreateRequest struct {
|
||||
FamilyID *int64
|
||||
UserID int64
|
||||
Action string
|
||||
EntityType string
|
||||
EntityID *int64
|
||||
Description string
|
||||
}
|
||||
|
||||
type ActivityLogListFilter struct {
|
||||
FamilyID *int64
|
||||
UserID *int64
|
||||
Limit int
|
||||
Offset int
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"FamilyHub/src/domain"
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ActivityRepository interface {
|
||||
Create(ctx context.Context, activity *domain.ActivityLog) error
|
||||
List(ctx context.Context, filter domain.ActivityLogListFilter) ([]*domain.ActivityLog, error)
|
||||
}
|
||||
|
||||
type ActivitySQLRepository struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewActivitySQLRepository(db *sql.DB) *ActivitySQLRepository {
|
||||
return &ActivitySQLRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *ActivitySQLRepository) Create(ctx context.Context, activity *domain.ActivityLog) error {
|
||||
query := `
|
||||
INSERT INTO activity_logs (family_id, user_id, action, entity_type, entity_id, description)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
RETURNING id, created_at
|
||||
`
|
||||
|
||||
return r.db.QueryRowContext(
|
||||
ctx,
|
||||
query,
|
||||
activity.FamilyID,
|
||||
activity.UserID,
|
||||
activity.Action,
|
||||
activity.EntityType,
|
||||
activity.EntityID,
|
||||
activity.Description,
|
||||
).Scan(&activity.ID, &activity.CreatedAt)
|
||||
}
|
||||
|
||||
func (r *ActivitySQLRepository) List(ctx context.Context, filter domain.ActivityLogListFilter) ([]*domain.ActivityLog, error) {
|
||||
var (
|
||||
whereClauses []string
|
||||
args []any
|
||||
)
|
||||
|
||||
appendFilter := func(condition string, value any) {
|
||||
args = append(args, value)
|
||||
whereClauses = append(whereClauses, fmt.Sprintf(condition, len(args)))
|
||||
}
|
||||
|
||||
query := `
|
||||
SELECT id, family_id, user_id, action, entity_type, entity_id, description, created_at
|
||||
FROM activity_logs
|
||||
`
|
||||
|
||||
if filter.FamilyID != nil {
|
||||
appendFilter("family_id = $%d", *filter.FamilyID)
|
||||
}
|
||||
if filter.UserID != nil {
|
||||
appendFilter("user_id = $%d", *filter.UserID)
|
||||
}
|
||||
|
||||
if len(whereClauses) > 0 {
|
||||
query += " WHERE " + strings.Join(whereClauses, " AND ")
|
||||
}
|
||||
|
||||
args = append(args, filter.Limit, filter.Offset)
|
||||
query += fmt.Sprintf(" ORDER BY created_at DESC LIMIT $%d OFFSET $%d", len(args)-1, len(args))
|
||||
|
||||
rows, err := r.db.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var activities []*domain.ActivityLog
|
||||
for rows.Next() {
|
||||
var activity domain.ActivityLog
|
||||
if err := rows.Scan(
|
||||
&activity.ID,
|
||||
&activity.FamilyID,
|
||||
&activity.UserID,
|
||||
&activity.Action,
|
||||
&activity.EntityType,
|
||||
&activity.EntityID,
|
||||
&activity.Description,
|
||||
&activity.CreatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
activities = append(activities, &activity)
|
||||
}
|
||||
|
||||
return activities, rows.Err()
|
||||
}
|
||||
Reference in New Issue
Block a user