7d8e82c69f
Ship the Ops backstage 对象存储 page (browse / upload / move / delete / folder ops) backed by a new object-storage service + handler, with the shared storage client extended with List/Stat/Copy/Usage/DownloadURL so both MinIO and Aliyun providers cover the new surface. Add ForcePathStyle to the object-storage config and treat r2/s3/custom_s3 as external providers so Cloudflare R2 and other S3-compatibles can share the MinIO client path without spinning up the bundled MinIO. Also add CSV import/export + template download to the site-domain mappings page, raise gin MaxMultipartMemory to 8 MiB for the new multipart endpoints, register Ops swagger routes, and extend the descriptions-parity test to cover the ops router. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
251 lines
6.7 KiB
Go
251 lines
6.7 KiB
Go
package repository
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
|
|
"github.com/geo-platform/tenant-api/internal/ops/domain"
|
|
)
|
|
|
|
var ErrSiteDomainMappingNotFound = errors.New("site domain mapping not found")
|
|
|
|
type SiteDomainMappingRepository struct {
|
|
pool *pgxpool.Pool
|
|
}
|
|
|
|
func NewSiteDomainMappingRepository(pool *pgxpool.Pool) *SiteDomainMappingRepository {
|
|
return &SiteDomainMappingRepository{pool: pool}
|
|
}
|
|
|
|
const siteDomainMappingColumns = `id, registrable_domain, site_key, site_name, is_active, created_at, updated_at`
|
|
|
|
type SiteDomainMappingFilter struct {
|
|
Keyword string
|
|
IsActive *bool
|
|
Limit int
|
|
Offset int
|
|
}
|
|
|
|
type CreateSiteDomainMappingInput struct {
|
|
RegistrableDomain string
|
|
SiteKey string
|
|
SiteName string
|
|
IsActive bool
|
|
}
|
|
|
|
type UpdateSiteDomainMappingInput struct {
|
|
RegistrableDomain string
|
|
SiteKey string
|
|
SiteName string
|
|
IsActive bool
|
|
}
|
|
|
|
type UpsertSiteDomainMappingInput struct {
|
|
RegistrableDomain string
|
|
SiteKey string
|
|
SiteName string
|
|
IsActive bool
|
|
}
|
|
|
|
type UpsertSiteDomainMappingResult struct {
|
|
Item *domain.SiteDomainMapping
|
|
Created bool
|
|
}
|
|
|
|
func scanSiteDomainMapping(row pgx.Row) (*domain.SiteDomainMapping, error) {
|
|
var item domain.SiteDomainMapping
|
|
err := row.Scan(
|
|
&item.ID,
|
|
&item.RegistrableDomain,
|
|
&item.SiteKey,
|
|
&item.SiteName,
|
|
&item.IsActive,
|
|
&item.CreatedAt,
|
|
&item.UpdatedAt,
|
|
)
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return nil, ErrSiteDomainMappingNotFound
|
|
}
|
|
return nil, err
|
|
}
|
|
return &item, nil
|
|
}
|
|
|
|
func scanSiteDomainMappingUpsert(row pgx.Row) (*UpsertSiteDomainMappingResult, error) {
|
|
var item domain.SiteDomainMapping
|
|
var created bool
|
|
err := row.Scan(
|
|
&item.ID,
|
|
&item.RegistrableDomain,
|
|
&item.SiteKey,
|
|
&item.SiteName,
|
|
&item.IsActive,
|
|
&item.CreatedAt,
|
|
&item.UpdatedAt,
|
|
&created,
|
|
)
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return nil, ErrSiteDomainMappingNotFound
|
|
}
|
|
return nil, err
|
|
}
|
|
return &UpsertSiteDomainMappingResult{Item: &item, Created: created}, nil
|
|
}
|
|
|
|
func buildSiteDomainMappingWhere(f SiteDomainMappingFilter) (string, []any) {
|
|
args := []any{}
|
|
where := "1=1"
|
|
|
|
if f.Keyword != "" {
|
|
args = append(args, "%"+f.Keyword+"%")
|
|
where += fmt.Sprintf(" AND (registrable_domain ILIKE $%d OR site_key ILIKE $%d OR site_name ILIKE $%d)",
|
|
len(args), len(args), len(args))
|
|
}
|
|
if f.IsActive != nil {
|
|
args = append(args, *f.IsActive)
|
|
where += fmt.Sprintf(" AND is_active = $%d", len(args))
|
|
}
|
|
|
|
return where, args
|
|
}
|
|
|
|
func (r *SiteDomainMappingRepository) List(ctx context.Context, f SiteDomainMappingFilter) ([]domain.SiteDomainMapping, int64, error) {
|
|
where, args := buildSiteDomainMappingWhere(f)
|
|
|
|
var total int64
|
|
if err := r.pool.QueryRow(ctx, "SELECT COUNT(*) FROM site_domain_mappings WHERE "+where, args...).Scan(&total); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
limit := f.Limit
|
|
if limit <= 0 || limit > 200 {
|
|
limit = 20
|
|
}
|
|
offset := f.Offset
|
|
if offset < 0 {
|
|
offset = 0
|
|
}
|
|
args = append(args, limit, offset)
|
|
limitArg := len(args) - 1
|
|
offsetArg := len(args)
|
|
|
|
rows, err := r.pool.Query(ctx, fmt.Sprintf(`
|
|
SELECT %s
|
|
FROM site_domain_mappings
|
|
WHERE %s
|
|
ORDER BY is_active DESC, registrable_domain ASC, id DESC
|
|
LIMIT $%d OFFSET $%d`, siteDomainMappingColumns, where, limitArg, offsetArg), args...)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
items := make([]domain.SiteDomainMapping, 0, limit)
|
|
for rows.Next() {
|
|
item, scanErr := scanSiteDomainMapping(rows)
|
|
if scanErr != nil {
|
|
return nil, 0, scanErr
|
|
}
|
|
items = append(items, *item)
|
|
}
|
|
return items, total, rows.Err()
|
|
}
|
|
|
|
func (r *SiteDomainMappingRepository) Export(ctx context.Context, f SiteDomainMappingFilter) ([]domain.SiteDomainMapping, error) {
|
|
where, args := buildSiteDomainMappingWhere(f)
|
|
limit := f.Limit
|
|
if limit <= 0 {
|
|
limit = 10000
|
|
}
|
|
args = append(args, limit)
|
|
limitArg := len(args)
|
|
|
|
rows, err := r.pool.Query(ctx, fmt.Sprintf(`
|
|
SELECT %s
|
|
FROM site_domain_mappings
|
|
WHERE %s
|
|
ORDER BY is_active DESC, registrable_domain ASC, id DESC
|
|
LIMIT $%d`, siteDomainMappingColumns, where, limitArg), args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
items := make([]domain.SiteDomainMapping, 0, limit)
|
|
for rows.Next() {
|
|
item, scanErr := scanSiteDomainMapping(rows)
|
|
if scanErr != nil {
|
|
return nil, scanErr
|
|
}
|
|
items = append(items, *item)
|
|
}
|
|
return items, rows.Err()
|
|
}
|
|
|
|
func (r *SiteDomainMappingRepository) GetByID(ctx context.Context, id int64) (*domain.SiteDomainMapping, error) {
|
|
return scanSiteDomainMapping(r.pool.QueryRow(ctx, `
|
|
SELECT `+siteDomainMappingColumns+`
|
|
FROM site_domain_mappings
|
|
WHERE id = $1`, id))
|
|
}
|
|
|
|
func (r *SiteDomainMappingRepository) Create(ctx context.Context, in CreateSiteDomainMappingInput) (*domain.SiteDomainMapping, error) {
|
|
return scanSiteDomainMapping(r.pool.QueryRow(ctx, `
|
|
INSERT INTO site_domain_mappings (registrable_domain, site_key, site_name, is_active)
|
|
VALUES ($1, $2, $3, $4)
|
|
RETURNING `+siteDomainMappingColumns,
|
|
in.RegistrableDomain, in.SiteKey, in.SiteName, in.IsActive))
|
|
}
|
|
|
|
func (r *SiteDomainMappingRepository) Update(ctx context.Context, id int64, in UpdateSiteDomainMappingInput) (*domain.SiteDomainMapping, error) {
|
|
return scanSiteDomainMapping(r.pool.QueryRow(ctx, `
|
|
UPDATE site_domain_mappings
|
|
SET registrable_domain = $1,
|
|
site_key = $2,
|
|
site_name = $3,
|
|
is_active = $4,
|
|
updated_at = NOW()
|
|
WHERE id = $5
|
|
RETURNING `+siteDomainMappingColumns,
|
|
in.RegistrableDomain, in.SiteKey, in.SiteName, in.IsActive, id))
|
|
}
|
|
|
|
func (r *SiteDomainMappingRepository) Upsert(ctx context.Context, in UpsertSiteDomainMappingInput) (*UpsertSiteDomainMappingResult, error) {
|
|
return scanSiteDomainMappingUpsert(r.pool.QueryRow(ctx, `
|
|
INSERT INTO site_domain_mappings (registrable_domain, site_key, site_name, is_active)
|
|
VALUES ($1, $2, $3, $4)
|
|
ON CONFLICT (registrable_domain) DO UPDATE
|
|
SET site_key = EXCLUDED.site_key,
|
|
site_name = EXCLUDED.site_name,
|
|
is_active = EXCLUDED.is_active,
|
|
updated_at = NOW()
|
|
RETURNING `+siteDomainMappingColumns+`, xmax = 0`,
|
|
in.RegistrableDomain, in.SiteKey, in.SiteName, in.IsActive))
|
|
}
|
|
|
|
func (r *SiteDomainMappingRepository) SetActive(ctx context.Context, id int64, active bool) (*domain.SiteDomainMapping, error) {
|
|
return scanSiteDomainMapping(r.pool.QueryRow(ctx, `
|
|
UPDATE site_domain_mappings
|
|
SET is_active = $1,
|
|
updated_at = NOW()
|
|
WHERE id = $2
|
|
RETURNING `+siteDomainMappingColumns, active, id))
|
|
}
|
|
|
|
func (r *SiteDomainMappingRepository) Delete(ctx context.Context, id int64) error {
|
|
cmd, err := r.pool.Exec(ctx, `DELETE FROM site_domain_mappings WHERE id = $1`, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if cmd.RowsAffected() == 0 {
|
|
return ErrSiteDomainMappingNotFound
|
|
}
|
|
return nil
|
|
}
|