Files
amcs/internal/store/files.go
T
warkanum 196b543bee
CI / build-and-test (push) Successful in 1m47s
feat(ui): add identity management for tenants and users
* Implement tenant and user creation in IdentityPage
* Add API calls for managing tenants and users
* Introduce tenant-scoped API requests
* Update sidebar to include identity navigation
* Create BooleanStatusBadge component for key status
2026-07-20 22:50:55 +02:00

199 lines
5.4 KiB
Go

package store
import (
"context"
"encoding/json"
"fmt"
"strings"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"git.warky.dev/wdevs/amcs/internal/generatedmodels"
thoughttypes "git.warky.dev/wdevs/amcs/internal/types"
)
func (db *DB) InsertStoredFile(ctx context.Context, file thoughttypes.StoredFile) (thoughttypes.StoredFile, error) {
row := db.pool.QueryRow(ctx, `
insert into stored_files (thought_id, project_id, tenant_id, name, media_type, kind, encoding, size_bytes, sha256, content)
values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
returning id, guid, thought_id, project_id, name, media_type, kind, encoding, size_bytes, sha256, created_at, updated_at
`, file.ThoughtID, file.ProjectID, tenantKeyPtr(ctx), file.Name, file.MediaType, file.Kind, file.Encoding, file.SizeBytes, file.SHA256, file.Content)
var model generatedmodels.ModelPublicStoredFiles
if err := row.Scan(
&model.ID,
&model.GUID,
&model.ThoughtID,
&model.ProjectID,
&model.Name,
&model.MediaType,
&model.Kind,
&model.Encoding,
&model.SizeBytes,
&model.Sha256,
&model.CreatedAt,
&model.UpdatedAt,
); err != nil {
return thoughttypes.StoredFile{}, fmt.Errorf("insert stored file: %w", err)
}
return storedFileFromModel(model), nil
}
func (db *DB) GetStoredFile(ctx context.Context, id uuid.UUID) (thoughttypes.StoredFile, error) {
args := []any{id}
row := db.pool.QueryRow(ctx, `
select id, guid, thought_id, project_id, name, media_type, kind, encoding, size_bytes, sha256, content, created_at, updated_at
from stored_files
where guid = $1`+tenantSQL(ctx, &args, "tenant_id"), args...)
var model generatedmodels.ModelPublicStoredFiles
if err := row.Scan(
&model.ID,
&model.GUID,
&model.ThoughtID,
&model.ProjectID,
&model.Name,
&model.MediaType,
&model.Kind,
&model.Encoding,
&model.SizeBytes,
&model.Sha256,
&model.Content,
&model.CreatedAt,
&model.UpdatedAt,
); err != nil {
if err == pgx.ErrNoRows {
return thoughttypes.StoredFile{}, err
}
return thoughttypes.StoredFile{}, fmt.Errorf("get stored file: %w", err)
}
return storedFileFromModel(model), nil
}
func (db *DB) ListStoredFiles(ctx context.Context, filter thoughttypes.StoredFileFilter) ([]thoughttypes.StoredFile, error) {
args := make([]any, 0, 4)
conditions := make([]string, 0, 3)
addTenantCondition(ctx, &args, &conditions, "tenant_id")
if filter.ThoughtID != nil {
args = append(args, *filter.ThoughtID)
conditions = append(conditions, fmt.Sprintf("thought_id = $%d", len(args)))
}
if filter.ProjectID != nil {
args = append(args, *filter.ProjectID)
conditions = append(conditions, fmt.Sprintf("project_id = $%d", len(args)))
}
if kind := strings.TrimSpace(filter.Kind); kind != "" {
args = append(args, kind)
conditions = append(conditions, fmt.Sprintf("kind = $%d", len(args)))
}
query := `
select id, guid, thought_id, project_id, name, media_type, kind, encoding, size_bytes, sha256, created_at, updated_at
from stored_files
`
if len(conditions) > 0 {
query += " where " + strings.Join(conditions, " and ")
}
args = append(args, filter.Limit)
query += fmt.Sprintf(" order by created_at desc limit $%d", len(args))
rows, err := db.pool.Query(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("list stored files: %w", err)
}
defer rows.Close()
files := make([]thoughttypes.StoredFile, 0, filter.Limit)
for rows.Next() {
var model generatedmodels.ModelPublicStoredFiles
if err := rows.Scan(
&model.ID,
&model.GUID,
&model.ThoughtID,
&model.ProjectID,
&model.Name,
&model.MediaType,
&model.Kind,
&model.Encoding,
&model.SizeBytes,
&model.Sha256,
&model.CreatedAt,
&model.UpdatedAt,
); err != nil {
return nil, fmt.Errorf("scan stored file: %w", err)
}
files = append(files, storedFileFromModel(model))
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("iterate stored files: %w", err)
}
return files, nil
}
func (db *DB) AddThoughtAttachment(ctx context.Context, thoughtID int64, attachment thoughttypes.ThoughtAttachment) error {
tx, err := db.pool.Begin(ctx)
if err != nil {
return fmt.Errorf("begin transaction: %w", err)
}
defer func() {
_ = tx.Rollback(ctx)
}()
var metadataBytes []byte
if err := tx.QueryRow(ctx, `select metadata from thoughts where id = $1 for update`, thoughtID).Scan(&metadataBytes); err != nil {
if err == pgx.ErrNoRows {
return err
}
return fmt.Errorf("load thought metadata: %w", err)
}
var metadata thoughttypes.ThoughtMetadata
if len(metadataBytes) > 0 {
if err := json.Unmarshal(metadataBytes, &metadata); err != nil {
return fmt.Errorf("decode thought metadata: %w", err)
}
}
replaced := false
for i := range metadata.Attachments {
if metadata.Attachments[i].FileID == attachment.FileID {
metadata.Attachments[i] = attachment
replaced = true
break
}
}
if !replaced {
metadata.Attachments = append(metadata.Attachments, attachment)
}
updatedMetadata, err := json.Marshal(metadata)
if err != nil {
return fmt.Errorf("encode thought metadata: %w", err)
}
tag, err := tx.Exec(ctx, `
update thoughts
set metadata = $2::jsonb,
updated_at = now()
where id = $1
`, thoughtID, updatedMetadata)
if err != nil {
return fmt.Errorf("update thought attachments: %w", err)
}
if tag.RowsAffected() == 0 {
return pgx.ErrNoRows
}
if err := tx.Commit(ctx); err != nil {
return fmt.Errorf("commit attachment update: %w", err)
}
return nil
}