mirror of
https://github.com/J3vb/OwnCord.git
synced 2026-09-03 03:50:00 +03:00
refactor(server/db): delegate invites + attachments to dbgen (D2)
invites: CreateInvite, GetInvite, UseInviteAtomic, RevokeInvite, ListInvites. attachments: CreateAttachment, GetAttachmentByID, GetAttachmentWithChannel, DeleteOrphanedAttachments. Added ptrI64toI / ptrItoI64 helpers for the *int64<->*int narrowing (invite max_uses, attachment width/height). LinkAttachmentsToMessage and GetAttachmentsByMessageIDs keep raw SQL (variable-length IN() lists sqlc can't express). Behavior and signatures unchanged. Verified: go build ./...; go test ./db (Invite, Attachment). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA
This commit is contained in:
@@ -5,6 +5,8 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/owncord/server/db/dbgen"
|
||||
)
|
||||
|
||||
// Attachment represents a row in the attachments table.
|
||||
@@ -32,11 +34,16 @@ type AttachmentAccess struct {
|
||||
// uploaderID records who uploaded the file for ownership checks on unlinked files.
|
||||
// width and height are optional image dimensions (pass nil for non-image files).
|
||||
func (d *DB) CreateAttachment(id string, uploaderID int64, filename, storedAs, mimeType string, size int64, width, height *int) error {
|
||||
_, err := d.sqlDB.Exec(
|
||||
`INSERT INTO attachments (id, uploader_id, filename, stored_as, mime_type, size, width, height) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
id, uploaderID, filename, storedAs, mimeType, size, width, height,
|
||||
)
|
||||
if err != nil {
|
||||
if err := d.q.CreateAttachment(dbCtx(), dbgen.CreateAttachmentParams{
|
||||
ID: id,
|
||||
UploaderID: &uploaderID,
|
||||
Filename: filename,
|
||||
StoredAs: storedAs,
|
||||
MimeType: mimeType,
|
||||
Size: size,
|
||||
Width: ptrItoI64(width),
|
||||
Height: ptrItoI64(height),
|
||||
}); err != nil {
|
||||
return fmt.Errorf("CreateAttachment: %w", err)
|
||||
}
|
||||
return nil
|
||||
@@ -44,19 +51,23 @@ func (d *DB) CreateAttachment(id string, uploaderID int64, filename, storedAs, m
|
||||
|
||||
// GetAttachmentByID returns the attachment with the given ID, or nil if not found.
|
||||
func (d *DB) GetAttachmentByID(id string) (*Attachment, error) {
|
||||
row := d.sqlDB.QueryRow(
|
||||
`SELECT id, message_id, filename, stored_as, mime_type, size, uploaded_at, uploader_id
|
||||
FROM attachments WHERE id = ?`, id,
|
||||
)
|
||||
a := &Attachment{}
|
||||
err := row.Scan(&a.ID, &a.MessageID, &a.Filename, &a.StoredAs, &a.MimeType, &a.Size, &a.UploadedAt, &a.UploaderID)
|
||||
r, err := d.q.GetAttachmentByID(dbCtx(), id)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetAttachmentByID: %w", err)
|
||||
}
|
||||
return a, nil
|
||||
return &Attachment{
|
||||
ID: r.ID,
|
||||
MessageID: r.MessageID,
|
||||
Filename: r.Filename,
|
||||
StoredAs: r.StoredAs,
|
||||
MimeType: r.MimeType,
|
||||
Size: r.Size,
|
||||
UploadedAt: r.UploadedAt,
|
||||
UploaderID: r.UploaderID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetAttachmentWithChannel returns the attachment plus the channel context
|
||||
@@ -64,30 +75,27 @@ func (d *DB) GetAttachmentByID(id string) (*Attachment, error) {
|
||||
// attachment does not exist. ChannelID/ChannelType are nil/empty when the
|
||||
// attachment is unlinked or its message/channel was deleted.
|
||||
func (d *DB) GetAttachmentWithChannel(id string) (*AttachmentAccess, error) {
|
||||
row := d.sqlDB.QueryRow(
|
||||
`SELECT a.id, a.message_id, a.filename, a.stored_as, a.mime_type, a.size,
|
||||
a.uploaded_at, a.uploader_id, m.channel_id, c.type
|
||||
FROM attachments a
|
||||
LEFT JOIN messages m ON m.id = a.message_id
|
||||
LEFT JOIN channels c ON c.id = m.channel_id
|
||||
WHERE a.id = ?`, id,
|
||||
)
|
||||
aa := &AttachmentAccess{}
|
||||
var chType *string
|
||||
err := row.Scan(
|
||||
&aa.ID, &aa.MessageID, &aa.Filename, &aa.StoredAs, &aa.MimeType,
|
||||
&aa.Size, &aa.UploadedAt, &aa.UploaderID, &aa.ChannelID, &chType,
|
||||
)
|
||||
r, err := d.q.GetAttachmentWithChannel(dbCtx(), id)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetAttachmentWithChannel: %w", err)
|
||||
}
|
||||
if chType != nil {
|
||||
aa.ChannelType = *chType
|
||||
}
|
||||
return aa, nil
|
||||
return &AttachmentAccess{
|
||||
Attachment: Attachment{
|
||||
ID: r.ID,
|
||||
MessageID: r.MessageID,
|
||||
Filename: r.Filename,
|
||||
StoredAs: r.StoredAs,
|
||||
MimeType: r.MimeType,
|
||||
Size: r.Size,
|
||||
UploadedAt: r.UploadedAt,
|
||||
UploaderID: r.UploaderID,
|
||||
},
|
||||
ChannelID: r.ChannelID,
|
||||
ChannelType: derefString(r.Type),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// LinkAttachmentsToMessage sets message_id on attachments that are currently
|
||||
@@ -177,26 +185,9 @@ func (d *DB) GetAttachmentsByMessageIDs(msgIDs []int64) (map[int64][]AttachmentI
|
||||
// preventing a race where an attachment linked between SELECT and DELETE
|
||||
// would have its file deleted while the DB row survives.
|
||||
func (d *DB) DeleteOrphanedAttachments(cutoff string) ([]string, error) {
|
||||
rows, err := d.sqlDB.Query(
|
||||
`DELETE FROM attachments WHERE message_id IS NULL AND uploaded_at < ? RETURNING stored_as`,
|
||||
cutoff,
|
||||
)
|
||||
files, err := d.q.DeleteOrphanedAttachments(dbCtx(), cutoff)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("DeleteOrphanedAttachments: %w", err)
|
||||
}
|
||||
defer rows.Close() //nolint:errcheck
|
||||
|
||||
var files []string
|
||||
for rows.Next() {
|
||||
var storedAs string
|
||||
if scanErr := rows.Scan(&storedAs); scanErr != nil {
|
||||
return nil, fmt.Errorf("DeleteOrphanedAttachments scan: %w", scanErr)
|
||||
}
|
||||
files = append(files, storedAs)
|
||||
}
|
||||
if rows.Err() != nil {
|
||||
return nil, fmt.Errorf("DeleteOrphanedAttachments rows: %w", rows.Err())
|
||||
}
|
||||
|
||||
return files, nil
|
||||
}
|
||||
|
||||
+19
-27
@@ -344,11 +344,12 @@ func (d *DB) CreateInvite(createdBy int64, maxUses int, expiresAt *time.Time) (s
|
||||
expiresStr = &s
|
||||
}
|
||||
|
||||
_, err = d.sqlDB.Exec(
|
||||
`INSERT INTO invites (code, created_by, max_uses, expires_at) VALUES (?, ?, ?, ?)`,
|
||||
code, createdBy, maxUsesVal, expiresStr,
|
||||
)
|
||||
if err != nil {
|
||||
if err := d.q.CreateInvite(dbCtx(), dbgen.CreateInviteParams{
|
||||
Code: code,
|
||||
CreatedBy: createdBy,
|
||||
MaxUses: ptrItoI64(maxUsesVal),
|
||||
ExpiresAt: expiresStr,
|
||||
}); err != nil {
|
||||
return "", fmt.Errorf("CreateInvite insert: %w", err)
|
||||
}
|
||||
return code, nil
|
||||
@@ -356,25 +357,23 @@ func (d *DB) CreateInvite(createdBy int64, maxUses int, expiresAt *time.Time) (s
|
||||
|
||||
// GetInvite returns the invite for the given code, or nil if not found.
|
||||
func (d *DB) GetInvite(code string) (*Invite, error) {
|
||||
row := d.sqlDB.QueryRow(
|
||||
`SELECT id, code, created_by, max_uses, use_count, expires_at, revoked, created_at
|
||||
FROM invites WHERE code = ?`,
|
||||
code,
|
||||
)
|
||||
inv := &Invite{}
|
||||
var revoked int
|
||||
err := row.Scan(
|
||||
&inv.ID, &inv.Code, &inv.CreatedBy, &inv.MaxUses,
|
||||
&inv.Uses, &inv.ExpiresAt, &revoked, &inv.CreatedAt,
|
||||
)
|
||||
r, err := d.q.GetInvite(dbCtx(), code)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetInvite: %w", err)
|
||||
}
|
||||
inv.Revoked = revoked != 0
|
||||
return inv, nil
|
||||
return &Invite{
|
||||
ID: r.ID,
|
||||
Code: r.Code,
|
||||
CreatedBy: r.CreatedBy,
|
||||
Uses: int(r.UseCount),
|
||||
MaxUses: ptrI64toI(r.MaxUses),
|
||||
ExpiresAt: r.ExpiresAt,
|
||||
Revoked: r.Revoked != 0,
|
||||
CreatedAt: r.CreatedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UseInviteAtomic validates and increments the use_count in a single SQL
|
||||
@@ -390,13 +389,7 @@ func (d *DB) GetInvite(code string) (*Invite, error) {
|
||||
// If zero rows are affected the invite is missing, revoked, expired, or
|
||||
// exhausted — an error is returned in all such cases.
|
||||
func (d *DB) UseInviteAtomic(code string) error {
|
||||
result, err := d.sqlDB.Exec(
|
||||
`UPDATE invites SET use_count = use_count + 1
|
||||
WHERE code = ? AND revoked = 0
|
||||
AND (max_uses IS NULL OR use_count < max_uses)
|
||||
AND (expires_at IS NULL OR strftime('%s', expires_at) > strftime('%s', 'now'))`,
|
||||
code,
|
||||
)
|
||||
result, err := d.q.UseInviteAtomic(dbCtx(), code)
|
||||
if err != nil {
|
||||
return fmt.Errorf("UseInviteAtomic: %w", err)
|
||||
}
|
||||
@@ -412,8 +405,7 @@ func (d *DB) UseInviteAtomic(code string) error {
|
||||
|
||||
// RevokeInvite marks an invite as revoked.
|
||||
func (d *DB) RevokeInvite(code string) error {
|
||||
_, err := d.sqlDB.Exec(`UPDATE invites SET revoked = 1 WHERE code = ?`, code)
|
||||
if err != nil {
|
||||
if err := d.q.RevokeInvite(dbCtx(), code); err != nil {
|
||||
return fmt.Errorf("RevokeInvite: %w", err)
|
||||
}
|
||||
return nil
|
||||
|
||||
+14
-19
@@ -5,27 +5,22 @@ import "fmt"
|
||||
// ListInvites returns invites ordered by creation time descending.
|
||||
// M-12: Limited to 200 rows to prevent unbounded result sets.
|
||||
func (d *DB) ListInvites() ([]*Invite, error) {
|
||||
rows, err := d.sqlDB.Query(
|
||||
`SELECT id, code, created_by, max_uses, use_count, expires_at, revoked, created_at
|
||||
FROM invites ORDER BY created_at DESC LIMIT 200`,
|
||||
)
|
||||
rows, err := d.q.ListInvites(dbCtx())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ListInvites: %w", err)
|
||||
}
|
||||
defer rows.Close() //nolint:errcheck
|
||||
|
||||
var invites []*Invite
|
||||
for rows.Next() {
|
||||
inv := &Invite{}
|
||||
var revoked int
|
||||
if err := rows.Scan(
|
||||
&inv.ID, &inv.Code, &inv.CreatedBy, &inv.MaxUses,
|
||||
&inv.Uses, &inv.ExpiresAt, &revoked, &inv.CreatedAt,
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("ListInvites scan: %w", err)
|
||||
}
|
||||
inv.Revoked = revoked != 0
|
||||
invites = append(invites, inv)
|
||||
invites := make([]*Invite, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
invites = append(invites, &Invite{
|
||||
ID: r.ID,
|
||||
Code: r.Code,
|
||||
CreatedBy: r.CreatedBy,
|
||||
Uses: int(r.UseCount),
|
||||
MaxUses: ptrI64toI(r.MaxUses),
|
||||
ExpiresAt: r.ExpiresAt,
|
||||
Revoked: r.Revoked != 0,
|
||||
CreatedAt: r.CreatedAt,
|
||||
})
|
||||
}
|
||||
return invites, rows.Err()
|
||||
return invites, nil
|
||||
}
|
||||
|
||||
@@ -18,6 +18,26 @@ func derefString(s *string) string {
|
||||
return *s
|
||||
}
|
||||
|
||||
// ptrI64toI narrows a *int64 (sqlc's nullable-integer type) to a *int, which
|
||||
// the domain models use for optional counts (e.g. invite max_uses).
|
||||
func ptrI64toI(p *int64) *int {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
v := int(*p)
|
||||
return &v
|
||||
}
|
||||
|
||||
// ptrItoI64 widens a *int to a *int64 for passing into a generated params
|
||||
// struct (the inverse of ptrI64toI).
|
||||
func ptrItoI64(p *int) *int64 {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
v := int64(*p)
|
||||
return &v
|
||||
}
|
||||
|
||||
// userFromGen maps a generated user row to the domain User model.
|
||||
func userFromGen(u dbgen.User) *User {
|
||||
return &User{
|
||||
|
||||
Reference in New Issue
Block a user