UPDATE: Change cursor to offset, bc FE dk implement
All checks were successful
Build and Release / release (push) Successful in 1m3s

This commit is contained in:
2026-04-08 13:35:18 +07:00
parent ff478f33b4
commit 82241b432e
17 changed files with 683 additions and 310 deletions

View File

@@ -15,14 +15,18 @@ CREATE TABLE IF NOT EXISTS users (
updated_at TIMESTAMPTZ DEFAULT now() updated_at TIMESTAMPTZ DEFAULT now()
); );
CREATE INDEX idx_users_active_created_at ALTER TABLE users ADD CONSTRAINT check_auth_provider
ON users (created_at DESC) CHECK (auth_provider IN ('local', 'google', 'facebook', 'github'));
WHERE is_deleted = false;
CREATE INDEX idx_users_provider_created_at ON users (auth_provider, created_at DESC);
CREATE INDEX idx_users_email_active CREATE INDEX idx_users_email_active
ON users (email) ON users (email)
WHERE is_deleted = false; WHERE is_deleted = false;
CREATE INDEX idx_users_email_trgm ON users USING gin (email gin_trgm_ops);
CREATE INDEX idx_users_id_trgm ON users USING gin ((id::text) gin_trgm_ops);
CREATE OR REPLACE FUNCTION update_updated_at() CREATE OR REPLACE FUNCTION update_updated_at()
RETURNS TRIGGER AS $$ RETURNS TRIGGER AS $$
BEGIN BEGIN

View File

@@ -12,5 +12,9 @@ CREATE TABLE medias (
updated_at TIMESTAMPTZ DEFAULT now() updated_at TIMESTAMPTZ DEFAULT now()
); );
CREATE INDEX idx_medias_user_created ON medias (user_id, created_at DESC);
CREATE INDEX idx_medias_original_name_trgm ON medias USING GIN (original_name gin_trgm_ops); CREATE INDEX idx_medias_original_name_trgm ON medias USING GIN (original_name gin_trgm_ops);
CREATE INDEX idx_medias_storage_key_trgm ON medias USING GIN (storage_key gin_trgm_ops);
CREATE INDEX idx_medias_size ON medias (size);
CREATE INDEX idx_medias_mime_type ON medias (mime_type);
CREATE INDEX idx_medias_user_created ON medias (user_id, created_at DESC);
CREATE INDEX idx_medias_created_at ON medias (created_at DESC);

View File

@@ -11,46 +11,61 @@ DELETE FROM medias
WHERE id = $1; WHERE id = $1;
-- name: SearchMedias :many -- name: SearchMedias :many
SELECT * SELECT
id, user_id, storage_key, original_name, mime_type, size, file_metadata, created_at, updated_at
FROM medias FROM medias
WHERE WHERE
(sqlc.narg('cursor')::uuid IS NULL OR id > sqlc.narg('cursor')::uuid) (sqlc.narg('user_ids')::uuid[] IS NULL OR user_id = ANY(sqlc.narg('user_ids')::uuid[]))
AND (sqlc.narg('mime_type')::text IS NULL OR mime_type ILIKE sqlc.narg('mime_type')::text || '%')
AND (sqlc.narg('min_size')::bigint IS NULL OR size >= sqlc.narg('min_size')::bigint)
AND (sqlc.narg('max_size')::bigint IS NULL OR size <= sqlc.narg('max_size')::bigint)
AND ( AND (
sqlc.narg('search_text')::text IS NULL OR sqlc.narg('search_text')::text IS NULL OR
id::text ILIKE '%' || sqlc.narg('search_text')::text || '%' OR
original_name ILIKE '%' || sqlc.narg('search_text')::text || '%' OR original_name ILIKE '%' || sqlc.narg('search_text')::text || '%' OR
storage_key ILIKE '%' || sqlc.narg('search_text')::text || '%' storage_key ILIKE '%' || sqlc.narg('search_text')::text || '%'
) )
ORDER BY ORDER BY
-- id CASE WHEN sqlc.narg('sort') = 'id' AND sqlc.narg('order') = 'asc' THEN id END ASC,
CASE CASE WHEN sqlc.narg('sort') = 'id' AND sqlc.narg('order') = 'desc' THEN id END DESC,
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 CASE WHEN sqlc.narg('sort') = 'created_at' AND sqlc.narg('order') = 'desc' THEN created_at END DESC,
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 CASE WHEN sqlc.narg('sort') = 'updated_at' AND sqlc.narg('order') = 'desc' THEN updated_at END DESC,
WHEN sqlc.narg('sort') = 'updated_at' AND sqlc.narg('order') = 'asc' THEN updated_at
END ASC, CASE WHEN sqlc.narg('sort') = 'size' AND sqlc.narg('order') = 'asc' THEN size END ASC,
CASE CASE WHEN sqlc.narg('sort') = 'size' AND sqlc.narg('order') = 'desc' THEN size END DESC,
WHEN sqlc.narg('sort') = 'updated_at' AND sqlc.narg('order') = 'desc' THEN updated_at
END DESC, CASE WHEN sqlc.narg('sort') = 'original_name' AND sqlc.narg('order') = 'asc' THEN original_name END ASC,
CASE WHEN sqlc.narg('sort') = 'original_name' AND sqlc.narg('order') = 'desc' THEN original_name END DESC,
CASE WHEN sqlc.narg('sort') = 'storage_key' AND sqlc.narg('order') = 'asc' THEN storage_key END ASC,
CASE WHEN sqlc.narg('sort') = 'storage_key' AND sqlc.narg('order') = 'desc' THEN storage_key END DESC,
CASE WHEN sqlc.narg('sort') = 'mime_type' AND sqlc.narg('order') = 'asc' THEN mime_type END ASC,
CASE WHEN sqlc.narg('sort') = 'mime_type' AND sqlc.narg('order') = 'desc' THEN mime_type END DESC,
-- fallback
id ASC id ASC
LIMIT sqlc.arg('limit')
OFFSET sqlc.arg('offset');
LIMIT sqlc.arg('limit');
-- name: CountMedias :one
SELECT count(*)
FROM medias
WHERE
(sqlc.narg('user_ids')::uuid[] IS NULL OR user_id = ANY(sqlc.narg('user_ids')::uuid[]))
AND (sqlc.narg('mime_type')::text IS NULL OR mime_type ILIKE sqlc.narg('mime_type')::text || '%')
AND (sqlc.narg('min_size')::bigint IS NULL OR size >= sqlc.narg('min_size')::bigint)
AND (sqlc.narg('max_size')::bigint IS NULL OR size <= sqlc.narg('max_size')::bigint)
AND (
sqlc.narg('search_text')::text IS NULL OR
id::text ILIKE '%' || sqlc.narg('search_text')::text || '%' OR
original_name ILIKE '%' || sqlc.narg('search_text')::text || '%' OR
storage_key ILIKE '%' || sqlc.narg('search_text')::text || '%'
);
-- name: GetMediasByUserID :many -- name: GetMediasByUserID :many
SELECT * FROM medias SELECT * FROM medias

View File

