368 lines
10 KiB
Go
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
|
|
}
|