Files

368 lines
10 KiB
Go

package repository
import (
"context"
"encoding/json"
"errors"
"fmt"
"time"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/geo-platform/tenant-api/internal/ops/domain"
)
var ErrDesktopBugReportNotFound = errors.New("desktop bug report not found")
type DesktopBugReportRepository struct {
pool *pgxpool.Pool
}
func NewDesktopBugReportRepository(pool *pgxpool.Pool) *DesktopBugReportRepository {
return &DesktopBugReportRepository{pool: pool}
}
type CreateDesktopBugReportInput struct {
ID string
ReportType string
Status string
Severity string
TenantID *int64
WorkspaceID *int64
UserID *int64
DesktopClientID *string
CrashGUID *string
CrashReportID *string
ProcessType *string
ElectronVersion *string
AppVersion *string
ProductName *string
Platform *string
OSArch *string
DeviceName *string
Title string
Description *string
DumpObjectKey *string
DumpFileName *string
DumpSizeBytes *int64
DumpSHA256 *string
FormFields []byte
RuntimeSnapshot []byte
AppLogTail *string
UploaderIP *string
UserAgent *string
OccurredAt time.Time
}
type DesktopBugReportFilter struct {
Keyword string
Status string
Severity string
ReportType string
Platform string
Process string
TenantID *int64
ClientID string
Limit int
Offset int
}
type UpdateDesktopBugReportStateInput struct {
Status string
Severity string
ResolutionNote *string
}
const desktopBugReportSelectColumns = `
br.id::text,
br.report_type,
br.status,
br.severity,
br.tenant_id,
t.name,
br.workspace_id,
w.name,
br.user_id,
u.name,
u.phone,
u.email,
br.desktop_client_id::text,
br.crash_guid,
br.crash_report_id,
br.process_type,
br.electron_version,
br.app_version,
br.product_name,
br.platform,
br.os_arch,
br.device_name,
br.title,
br.description,
br.dump_object_key,
br.dump_file_name,
br.dump_size_bytes,
br.dump_sha256,
br.form_fields,
br.runtime_snapshot,
br.app_log_tail,
br.resolution_note,
br.uploader_ip,
br.user_agent,
br.occurred_at,
br.resolved_at,
br.created_at,
br.updated_at`
const desktopBugReportFromClause = `
FROM desktop_bug_reports br
LEFT JOIN tenants t ON t.id = br.tenant_id
LEFT JOIN workspaces w ON w.id = br.workspace_id
LEFT JOIN users u ON u.id = br.user_id`
func (r *DesktopBugReportRepository) Create(ctx context.Context, in CreateDesktopBugReportInput) (*domain.DesktopBugReport, error) {
var id string
if err := r.pool.QueryRow(ctx, `
INSERT INTO desktop_bug_reports (
id, report_type, status, severity, tenant_id, workspace_id, user_id,
desktop_client_id, crash_guid, crash_report_id, process_type, electron_version,
app_version, product_name, platform, os_arch, device_name, title, description,
dump_object_key, dump_file_name, dump_size_bytes, dump_sha256, form_fields,
runtime_snapshot, app_log_tail, uploader_ip, user_agent, occurred_at
)
VALUES (
$1::uuid, $2, $3, $4, $5, $6, $7,
$8::uuid, $9, $10, $11, $12,
$13, $14, $15, $16, $17, $18, $19,
$20, $21, $22, $23, $24::jsonb,
$25::jsonb, $26, $27, $28, $29
)
RETURNING id::text`,
in.ID, in.ReportType, in.Status, in.Severity, in.TenantID, in.WorkspaceID, in.UserID,
in.DesktopClientID, in.CrashGUID, in.CrashReportID, in.ProcessType, in.ElectronVersion,
in.AppVersion, in.ProductName, in.Platform, in.OSArch, in.DeviceName, in.Title, in.Description,
in.DumpObjectKey, in.DumpFileName, in.DumpSizeBytes, in.DumpSHA256, string(defaultJSON(in.FormFields)),
string(defaultJSON(in.RuntimeSnapshot)), in.AppLogTail, in.UploaderIP, in.UserAgent, in.OccurredAt).Scan(&id); err != nil {
return nil, err
}
return r.GetByID(ctx, id)
}
func (r *DesktopBugReportRepository) List(ctx context.Context, f DesktopBugReportFilter) ([]domain.DesktopBugReport, int64, error) {
args := []any{}
where := "1=1"
if f.Keyword != "" {
args = append(args, "%"+f.Keyword+"%")
where += fmt.Sprintf(` AND (
br.id::text ILIKE $%d OR
COALESCE(br.title, '') ILIKE $%d OR
COALESCE(br.description, '') ILIKE $%d OR
COALESCE(br.crash_guid, '') ILIKE $%d OR
COALESCE(br.crash_report_id, '') ILIKE $%d OR
COALESCE(br.dump_object_key, '') ILIKE $%d OR
COALESCE(br.device_name, '') ILIKE $%d OR
COALESCE(t.name, '') ILIKE $%d OR
COALESCE(w.name, '') ILIKE $%d OR
COALESCE(u.name, '') ILIKE $%d OR
COALESCE(u.phone, '') ILIKE $%d OR
COALESCE(u.email, '') ILIKE $%d
)`, len(args), len(args), len(args), len(args), len(args), len(args), len(args), len(args), len(args), len(args), len(args), len(args))
}
if f.Status != "" {
args = append(args, f.Status)
where += fmt.Sprintf(" AND br.status = $%d", len(args))
}
if f.Severity != "" {
args = append(args, f.Severity)
where += fmt.Sprintf(" AND br.severity = $%d", len(args))
}
if f.ReportType != "" {
args = append(args, f.ReportType)
where += fmt.Sprintf(" AND br.report_type = $%d", len(args))
}
if f.Platform != "" {
args = append(args, f.Platform)
where += fmt.Sprintf(" AND br.platform = $%d", len(args))
}
if f.Process != "" {
args = append(args, f.Process)
where += fmt.Sprintf(" AND br.process_type = $%d", len(args))
}
if f.TenantID != nil {
args = append(args, *f.TenantID)
where += fmt.Sprintf(" AND br.tenant_id = $%d", len(args))
}
if f.ClientID != "" {
args = append(args, f.ClientID)
where += fmt.Sprintf(" AND br.desktop_client_id = $%d::uuid", len(args))
}
var total int64
if err := r.pool.QueryRow(ctx, "SELECT COUNT(*) "+desktopBugReportFromClause+" 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
%s
WHERE %s
ORDER BY br.created_at DESC, br.id DESC
LIMIT $%d OFFSET $%d`, desktopBugReportSelectColumns, desktopBugReportFromClause, where, limitArg, offsetArg), args...)
if err != nil {
return nil, 0, err
}
defer rows.Close()
items := make([]domain.DesktopBugReport, 0, limit)
for rows.Next() {
item, scanErr := scanDesktopBugReport(rows)
if scanErr != nil {
return nil, 0, scanErr
}
items = append(items, *item)
}
return items, total, rows.Err()
}
func (r *DesktopBugReportRepository) GetByID(ctx context.Context, id string) (*domain.DesktopBugReport, error) {
return scanDesktopBugReport(r.pool.QueryRow(ctx, `
SELECT `+desktopBugReportSelectColumns+`
`+desktopBugReportFromClause+`
WHERE br.id = $1::uuid`, id))
}
func (r *DesktopBugReportRepository) UpdateState(ctx context.Context, id string, in UpdateDesktopBugReportStateInput) (*domain.DesktopBugReport, error) {
var updatedID string
if err := r.pool.QueryRow(ctx, `
UPDATE desktop_bug_reports br
SET status = $2,
severity = $3,
resolution_note = $4,
resolved_at = CASE
WHEN $2 IN ('resolved', 'ignored') AND resolved_at IS NULL THEN NOW()
WHEN $2 NOT IN ('resolved', 'ignored') THEN NULL
ELSE resolved_at
END,
updated_at = NOW()
WHERE br.id = $1::uuid
RETURNING br.id::text`,
id, in.Status, in.Severity, in.ResolutionNote).Scan(&updatedID); err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, ErrDesktopBugReportNotFound
}
return nil, err
}
return r.GetByID(ctx, updatedID)
}
func scanDesktopBugReport(row pgx.Row) (*domain.DesktopBugReport, error) {
var item domain.DesktopBugReport
var tenantID, workspaceID, userID, dumpSize pgtype.Int8
var tenantName, workspaceName, userName, userPhone, userEmail pgtype.Text
var desktopClientID, crashGUID, crashReportID, processType, electronVersion, appVersion pgtype.Text
var productName, platform, osArch, deviceName, description, dumpObjectKey, dumpFileName pgtype.Text
var dumpSHA256, appLogTail, resolutionNote, uploaderIP, userAgent pgtype.Text
var formFields, runtimeSnapshot []byte
var resolvedAt pgtype.Timestamptz
err := row.Scan(
&item.ID,
&item.ReportType,
&item.Status,
&item.Severity,
&tenantID,
&tenantName,
&workspaceID,
&workspaceName,
&userID,
&userName,
&userPhone,
&userEmail,
&desktopClientID,
&crashGUID,
&crashReportID,
&processType,
&electronVersion,
&appVersion,
&productName,
&platform,
&osArch,
&deviceName,
&item.Title,
&description,
&dumpObjectKey,
&dumpFileName,
&dumpSize,
&dumpSHA256,
&formFields,
&runtimeSnapshot,
&appLogTail,
&resolutionNote,
&uploaderIP,
&userAgent,
&item.OccurredAt,
&resolvedAt,
&item.CreatedAt,
&item.UpdatedAt,
)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, ErrDesktopBugReportNotFound
}
return nil, err
}
item.TenantID = int64PtrFromPg(tenantID)
item.TenantName = stringPtrFromPg(tenantName)
item.WorkspaceID = int64PtrFromPg(workspaceID)
item.WorkspaceName = stringPtrFromPg(workspaceName)
item.UserID = int64PtrFromPg(userID)
item.UserName = stringPtrFromPg(userName)
item.UserPhone = stringPtrFromPg(userPhone)
item.UserEmail = stringPtrFromPg(userEmail)
item.DesktopClientID = stringPtrFromPg(desktopClientID)
item.CrashGUID = stringPtrFromPg(crashGUID)
item.CrashReportID = stringPtrFromPg(crashReportID)
item.ProcessType = stringPtrFromPg(processType)
item.ElectronVersion = stringPtrFromPg(electronVersion)
item.AppVersion = stringPtrFromPg(appVersion)
item.ProductName = stringPtrFromPg(productName)
item.Platform = stringPtrFromPg(platform)
item.OSArch = stringPtrFromPg(osArch)
item.DeviceName = stringPtrFromPg(deviceName)
item.Description = stringPtrFromPg(description)
item.DumpObjectKey = stringPtrFromPg(dumpObjectKey)
item.DumpFileName = stringPtrFromPg(dumpFileName)
item.DumpSizeBytes = int64PtrFromPg(dumpSize)
item.DumpSHA256 = stringPtrFromPg(dumpSHA256)
item.FormFields = json.RawMessage(defaultJSON(formFields))
item.RuntimeSnapshot = json.RawMessage(defaultJSON(runtimeSnapshot))
item.AppLogTail = stringPtrFromPg(appLogTail)
item.ResolutionNote = stringPtrFromPg(resolutionNote)
item.UploaderIP = stringPtrFromPg(uploaderIP)
item.UserAgent = stringPtrFromPg(userAgent)
item.ResolvedAt = timePtrFromPg(resolvedAt)
return &item, nil
}
func defaultJSON(value []byte) []byte {
if len(value) == 0 || !json.Valid(value) {
return []byte("{}")
}
return value
}