@@ -201,11 +201,12 @@ SELECT
u.email, u.email,
u.password_hash, u.password_hash,
u.token_version, u.token_version,
u.google_id,
u.auth_provider,
u.refresh_token, u.refresh_token,
u.is_deleted, u.is_deleted,
u.created_at, u.created_at,
u.updated_at, u.updated_at,
( (
SELECT json_build_object( SELECT json_build_object(
'display_name', p.display_name, 'display_name', p.display_name,
@@ -220,7 +221,6 @@ SELECT
FROM user_profiles p FROM user_profiles p
WHERE p.user_id = u.id WHERE p.user_id = u.id
) AS profile, ) AS profile,
( (
SELECT COALESCE( SELECT COALESCE(
json_agg(json_build_object('id', r.id, 'name', r.name)), json_agg(json_build_object('id', r.id, 'name', r.name)),
@@ -230,14 +230,9 @@ SELECT
JOIN roles r ON ur.role_id = r.id JOIN roles r ON ur.role_id = r.id
WHERE ur.user_id = u.id WHERE ur.user_id = u.id
) AS roles ) AS roles
FROM users u FROM users u
WHERE WHERE
(sqlc.narg('cursor')::uuid IS NULL OR u.id > sqlc.narg('cursor')::uuid) (sqlc.narg('is_deleted')::boolean IS NULL OR u.is_deleted = sqlc.narg('is_deleted')::boolean)
AND (sqlc.narg('is_deleted')::boolean IS NULL OR u.is_deleted = sqlc.narg('is_deleted')::boolean)
AND ( AND (
sqlc.narg('role_ids')::uuid[] IS NULL OR sqlc.narg('role_ids')::uuid[] IS NULL OR
EXISTS ( EXISTS (
@@ -246,37 +241,65 @@ WHERE
AND ur2.role_id = ANY(sqlc.narg('role_ids')::uuid[]) AND ur2.role_id = ANY(sqlc.narg('role_ids')::uuid[])
) )
) )
AND (sqlc.narg('auth_provider')::text IS NULL OR u.auth_provider = sqlc.narg('auth_provider')::text)
AND (sqlc.narg('search_id')::uuid IS NULL OR u.id = sqlc.narg('search_id')::uuid) AND (sqlc.narg('created_from')::timestamp IS NULL OR u.created_at >= sqlc.narg('created_from')::timestamp)
AND (sqlc.narg('created_to')::timestamp IS NULL OR u.created_at <= sqlc.narg('created_to')::timestamp)
AND ( AND (
sqlc.narg('search_text')::text IS NULL OR sqlc.narg('search_text')::text IS NULL OR
u.email ILIKE '%' || sqlc.narg('search_text')::text || '%' u.id::text ILIKE '%' || sqlc.narg('search_text')::text || '%' 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.full_name ILIKE '%' || sqlc.narg('search_text')::text || '%' OR
p.phone ILIKE '%' || sqlc.narg('search_text')::text || '%'
)
)
) )
ORDER BY ORDER BY
-- id CASE WHEN sqlc.narg('sort') = 'id' AND sqlc.narg('order') = 'asc' THEN u.id END ASC,
CASE CASE WHEN sqlc.narg('sort') = 'id' AND sqlc.narg('order') = 'desc' THEN u.id END DESC,
WHEN sqlc.narg('sort') = 'id' AND sqlc.narg('order') = 'asc' THEN id CASE WHEN sqlc.narg('sort') = 'created_at' AND sqlc.narg('order') = 'asc' THEN u.created_at END ASC,
END ASC, CASE WHEN sqlc.narg('sort') = 'created_at' AND sqlc.narg('order') = 'desc' THEN u.created_at END DESC,
CASE CASE WHEN sqlc.narg('sort') = 'updated_at' AND sqlc.narg('order') = 'asc' THEN u.updated_at END ASC,
WHEN sqlc.narg('sort') = 'id' AND sqlc.narg('order') = 'desc' THEN id CASE WHEN sqlc.narg('sort') = 'updated_at' AND sqlc.narg('order') = 'desc' THEN u.updated_at END DESC,
END DESC, CASE WHEN sqlc.narg('sort') = 'email' AND sqlc.narg('order') = 'asc' THEN u.email END ASC,
-- created_at CASE WHEN sqlc.narg('sort') = 'email' AND sqlc.narg('order') = 'desc' THEN u.email END DESC,
CASE CASE WHEN sqlc.narg('sort') = 'is_deleted' AND sqlc.narg('order') = 'asc' THEN u.is_deleted END ASC,
WHEN sqlc.narg('sort') = 'created_at' AND sqlc.narg('order') = 'asc' THEN u.created_at CASE WHEN sqlc.narg('sort') = 'is_deleted' AND sqlc.narg('order') = 'desc' THEN u.is_deleted END DESC,
END ASC, CASE WHEN sqlc.narg('sort') = 'auth_provider' AND sqlc.narg('order') = 'asc' THEN u.auth_provider END ASC,
CASE CASE WHEN sqlc.narg('sort') = 'auth_provider' AND sqlc.narg('order') = 'desc' THEN u.auth_provider END DESC,
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 u.id ASC
LIMIT sqlc.arg('limit')
OFFSET sqlc.arg('offset');
LIMIT sqlc.arg('limit'); -- name: CountUsers :one
SELECT count(*)
FROM users u
WHERE
(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[])
)
)
AND (sqlc.narg('auth_provider')::text IS NULL OR u.auth_provider = sqlc.narg('auth_provider')::text)
AND (sqlc.narg('created_from')::timestamp IS NULL OR u.created_at >= sqlc.narg('created_from')::timestamp)
AND (sqlc.narg('created_to')::timestamp IS NULL OR u.created_at <= sqlc.narg('created_to')::timestamp)
AND (
sqlc.narg('search_text')::text IS NULL OR
u.id::text ILIKE '%' || sqlc.narg('search_text')::text || '%' 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.full_name ILIKE '%' || sqlc.narg('search_text')::text || '%' OR
p.phone ILIKE '%' || sqlc.narg('search_text')::text || '%'
)
)
);

View File

@@ -869,7 +869,17 @@ const docTemplate = `{
"parameters": [ "parameters": [
{ {
"type": "string", "type": "string",
"name": "cursor", "name": "auth_provider",
"in": "query"
},
{
"type": "string",
"name": "created_from",
"in": "query"
},
{
"type": "string",
"name": "created_to",
"in": "query" "in": "query"
}, },
{ {
@@ -894,6 +904,12 @@ const docTemplate = `{
"name": "order", "name": "order",
"in": "query" "in": "query"
}, },
{
"minimum": 1,
"type": "integer",
"name": "page",
"in": "query"
},
{ {
"type": "array", "type": "array",
"items": { "items": {
@@ -914,7 +930,10 @@ const docTemplate = `{
"enum": [ "enum": [
"id", "id",
"created_at", "created_at",
"updated_at" "updated_at",
"email",
"is_deleted",
"auth_provider"
], ],
"type": "string", "type": "string",
"name": "sort", "name": "sort",
@@ -1570,21 +1589,30 @@ const docTemplate = `{
"type": "string" "type": "string"
}, },
"pagination": { "pagination": {
"type": "object", "$ref": "#/definitions/history-api_internal_dtos_response.PaginationMeta"
"properties": {
"has_more": {
"type": "boolean"
},
"next_cursor": {
"type": "string"
}
}
}, },
"status": { "status": {
"type": "boolean" "type": "boolean"
} }
} }
}, },
"history-api_internal_dtos_response.PaginationMeta": {
"type": "object",
"properties": {
"current_page": {
"type": "integer"
},
"page_size": {
"type": "integer"
},
"total_pages": {
"type": "integer"
},
"total_records": {
"type": "integer"
}
}
},
"history-api_pkg_constants.TokenType": { "history-api_pkg_constants.TokenType": {
"type": "integer", "type": "integer",
"format": "int32", "format": "int32",

View File

@@ -862,7 +862,17 @@
"parameters": [ "parameters": [
{ {
"type": "string", "type": "string",
"name": "cursor", "name": "auth_provider",
"in": "query"
},
{
"type": "string",
"name": "created_from",
"in": "query"
},
{
"type": "string",
"name": "created_to",
"in": "query" "in": "query"
}, },
{ {
@@ -887,6 +897,12 @@
"name": "order", "name": "order",
"in": "query" "in": "query"
}, },
{
"minimum": 1,
"type": "integer",
"name": "page",
"in": "query"
},
{ {
"type": "array", "type": "array",
"items": { "items": {
@@ -907,7 +923,10 @@
"enum": [ "enum": [
"id", "id",
"created_at", "created_at",
"updated_at" "updated_at",
"email",
"is_deleted",
"auth_provider"
], ],
"type": "string", "type": "string",
"name": "sort", "name": "sort",
@@ -1563,21 +1582,30 @@
"type": "string" "type": "string"
}, },
"pagination": { "pagination": {
"type": "object", "$ref": "#/definitions/history-api_internal_dtos_response.PaginationMeta"
"properties": {
"has_more": {
"type": "boolean"
},
"next_cursor": {
"type": "string"
}
}
}, },
"status": { "status": {
"type": "boolean" "type": "boolean"
} }
} }
}, },
"history-api_internal_dtos_response.PaginationMeta": {
"type": "object",
"properties": {
"current_page": {
"type": "integer"
},
"page_size": {
"type": "integer"
},
"total_pages": {
"type": "integer"
},
"total_records": {
"type": "integer"
}
}
},
"history-api_pkg_constants.TokenType": { "history-api_pkg_constants.TokenType": {
"type": "integer", "type": "integer",
"format": "int32", "format": "int32",

View File

@@ -156,15 +156,21 @@ definitions:
message: message:
type: string type: string
pagination: pagination:
properties: $ref: '#/definitions/history-api_internal_dtos_response.PaginationMeta'
has_more:
type: boolean
next_cursor:
type: string
type: object
status: status:
type: boolean type: boolean
type: object type: object
history-api_internal_dtos_response.PaginationMeta:
properties:
current_page:
type: integer
page_size:
type: integer
total_pages:
type: integer
total_records:
type: integer
type: object
history-api_pkg_constants.TokenType: history-api_pkg_constants.TokenType:
enum: enum:
- 1 - 1
@@ -732,7 +738,13 @@ paths:
description: Search and filter users with pagination (Admin/Mod only) description: Search and filter users with pagination (Admin/Mod only)
parameters: parameters:
- in: query - in: query
name: cursor name: auth_provider
type: string
- in: query
name: created_from
type: string
- in: query
name: created_to
type: string type: string
- in: query - in: query
name: is_deleted name: is_deleted
@@ -749,6 +761,10 @@ paths:
in: query in: query
name: order name: order
type: string type: string
- in: query
minimum: 1
name: page
type: integer
- collectionFormat: csv - collectionFormat: csv
in: query in: query
items: items:
@@ -764,6 +780,9 @@ paths:
- id - id
- created_at - created_at
- updated_at - updated_at
- email
- is_deleted
- auth_provider
in: query in: query
name: sort name: sort
type: string type: string

View File

@@ -11,6 +11,11 @@ type PreSignedCompleteDto struct {
} }
type SearchMediaDto struct { type SearchMediaDto struct {
CursorPaginationDto PaginationDto
Search string `json:"search" query:"search" validate:"omitempty,min=2,max=200"` Sort string `json:"sort" query:"sort" validate:"omitempty,oneof=id created_at updated_at size original_name storage_key mime_type"`
Search string `json:"search" query:"search" validate:"omitempty,min=2,max=200"`
UserIDs []string `json:"user_ids" query:"user_ids" validate:"omitempty,dive,uuid"`
MimeType string `json:"mime_type" query:"mime_type" validate:"omitempty,max=100"`
MinSize *int64 `json:"min_size" query:"min_size" validate:"omitempty,min=0"`
MaxSize *int64 `json:"max_size" query:"max_size" validate:"omitempty,min=0,gtefield=MinSize"`
} }

View File

@@ -1,5 +1,7 @@
package request package request
import "time"
type UpdateProfileDto struct { type UpdateProfileDto struct {
DisplayName string `json:"display_name" validate:"omitempty,min=2,max=50"` DisplayName string `json:"display_name" validate:"omitempty,min=2,max=50"`
FullName string `json:"full_name" validate:"omitempty,min=2,max=100"` FullName string `json:"full_name" validate:"omitempty,min=2,max=100"`
@@ -21,21 +23,18 @@ type ChangeRoleDto struct {
Roles []string `json:"role_ids" validate:"required,min=1,dive,required,uuid"` Roles []string `json:"role_ids" validate:"required,min=1,dive,required,uuid"`
} }
type GetAllUserDto struct { type PaginationDto struct {
CursorPaginationDto Page int `json:"page" query:"page" validate:"omitempty,min=1"`
IsDeleted *bool `json:"is_deleted" query:"is_deleted" validate:"omitempty"` Limit int `json:"limit" query:"limit" validate:"required,min=1,max=100"`
RoleIDs []string `json:"role_ids" query:"role_ids" validate:"omitempty,dive,uuid"` Order string `json:"order" query:"order" validate:"omitempty,oneof=asc desc"`
}
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=id created_at updated_at"`
Order string `json:"order" query:"order" validate:"omitempty,oneof=asc desc"`
} }
type SearchUserDto struct { type SearchUserDto struct {
CursorPaginationDto PaginationDto
Search string `json:"search" query:"search" validate:"omitempty,min=2,max=200"` Sort string `json:"sort" query:"sort" validate:"omitempty,oneof=id created_at updated_at email is_deleted auth_provider"`
IsDeleted *bool `json:"is_deleted" query:"is_deleted" validate:"omitempty"` Search string `json:"search" query:"search" validate:"omitempty,min=2,max=200"`
RoleIDs []string `json:"role_ids" query:"role_ids" validate:"omitempty,dive,uuid"` IsDeleted *bool `json:"is_deleted" query:"is_deleted" validate:"omitempty"`
RoleIDs []string `json:"role_ids" query:"role_ids" validate:"omitempty,dive,uuid"`
AuthProvider string `json:"auth_provider" query:"auth_provider" validate:"omitempty"`
CreatedFrom *time.Time `json:"created_from" query:"created_from" validate:"omitempty"`
CreatedTo *time.Time `json:"created_to" query:"created_to" validate:"omitempty"`
} }

View File

@@ -13,18 +13,45 @@ type CommonResponse struct {
} }
type JWTClaims struct { type JWTClaims struct {
UId string `json:"uid"` UId string `json:"uid"`
Roles []constants.Role `json:"roles"` Roles []constants.Role `json:"roles"`
TokenVersion int32 `json:"token_version"` TokenVersion int32 `json:"token_version"`
jwt.RegisteredClaims jwt.RegisteredClaims
} }
type PaginatedResponse struct { type PaginationMeta struct {
Data any `json:"data"` CurrentPage int `json:"current_page"`
Status bool `json:"status"` PageSize int `json:"page_size"`
Message string `json:"message"` TotalRecords int64 `json:"total_records"`
Pagination struct { TotalPages int `json:"total_pages"`
NextCursor string `json:"next_cursor"` }
HasMore bool `json:"has_more"`
} `json:"pagination"` type PaginatedResponse struct {
Status bool `json:"status"`
Message string `json:"message"`
Data any `json:"data"`
Pagination *PaginationMeta `json:"pagination"`
}
func BuildPaginatedResponse(data any, totalRecords int64, page int, limit int) *PaginatedResponse {
if page < 1 {
page = 1
}
if limit < 1 {
limit = 10
}
totalPages := int((totalRecords + int64(limit) - 1) / int64(limit))
return &PaginatedResponse{
Status: true,
Message: "Success",
Data: data,
Pagination: &PaginationMeta{
CurrentPage: page,
PageSize: limit,
TotalRecords: totalRecords,
TotalPages: totalPages,
},
}
} }

View File

@@ -11,6 +11,43 @@ import (
"github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgtype"
) )
const countMedias = `-- name: CountMedias :one
SELECT count(*)
FROM medias
WHERE
($1::uuid[] IS NULL OR user_id = ANY($1::uuid[]))
AND ($2::text IS NULL OR mime_type ILIKE $2::text || '%')
AND ($3::bigint IS NULL OR size >= $3::bigint)
AND ($4::bigint IS NULL OR size <= $4::bigint)
AND (
$5::text IS NULL OR
id::text ILIKE '%' || $5::text || '%' OR
original_name ILIKE '%' || $5::text || '%' OR
storage_key ILIKE '%' || $5::text || '%'
)
`
type CountMediasParams struct {
UserIds []pgtype.UUID `json:"user_ids"`
MimeType pgtype.Text `json:"mime_type"`
MinSize pgtype.Int8 `json:"min_size"`
MaxSize pgtype.Int8 `json:"max_size"`
SearchText pgtype.Text `json:"search_text"`
}
func (q *Queries) CountMedias(ctx context.Context, arg CountMediasParams) (int64, error) {
row := q.db.QueryRow(ctx, countMedias,
arg.UserIds,
arg.MimeType,
arg.MinSize,
arg.MaxSize,
arg.SearchText,
)
var count int64
err := row.Scan(&count)
return count, err
}
const createMedia = `-- name: CreateMedia :one const createMedia = `-- name: CreateMedia :one
INSERT INTO medias ( INSERT INTO medias (
user_id, storage_key, original_name, mime_type, size, file_metadata user_id, storage_key, original_name, mime_type, size, file_metadata
@@ -122,62 +159,69 @@ func (q *Queries) GetMediasByUserID(ctx context.Context, userID pgtype.UUID) ([]
} }
const searchMedias = `-- name: SearchMedias :many const searchMedias = `-- name: SearchMedias :many
SELECT id, user_id, storage_key, original_name, mime_type, size, file_metadata, created_at, updated_at SELECT
id, user_id, storage_key, original_name, mime_type, size, file_metadata, created_at, updated_at
FROM medias FROM medias
WHERE WHERE
($1::uuid IS NULL OR id > $1::uuid) ($1::uuid[] IS NULL OR user_id = ANY($1::uuid[]))
AND ($2::text IS NULL OR mime_type ILIKE $2::text || '%')
AND ($3::bigint IS NULL OR size >= $3::bigint)
AND ($4::bigint IS NULL OR size <= $4::bigint)
AND ( AND (
$2::text IS NULL OR $5::text IS NULL OR
original_name ILIKE '%' || $2::text || '%' OR id::text ILIKE '%' || $5::text || '%' OR
storage_key ILIKE '%' || $2::text || '%' original_name ILIKE '%' || $5::text || '%' OR
storage_key ILIKE '%' || $5::text || '%'
) )
ORDER BY ORDER BY
-- id CASE WHEN $6 = 'id' AND $7 = 'asc' THEN id END ASC,
CASE CASE WHEN $6 = 'id' AND $7 = 'desc' THEN id END DESC,
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 $6 = 'created_at' AND $7 = 'asc' THEN created_at END ASC,
CASE CASE WHEN $6 = 'created_at' AND $7 = 'desc' THEN created_at END DESC,
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 $6 = 'updated_at' AND $7 = 'asc' THEN updated_at END ASC,
CASE CASE WHEN $6 = 'updated_at' AND $7 = 'desc' THEN updated_at END DESC,
WHEN $3 = 'updated_at' AND $4 = 'asc' THEN updated_at
END ASC, CASE WHEN $6 = 'size' AND $7 = 'asc' THEN size END ASC,
CASE CASE WHEN $6 = 'size' AND $7 = 'desc' THEN size END DESC,
WHEN $3 = 'updated_at' AND $4 = 'desc' THEN updated_at
END DESC, CASE WHEN $6 = 'original_name' AND $7 = 'asc' THEN original_name END ASC,
CASE WHEN $6 = 'original_name' AND $7 = 'desc' THEN original_name END DESC,
CASE WHEN $6 = 'storage_key' AND $7 = 'asc' THEN storage_key END ASC,
CASE WHEN $6 = 'storage_key' AND $7 = 'desc' THEN storage_key END DESC,
CASE WHEN $6 = 'mime_type' AND $7 = 'asc' THEN mime_type END ASC,
CASE WHEN $6 = 'mime_type' AND $7 = 'desc' THEN mime_type END DESC,
-- fallback
id ASC id ASC
LIMIT $9
LIMIT $5 OFFSET $8
` `
type SearchMediasParams struct { type SearchMediasParams struct {
Cursor pgtype.UUID `json:"cursor"` UserIds []pgtype.UUID `json:"user_ids"`
SearchText pgtype.Text `json:"search_text"` MimeType pgtype.Text `json:"mime_type"`
Sort interface{} `json:"sort"` MinSize pgtype.Int8 `json:"min_size"`
Order interface{} `json:"order"` MaxSize pgtype.Int8 `json:"max_size"`
Limit int32 `json:"limit"` SearchText pgtype.Text `json:"search_text"`
Sort interface{} `json:"sort"`
Order interface{} `json:"order"`
Offset int32 `json:"offset"`
Limit int32 `json:"limit"`
} }
func (q *Queries) SearchMedias(ctx context.Context, arg SearchMediasParams) ([]Media, error) { func (q *Queries) SearchMedias(ctx context.Context, arg SearchMediasParams) ([]Media, error) {
rows, err := q.db.Query(ctx, searchMedias, rows, err := q.db.Query(ctx, searchMedias,
arg.Cursor, arg.UserIds,
arg.MimeType,
arg.MinSize,
arg.MaxSize,
arg.SearchText, arg.SearchText,
arg.Sort, arg.Sort,
arg.Order, arg.Order,
arg.Offset,
arg.Limit, arg.Limit,
) )
if err != nil { if err != nil {

View File

@@ -11,6 +11,60 @@ import (
"github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgtype"
) )
const countUsers = `-- name: CountUsers :one
SELECT count(*)
FROM users u
WHERE
($1::boolean IS NULL OR u.is_deleted = $1::boolean)
AND (
$2::uuid[] IS NULL OR
EXISTS (
SELECT 1 FROM user_roles ur2
WHERE ur2.user_id = u.id
AND ur2.role_id = ANY($2::uuid[])
)
)
AND ($3::text IS NULL OR u.auth_provider = $3::text)
AND ($4::timestamp IS NULL OR u.created_at >= $4::timestamp)
AND ($5::timestamp IS NULL OR u.created_at <= $5::timestamp)
AND (
$6::text IS NULL OR
u.id::text ILIKE '%' || $6::text || '%' OR
u.email ILIKE '%' || $6::text || '%' OR
EXISTS (
SELECT 1 FROM user_profiles p
WHERE p.user_id = u.id
AND (
p.full_name ILIKE '%' || $6::text || '%' OR
p.phone ILIKE '%' || $6::text || '%'
)
)
)
`
type CountUsersParams struct {
IsDeleted pgtype.Bool `json:"is_deleted"`
RoleIds []pgtype.UUID `json:"role_ids"`
AuthProvider pgtype.Text `json:"auth_provider"`
CreatedFrom pgtype.Timestamp `json:"created_from"`
CreatedTo pgtype.Timestamp `json:"created_to"`
SearchText pgtype.Text `json:"search_text"`
}
func (q *Queries) CountUsers(ctx context.Context, arg CountUsersParams) (int64, error) {
row := q.db.QueryRow(ctx, countUsers,
arg.IsDeleted,
arg.RoleIds,
arg.AuthProvider,
arg.CreatedFrom,
arg.CreatedTo,
arg.SearchText,
)
var count int64
err := row.Scan(&count)
return count, err
}
const createUserProfile = `-- name: CreateUserProfile :one const createUserProfile = `-- name: CreateUserProfile :one
INSERT INTO user_profiles ( INSERT INTO user_profiles (
user_id, user_id,
@@ -304,11 +358,12 @@ SELECT
u.email, u.email,
u.password_hash, u.password_hash,
u.token_version, u.token_version,
u.google_id,
u.auth_provider,
u.refresh_token, u.refresh_token,
u.is_deleted, u.is_deleted,
u.created_at, u.created_at,
u.updated_at, u.updated_at,
( (
SELECT json_build_object( SELECT json_build_object(
'display_name', p.display_name, 'display_name', p.display_name,
@@ -323,7 +378,6 @@ SELECT
FROM user_profiles p FROM user_profiles p
WHERE p.user_id = u.id WHERE p.user_id = u.id
) AS profile, ) AS profile,
( (
SELECT COALESCE( SELECT COALESCE(
json_agg(json_build_object('id', r.id, 'name', r.name)), json_agg(json_build_object('id', r.id, 'name', r.name)),
@@ -333,67 +387,62 @@ SELECT
JOIN roles r ON ur.role_id = r.id JOIN roles r ON ur.role_id = r.id
WHERE ur.user_id = u.id WHERE ur.user_id = u.id
) AS roles ) AS roles
FROM users u FROM users u
WHERE WHERE
($1::uuid IS NULL OR u.id > $1::uuid) ($1::boolean IS NULL OR u.is_deleted = $1::boolean)
AND ($2::boolean IS NULL OR u.is_deleted = $2::boolean)
AND ( AND (
$3::uuid[] IS NULL OR $2::uuid[] IS NULL OR
EXISTS ( EXISTS (
SELECT 1 FROM user_roles ur2 SELECT 1 FROM user_roles ur2
WHERE ur2.user_id = u.id WHERE ur2.user_id = u.id
AND ur2.role_id = ANY($3::uuid[]) AND ur2.role_id = ANY($2::uuid[])
) )
) )
AND ($3::text IS NULL OR u.auth_provider = $3::text)
AND ($4::uuid IS NULL OR u.id = $4::uuid) AND ($4::timestamp IS NULL OR u.created_at >= $4::timestamp)
AND ($5::timestamp IS NULL OR u.created_at <= $5::timestamp)
AND ( AND (
$5::text IS NULL OR $6::text IS NULL OR
u.email ILIKE '%' || $5::text || '%' u.id::text ILIKE '%' || $6::text || '%' OR
u.email ILIKE '%' || $6::text || '%' OR
EXISTS (
SELECT 1 FROM user_profiles p
WHERE p.user_id = u.id
AND (
p.full_name ILIKE '%' || $6::text || '%' OR
p.phone ILIKE '%' || $6::text || '%'
)
)
) )
ORDER BY ORDER BY
-- id CASE WHEN $7 = 'id' AND $8 = 'asc' THEN u.id END ASC,
CASE CASE WHEN $7 = 'id' AND $8 = 'desc' THEN u.id END DESC,
WHEN $6 = 'id' AND $7 = 'asc' THEN id CASE WHEN $7 = 'created_at' AND $8 = 'asc' THEN u.created_at END ASC,
END ASC, CASE WHEN $7 = 'created_at' AND $8 = 'desc' THEN u.created_at END DESC,
CASE CASE WHEN $7 = 'updated_at' AND $8 = 'asc' THEN u.updated_at END ASC,
WHEN $6 = 'id' AND $7 = 'desc' THEN id CASE WHEN $7 = 'updated_at' AND $8 = 'desc' THEN u.updated_at END DESC,
END DESC, CASE WHEN $7 = 'email' AND $8 = 'asc' THEN u.email END ASC,
-- created_at CASE WHEN $7 = 'email' AND $8 = 'desc' THEN u.email END DESC,
CASE CASE WHEN $7 = 'is_deleted' AND $8 = 'asc' THEN u.is_deleted END ASC,
WHEN $6 = 'created_at' AND $7 = 'asc' THEN u.created_at CASE WHEN $7 = 'is_deleted' AND $8 = 'desc' THEN u.is_deleted END DESC,
END ASC, CASE WHEN $7 = 'auth_provider' AND $8 = 'asc' THEN u.auth_provider END ASC,
CASE CASE WHEN $7 = 'auth_provider' AND $8 = 'desc' THEN u.auth_provider END DESC,
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 u.id ASC
LIMIT $10
LIMIT $8 OFFSET $9
` `
type SearchUsersParams struct { type SearchUsersParams struct {
Cursor pgtype.UUID `json:"cursor"` IsDeleted pgtype.Bool `json:"is_deleted"`
IsDeleted pgtype.Bool `json:"is_deleted"` RoleIds []pgtype.UUID `json:"role_ids"`
RoleIds []pgtype.UUID `json:"role_ids"` AuthProvider pgtype.Text `json:"auth_provider"`
SearchID pgtype.UUID `json:"search_id"` CreatedFrom pgtype.Timestamp `json:"created_from"`
SearchText pgtype.Text `json:"search_text"` CreatedTo pgtype.Timestamp `json:"created_to"`
Sort interface{} `json:"sort"` SearchText pgtype.Text `json:"search_text"`
Order interface{} `json:"order"` Sort interface{} `json:"sort"`
Limit int32 `json:"limit"` Order interface{} `json:"order"`
Offset int32 `json:"offset"`
Limit int32 `json:"limit"`
} }
type SearchUsersRow struct { type SearchUsersRow struct {
@@ -401,6 +450,8 @@ type SearchUsersRow struct {
Email string `json:"email"` Email string `json:"email"`
PasswordHash pgtype.Text `json:"password_hash"` PasswordHash pgtype.Text `json:"password_hash"`
TokenVersion int32 `json:"token_version"` TokenVersion int32 `json:"token_version"`
GoogleID pgtype.Text `json:"google_id"`
AuthProvider string `json:"auth_provider"`
RefreshToken pgtype.Text `json:"refresh_token"` RefreshToken pgtype.Text `json:"refresh_token"`
IsDeleted bool `json:"is_deleted"` IsDeleted bool `json:"is_deleted"`
CreatedAt pgtype.Timestamptz `json:"created_at"` CreatedAt pgtype.Timestamptz `json:"created_at"`
@@ -411,13 +462,15 @@ type SearchUsersRow struct {
func (q *Queries) SearchUsers(ctx context.Context, arg SearchUsersParams) ([]SearchUsersRow, error) { func (q *Queries) SearchUsers(ctx context.Context, arg SearchUsersParams) ([]SearchUsersRow, error) {
rows, err := q.db.Query(ctx, searchUsers, rows, err := q.db.Query(ctx, searchUsers,
arg.Cursor,
arg.IsDeleted, arg.IsDeleted,
arg.RoleIds, arg.RoleIds,
arg.SearchID, arg.AuthProvider,
arg.CreatedFrom,
arg.CreatedTo,
arg.SearchText, arg.SearchText,
arg.Sort, arg.Sort,
arg.Order, arg.Order,
arg.Offset,
arg.Limit, arg.Limit,
) )
if err != nil { if err != nil {
@@ -432,6 +485,8 @@ func (q *Queries) SearchUsers(ctx context.Context, arg SearchUsersParams) ([]Sea
&i.Email, &i.Email,
&i.PasswordHash, &i.PasswordHash,
&i.TokenVersion, &i.TokenVersion,
&i.GoogleID,
&i.AuthProvider,
&i.RefreshToken, &i.RefreshToken,
&i.IsDeleted, &i.IsDeleted,
&i.CreatedAt, &i.CreatedAt,

View File

@@ -18,6 +18,7 @@ type MediaRepository interface {
GetByID(ctx context.Context, id pgtype.UUID) (*models.MediaEntity, error) GetByID(ctx context.Context, id pgtype.UUID) (*models.MediaEntity, error)
GetByUserID(ctx context.Context, userId pgtype.UUID) ([]*models.MediaEntity, error) GetByUserID(ctx context.Context, userId pgtype.UUID) ([]*models.MediaEntity, error)
Search(ctx context.Context, params sqlc.SearchMediasParams) ([]*models.MediaEntity, error) Search(ctx context.Context, params sqlc.SearchMediasParams) ([]*models.MediaEntity, error)
Count(ctx context.Context, params sqlc.CountMediasParams) (int64, error)
Delete(ctx context.Context, id pgtype.UUID) error Delete(ctx context.Context, id pgtype.UUID) error
Create(ctx context.Context, params sqlc.CreateMediaParams) (*models.MediaEntity, error) Create(ctx context.Context, params sqlc.CreateMediaParams) (*models.MediaEntity, error)
} }
@@ -85,6 +86,7 @@ func (r *mediaRepository) GetByID(ctx context.Context, id pgtype.UUID) (*models.
var media models.MediaEntity var media models.MediaEntity
err := r.c.Get(ctx, cacheId, &media) err := r.c.Get(ctx, cacheId, &media)
if err == nil { if err == nil {
_ = r.c.Set(ctx, cacheId, media, constants.NormalCacheDuration)
return &media, nil return &media, nil
} }
@@ -118,11 +120,10 @@ func (r *mediaRepository) Create(ctx context.Context, params sqlc.CreateMediaPar
go func() { go func() {
bgCtx := context.Background() bgCtx := context.Background()
_ = r.c.DelByPattern(bgCtx, "media:target*")
_ = r.c.DelByPattern(bgCtx, "media:userId:*")
_ = r.c.DelByPattern(bgCtx, "media:search*") _ = r.c.DelByPattern(bgCtx, "media:search*")
_ = r.c.DelByPattern(bgCtx, "media:count*")
}() }()
media := models.MediaEntity{ media := models.MediaEntity{
ID: convert.UUIDToString(row.ID), ID: convert.UUIDToString(row.ID),
UserID: convert.UUIDToString(row.UserID), UserID: convert.UUIDToString(row.UserID),
@@ -155,7 +156,16 @@ func (r *mediaRepository) Search(ctx context.Context, params sqlc.SearchMediasPa
queryKey := r.generateQueryKey("media:search", params) queryKey := r.generateQueryKey("media:search", params)
var cachedIDs []string var cachedIDs []string
if err := r.c.Get(ctx, queryKey, &cachedIDs); err == nil && len(cachedIDs) > 0 { if err := r.c.Get(ctx, queryKey, &cachedIDs); err == nil && len(cachedIDs) > 0 {
return r.getByIDsWithFallback(ctx, cachedIDs) listItem, err := r.getByIDsWithFallback(ctx, cachedIDs)
if err != nil {
return nil, err
}
newCachedIDs := make([]string, len(listItem))
for i, media := range listItem {
newCachedIDs[i] = media.ID
}
_ = r.c.Set(ctx, queryKey, newCachedIDs, constants.ListCacheDuration)
return listItem, err
} }
rows, err := r.q.SearchMedias(ctx, params) rows, err := r.q.SearchMedias(ctx, params)
@@ -195,11 +205,35 @@ func (r *mediaRepository) Search(ctx context.Context, params sqlc.SearchMediasPa
return medias, nil return medias, nil
} }
func (r *mediaRepository) Count(ctx context.Context, params sqlc.CountMediasParams) (int64, error) {
queryKey := r.generateQueryKey("media:count", params)
var count int64
if err := r.c.Get(ctx, queryKey, &count); err == nil {
_ = r.c.Set(ctx, queryKey, count, constants.ListCacheDuration)
return count, nil
}
count, err := r.q.CountMedias(ctx, params)
if err != nil {
return 0, err
}
_ = r.c.Set(ctx, queryKey, count, constants.ListCacheDuration)
return count, nil
}
func (r *mediaRepository) GetByUserID(ctx context.Context, userId pgtype.UUID) ([]*models.MediaEntity, error) { func (r *mediaRepository) GetByUserID(ctx context.Context, userId pgtype.UUID) ([]*models.MediaEntity, error) {
queryKey := fmt.Sprintf("media:userId:%s", convert.UUIDToString(userId)) queryKey := fmt.Sprintf("media:userId:%s", convert.UUIDToString(userId))
var cachedIDs []string var cachedIDs []string
if err := r.c.Get(ctx, queryKey, &cachedIDs); err == nil && len(cachedIDs) > 0 { if err := r.c.Get(ctx, queryKey, &cachedIDs); err == nil && len(cachedIDs) > 0 {
return r.getByIDsWithFallback(ctx, cachedIDs) listItem, err := r.getByIDsWithFallback(ctx, cachedIDs)
if err != nil {
return nil, err
}
newCachedIDs := make([]string, len(listItem))
for i, media := range listItem {
newCachedIDs[i] = media.ID
}
_ = r.c.Set(ctx, queryKey, newCachedIDs, constants.ListCacheDuration)
return listItem, nil
} }
rows, err := r.q.GetMediasByUserID(ctx, userId) rows, err := r.q.GetMediasByUserID(ctx, userId)

View File

@@ -97,6 +97,7 @@ func (r *roleRepository) GetByID(ctx context.Context, id pgtype.UUID) (*models.R
var role models.RoleEntity var role models.RoleEntity
err := r.c.Get(ctx, cacheId, &role) err := r.c.Get(ctx, cacheId, &role)
if err == nil { if err == nil {
_ = r.c.Set(ctx, cacheId, role, constants.NormalCacheDuration)
return &role, nil return &role, nil
} }
@@ -122,6 +123,7 @@ func (r *roleRepository) GetByname(ctx context.Context, name string) (*models.Ro
var role models.RoleEntity var role models.RoleEntity
err := r.c.Get(ctx, cacheId, &role) err := r.c.Get(ctx, cacheId, &role)
if err == nil { if err == nil {
_ = r.c.Set(ctx, cacheId, role, constants.NormalCacheDuration)
return &role, nil return &role, nil
} }
row, err := r.q.GetRoleByName(ctx, name) row, err := r.q.GetRoleByName(ctx, name)
@@ -146,6 +148,11 @@ func (r *roleRepository) Create(ctx context.Context, name string) (*models.RoleE
if err != nil { if err != nil {
return nil, err return nil, err
} }
go func() {
bgCtx := context.Background()
_ = r.c.DelByPattern(bgCtx, "role:all*")
}()
role := models.RoleEntity{ role := models.RoleEntity{
ID: convert.UUIDToString(row.ID), ID: convert.UUIDToString(row.ID),
Name: row.Name, Name: row.Name,
@@ -183,24 +190,52 @@ func (r *roleRepository) Update(ctx context.Context, params sqlc.UpdateRoleParam
} }
func (r *roleRepository) All(ctx context.Context) ([]*models.RoleEntity, error) { func (r *roleRepository) All(ctx context.Context) ([]*models.RoleEntity, error) {
queryKey := "role:all"
var cachedIDs []string
if err := r.c.Get(ctx, queryKey, &cachedIDs); err == nil && len(cachedIDs) > 0 {
listItem, err := r.getByIDsWithFallback(ctx, cachedIDs)
if err != nil {
return nil, err
}
newCachedIDs := make([]string, len(listItem))
for i, media := range listItem {
newCachedIDs[i] = media.ID
}
_ = r.c.Set(ctx, queryKey, newCachedIDs, constants.ListCacheDuration)
return listItem, err
}
rows, err := r.q.GetRoles(ctx) rows, err := r.q.GetRoles(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
var roles []*models.RoleEntity
var ids []string
roleToCache := make(map[string]any)
var users []*models.RoleEntity
for _, row := range rows { for _, row := range rows {
user := &models.RoleEntity{ role := &models.RoleEntity{
ID: convert.UUIDToString(row.ID), ID: convert.UUIDToString(row.ID),
Name: row.Name, Name: row.Name,
IsDeleted: row.IsDeleted, IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt), CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt), UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
} }
users = append(users, user) ids = append(ids, role.ID)
roles = append(roles, role)
roleToCache[fmt.Sprintf("role:id:%s", role.ID)] = role
} }
return users, nil if len(roleToCache) > 0 {
_ = r.c.MSet(ctx, roleToCache, constants.NormalCacheDuration)
}
if len(ids) > 0 {
_ = r.c.Set(ctx, queryKey, ids, constants.ListCacheDuration)
}
return roles, nil
} }
func (r *roleRepository) Delete(ctx context.Context, id pgtype.UUID) error { func (r *roleRepository) Delete(ctx context.Context, id pgtype.UUID) error {

View File

@@ -20,6 +20,7 @@ type UserRepository interface {
GetByIDWithoutDeleted(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) GetByEmail(ctx context.Context, email string) (*models.UserEntity, error)
Search(ctx context.Context, params sqlc.SearchUsersParams) ([]*models.UserEntity, error) Search(ctx context.Context, params sqlc.SearchUsersParams) ([]*models.UserEntity, error)
Count(ctx context.Context, params sqlc.CountUsersParams) (int64, error)
UpsertUser(ctx context.Context, params sqlc.UpsertUserParams) (*models.UserEntity, error) UpsertUser(ctx context.Context, params sqlc.UpsertUserParams) (*models.UserEntity, error)
CreateProfile(ctx context.Context, params sqlc.CreateUserProfileParams) (*models.UserProfileSimple, error) CreateProfile(ctx context.Context, params sqlc.CreateUserProfileParams) (*models.UserProfileSimple, error)
UpdateProfile(ctx context.Context, params sqlc.UpdateUserProfileParams) (*models.UserEntity, error) UpdateProfile(ctx context.Context, params sqlc.UpdateUserProfileParams) (*models.UserEntity, error)
@@ -205,9 +206,8 @@ func (r *userRepository) UpsertUser(ctx context.Context, params sqlc.UpsertUserP
} }
go func() { go func() {
bgCtx := context.Background() bgCtx := context.Background()
_ = r.c.DelByPattern(bgCtx, "user:all*")
_ = r.c.DelByPattern(bgCtx, "user:search*") _ = r.c.DelByPattern(bgCtx, "user:search*")
_ = r.c.DelByPattern(bgCtx, "user:count*")
}() }()
return &models.UserEntity{ return &models.UserEntity{
@@ -320,6 +320,22 @@ func (r *userRepository) Search(ctx context.Context, params sqlc.SearchUsersPara
return users, nil return users, nil
} }
func (r *userRepository) Count(ctx context.Context, params sqlc.CountUsersParams) (int64, error) {
queryKey := r.generateQueryKey("user:count", params)
var count int64
if err := r.c.Get(ctx, queryKey, &count); err == nil {
return count, nil
}
count, err := r.q.CountUsers(ctx, params)
if err != nil {
return 0, err
}
_ = r.c.Set(ctx, queryKey, count, constants.NormalCacheDuration)
return count, nil
}
func (r *userRepository) Delete(ctx context.Context, id pgtype.UUID) error { func (r *userRepository) Delete(ctx context.Context, id pgtype.UUID) error {
user, err := r.GetByID(ctx, id) user, err := r.GetByID(ctx, id)
if err != nil { if err != nil {

View File

@@ -21,9 +21,10 @@ import (
"strings" "strings"
"github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3"
"github.com/rs/zerolog/log"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgtype"
"github.com/rs/zerolog/log"
"golang.org/x/sync/errgroup"
) )
type MediaService interface { type MediaService interface {
@@ -111,60 +112,86 @@ func (m *mediaService) GetMediaByUserID(ctx context.Context, id string) ([]*resp
return models.MediaEntitiesToResponse(medias), nil return models.MediaEntitiesToResponse(medias), nil
} }
func (m *mediaService) SearchMedia(ctx context.Context, dto *request.SearchMediaDto) (*response.PaginatedResponse, error) { func (m *mediaService) fillSearchArgs(arg *sqlc.SearchMediasParams, dto *request.SearchMediaDto) {
arg := sqlc.SearchMediasParams{
Limit: int32(dto.Limit + 1),
}
if dto.Sort != "" { if dto.Sort != "" {
arg.Sort = pgtype.Text{String: dto.Sort, Valid: true} arg.Sort = pgtype.Text{String: dto.Sort, Valid: true}
} else { } else {
arg.Sort = pgtype.Text{String: "id", Valid: true} arg.Sort = pgtype.Text{String: "id", Valid: true}
} }
if dto.Order != "" { arg.Order = pgtype.Text{String: "asc", Valid: true}
arg.Order = pgtype.Text{String: dto.Order, Valid: true} if dto.Order == "desc" {
} else { arg.Order = pgtype.Text{String: "desc", Valid: true}
arg.Order = pgtype.Text{String: "asc", Valid: true}
} }
if dto.Cursor != "" { if dto.MimeType != "" {
pgID, err := convert.StringToUUID(dto.Cursor) arg.MimeType = pgtype.Text{String: dto.MimeType, Valid: true}
if err != nil { }
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid cursor format")
if dto.MaxSize != nil {
arg.MaxSize = pgtype.Int8{Int64: *dto.MaxSize, Valid: true}
}
if dto.MinSize != nil {
arg.MinSize = pgtype.Int8{Int64: *dto.MinSize, Valid: true}
}
if len(dto.UserIDs) > 0 {
for _, id := range dto.UserIDs {
if u, err := convert.StringToUUID(id); err == nil {
arg.UserIds = append(arg.UserIds, u)
}
} }
arg.Cursor = pgID
} }
if dto.Search != "" { if dto.Search != "" {
arg.SearchText = pgtype.Text{String: dto.Search, Valid: true} arg.SearchText = pgtype.Text{String: dto.Search, Valid: true}
} }
}
rows, err := m.mediaRepo.Search(ctx, arg) func (m *mediaService) SearchMedia(ctx context.Context, dto *request.SearchMediaDto) (*response.PaginatedResponse, error) {
if err != nil { if dto.Page < 1 {
dto.Page = 1
}
offset := (dto.Page - 1) * dto.Limit
arg := sqlc.SearchMediasParams{
Limit: int32(dto.Limit),
Offset: int32(offset),
}
m.fillSearchArgs(&arg, dto)
var rows []*models.MediaEntity
var totalRecords int64
g, gCtx := errgroup.WithContext(ctx)
g.Go(func() error {
var err error
rows, err = m.mediaRepo.Search(gCtx, arg)
return err
})
g.Go(func() error {
countArg := sqlc.CountMediasParams{
UserIds: arg.UserIds,
MimeType: arg.MimeType,
MinSize: arg.MinSize,
MaxSize: arg.MaxSize,
SearchText: arg.SearchText,
}
var err error
totalRecords, err = m.mediaRepo.Count(gCtx, countArg)
return err
})
if err := g.Wait(); err != nil {
return nil, err return nil, err
} }
hasMore := false return response.BuildPaginatedResponse(rows, totalRecords, dto.Page, dto.Limit), nil
var nextCursor string
if len(rows) > dto.Limit {
hasMore = true
nextCursor = rows[dto.Limit-1].ID
rows = rows[:dto.Limit]
}
res := &response.PaginatedResponse{
Data: rows,
Status: true,
Message: "",
}
res.Pagination.HasMore = hasMore
res.Pagination.NextCursor = nextCursor
return res, nil
} }
func (m *mediaService) UploadServerSide(ctx context.Context, userId string, fileHeader *multipart.FileHeader) (*response.MediaResponse, error) { func (m *mediaService) UploadServerSide(ctx context.Context, userId string, fileHeader *multipart.FileHeader) (*response.MediaResponse, error) {
userIdUUID, err := convert.StringToUUID(userId) userIdUUID, err := convert.StringToUUID(userId)
if err != nil { if err != nil {

View File

@@ -12,6 +12,7 @@ import (
"github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3"
"github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgtype"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
"golang.org/x/sync/errgroup"
) )
type UserService interface { type UserService interface {
@@ -208,81 +209,90 @@ func (u *userService) RestoreUser(ctx context.Context, userId string) (*response
return user.ToResponse(), nil return user.ToResponse(), nil
} }
func (u *userService) SearchUser(ctx context.Context, dto *request.SearchUserDto) (*response.PaginatedResponse, error) { func (m *userService) fillSearchArgs(arg *sqlc.SearchUsersParams, dto *request.SearchUserDto) {
arg := sqlc.SearchUsersParams{
Limit: int32(dto.Limit + 1),
}
if dto.Sort != "" { if dto.Sort != "" {
arg.Sort = pgtype.Text{String: dto.Sort, Valid: true} arg.Sort = pgtype.Text{String: dto.Sort, Valid: true}
} else { } else {
arg.Sort = pgtype.Text{String: "id", Valid: true} arg.Sort = pgtype.Text{String: "id", Valid: true}
} }
if dto.Order != "" { arg.Order = pgtype.Text{String: "asc", Valid: true}
arg.Order = pgtype.Text{String: dto.Order, Valid: true} if dto.Order == "desc" {
} else { arg.Order = pgtype.Text{String: "desc", Valid: true}
arg.Order = pgtype.Text{String: "asc", Valid: true}
} }
if dto.Cursor != "" { if dto.AuthProvider != "" {
pgID, err := convert.StringToUUID(dto.Cursor) arg.AuthProvider = pgtype.Text{String: dto.AuthProvider, Valid: true}
if err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid cursor format")
}
arg.Cursor = pgID
} }
if dto.Search != "" { if dto.CreatedFrom != nil {
pgID, err := convert.StringToUUID(dto.Search) arg.CreatedFrom = pgtype.Timestamp{Time: *dto.CreatedFrom, Valid: true}
if err == nil { }
arg.SearchID = pgID
} else { if dto.CreatedTo != nil {
arg.SearchText = pgtype.Text{String: dto.Search, Valid: true} arg.CreatedTo = pgtype.Timestamp{Time: *dto.CreatedTo, Valid: true}
}
} }
if dto.IsDeleted != nil { if dto.IsDeleted != nil {
arg.IsDeleted = pgtype.Bool{Bool: *dto.IsDeleted, Valid: true} arg.IsDeleted = pgtype.Bool{Bool: *dto.IsDeleted, Valid: true}
} }
if len(dto.RoleIDs) > 0 { if len(dto.RoleIDs) > 0 {
var pgRoleIDs []pgtype.UUID for _, id := range dto.RoleIDs {
for _, idStr := range dto.RoleIDs { if u, err := convert.StringToUUID(id); err == nil {
pgID, err := convert.StringToUUID(idStr) arg.RoleIds = append(arg.RoleIds, u)
if err != nil {
continue
} }
pgRoleIDs = append(pgRoleIDs, pgID)
} }
arg.RoleIds = pgRoleIDs
} }
rows, err := u.userRepo.Search(ctx, arg) if dto.Search != "" {
if err != nil { arg.SearchText = pgtype.Text{String: dto.Search, Valid: true}
}
}
func (u *userService) SearchUser(ctx context.Context, dto *request.SearchUserDto) (*response.PaginatedResponse, error) {
if dto.Page < 1 {
dto.Page = 1
}
offset := (dto.Page - 1) * dto.Limit
arg := sqlc.SearchUsersParams{
Limit: int32(dto.Limit),
Offset: int32(offset),
}
u.fillSearchArgs(&arg, dto)
var rows []*models.UserEntity
var totalRecords int64
g, gCtx := errgroup.WithContext(ctx)
g.Go(func() error {
var err error
rows, err = u.userRepo.Search(gCtx, arg)
return err
})
g.Go(func() error {
countArg := sqlc.CountUsersParams{
RoleIds: arg.RoleIds,
AuthProvider: arg.AuthProvider,
CreatedFrom: arg.CreatedFrom,
CreatedTo: arg.CreatedTo,
IsDeleted: arg.IsDeleted,
SearchText: arg.SearchText,
}
var err error
totalRecords, err = u.userRepo.Count(gCtx, countArg)
return err
})
if err := g.Wait(); err != nil {
return nil, err return nil, err
} }
hasMore := false return response.BuildPaginatedResponse(rows, totalRecords, dto.Page, dto.Limit), nil
var nextCursor string
if len(rows) > dto.Limit {
hasMore = true
nextCursor = rows[dto.Limit-1].ID
rows = rows[:dto.Limit]
}
users := models.UsersEntityToResponse(rows)
res := &response.PaginatedResponse{
Data: users,
Status: true,
Message: "",
}
res.Pagination.HasMore = hasMore
res.Pagination.NextCursor = nextCursor
return res, nil
} }
func (u *userService) GetUserByID(ctx context.Context, userId string) (*response.UserResponse, error) { func (u *userService) GetUserByID(ctx context.Context, userId string) (*response.UserResponse, error) {