176 lines
7.6 KiB
Go
176 lines
7.6 KiB
Go
package postgres
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
|
|
"gardomatic.kleiax.de/internal/storage"
|
|
)
|
|
|
|
// JournalModel implements storage.JournalModelInterface for PostgreSQL and
|
|
// enforces the garden boundary on entries and attachments.
|
|
type JournalModel struct{ DB *sql.DB }
|
|
|
|
const journalColumns = `e.id, e.garden_id, e.author_id, u.name, u.color, e.entry_type, e.title, e.body, e.created_at, e.updated_at, e.version`
|
|
|
|
func scanJournalEntry(s scanner) (storage.JournalEntry, error) {
|
|
var entry storage.JournalEntry
|
|
err := s.Scan(&entry.ID, &entry.GardenID, &entry.AuthorID, &entry.AuthorName, &entry.AuthorColor, &entry.EntryType, &entry.Title, &entry.Body, &entry.CreatedAt, &entry.UpdatedAt, &entry.Version)
|
|
return entry, err
|
|
}
|
|
|
|
// Insert creates a journal entry and its tags atomically.
|
|
func (m JournalModel) Insert(entry storage.JournalEntry) (storage.JournalEntry, error) {
|
|
ctx, cancel := contextWithTimeout()
|
|
defer cancel()
|
|
if entry.EntryType == "" {
|
|
entry.EntryType = storage.JournalEntryTypeJournal
|
|
}
|
|
err := m.DB.QueryRowContext(ctx, `INSERT INTO journal_entries(garden_id,author_id,entry_type,title,body,created_at) VALUES($1,$2,$3,$4,$5,$6) RETURNING id,created_at,updated_at,version`, entry.GardenID, entry.AuthorID, entry.EntryType, entry.Title, entry.Body, entry.CreatedAt).Scan(&entry.ID, &entry.CreatedAt, &entry.UpdatedAt, &entry.Version)
|
|
if err != nil {
|
|
return storage.JournalEntry{}, recordError(err)
|
|
}
|
|
return entry, nil
|
|
}
|
|
|
|
// Get returns an entry with tags and attachment metadata within its garden.
|
|
func (m JournalModel) Get(gardenID, id int) (storage.JournalEntry, error) {
|
|
ctx, cancel := contextWithTimeout()
|
|
defer cancel()
|
|
entry, err := scanJournalEntry(m.DB.QueryRowContext(ctx, `SELECT `+journalColumns+` FROM journal_entries e JOIN users u ON u.id=e.author_id WHERE e.garden_id=$1 AND e.id=$2`, gardenID, id))
|
|
if err != nil {
|
|
return storage.JournalEntry{}, recordError(err)
|
|
}
|
|
entry.Attachments, err = m.attachments(ctx, entry.ID)
|
|
return entry, err
|
|
}
|
|
|
|
// GetAllForGarden lists entries of an optional type in a garden.
|
|
func (m JournalModel) GetAllForGarden(gardenID int, entryType storage.JournalEntryType) ([]storage.JournalEntry, error) {
|
|
ctx, cancel := contextWithTimeout()
|
|
defer cancel()
|
|
if entryType == "" {
|
|
entryType = storage.JournalEntryTypeJournal
|
|
}
|
|
rows, err := m.DB.QueryContext(ctx, `SELECT `+journalColumns+` FROM journal_entries e JOIN users u ON u.id=e.author_id WHERE e.garden_id=$1 AND e.entry_type=$2 ORDER BY e.created_at DESC,e.id DESC`, gardenID, entryType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
entries := []storage.JournalEntry{}
|
|
for rows.Next() {
|
|
entry, scanErr := scanJournalEntry(rows)
|
|
if scanErr != nil {
|
|
return nil, scanErr
|
|
}
|
|
entries = append(entries, entry)
|
|
}
|
|
if err = rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
for i := range entries {
|
|
entries[i].Attachments, err = m.attachments(ctx, entries[i].ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return entries, nil
|
|
}
|
|
|
|
// Update changes an entry and its tags atomically using optimistic locking.
|
|
func (m JournalModel) Update(gardenID int, entry storage.JournalEntry) (storage.JournalEntry, error) {
|
|
ctx, cancel := contextWithTimeout()
|
|
defer cancel()
|
|
err := m.DB.QueryRowContext(ctx, `UPDATE journal_entries SET title=$1,body=$2,created_at=$3,updated_at=now(),version=version+1 WHERE garden_id=$4 AND id=$5 AND version=$6 RETURNING created_at,updated_at,version`, entry.Title, entry.Body, entry.CreatedAt, gardenID, entry.ID, entry.Version).Scan(&entry.CreatedAt, &entry.UpdatedAt, &entry.Version)
|
|
if err == sql.ErrNoRows {
|
|
return storage.JournalEntry{}, storage.ErrEditConflict
|
|
}
|
|
if err != nil {
|
|
return storage.JournalEntry{}, err
|
|
}
|
|
return entry, nil
|
|
}
|
|
|
|
// Delete removes an entry within its garden.
|
|
func (m JournalModel) Delete(gardenID, id int) error {
|
|
return deleteByID(m.DB, `DELETE FROM journal_entries WHERE garden_id=$1 AND id=$2`, gardenID, id)
|
|
}
|
|
|
|
// InsertAttachment adds inline media or a library-image link to an entry.
|
|
func (m JournalModel) InsertAttachment(gardenID, entryID int, attachment storage.JournalAttachment) (storage.JournalAttachment, error) {
|
|
ctx, cancel := contextWithTimeout()
|
|
defer cancel()
|
|
var entryType storage.JournalEntryType
|
|
if err := m.DB.QueryRowContext(ctx, `SELECT entry_type FROM journal_entries WHERE garden_id=$1 AND id=$2`, gardenID, entryID).Scan(&entryType); err != nil {
|
|
return storage.JournalAttachment{}, recordError(err)
|
|
}
|
|
if attachment.ImageID != nil {
|
|
image, err := ImageModel(m).Get(gardenID, *attachment.ImageID)
|
|
if err != nil {
|
|
return storage.JournalAttachment{}, err
|
|
}
|
|
attachment.MediaType, attachment.Size = image.MediaType, image.Size
|
|
if attachment.FileName == "" {
|
|
attachment.FileName = image.FileName
|
|
}
|
|
if attachment.FileName == "" {
|
|
attachment.FileName = "Bild"
|
|
}
|
|
} else if len(attachment.MediaType) > 6 && attachment.MediaType[:6] == "image/" {
|
|
image, err := ImageModel(m).Insert(storage.Image{GardenID: gardenID, FileName: attachment.FileName, MediaType: attachment.MediaType, Data: attachment.Data, Source: string(entryType)})
|
|
if err != nil {
|
|
return storage.JournalAttachment{}, err
|
|
}
|
|
attachment.ImageID = &image.ID
|
|
}
|
|
data := attachment.Data
|
|
if attachment.ImageID != nil {
|
|
data = []byte{}
|
|
}
|
|
size := int64(len(attachment.Data))
|
|
if attachment.ImageID != nil {
|
|
size = attachment.Size
|
|
}
|
|
err := m.DB.QueryRowContext(ctx, `INSERT INTO journal_attachments(journal_entry_id,file_name,media_type,data,size,image_id) SELECT e.id,$1,$2,$3,$4,$5 FROM journal_entries e WHERE e.garden_id=$6 AND e.id=$7 RETURNING id,created_at`, attachment.FileName, attachment.MediaType, data, size, attachment.ImageID, gardenID, entryID).Scan(&attachment.ID, &attachment.CreatedAt)
|
|
if err != nil {
|
|
return storage.JournalAttachment{}, recordError(err)
|
|
}
|
|
attachment.EntryID, attachment.Size = entryID, size
|
|
attachment.Data = nil
|
|
return attachment, nil
|
|
}
|
|
|
|
// GetAttachment returns attachment metadata and data within its garden and entry.
|
|
func (m JournalModel) GetAttachment(gardenID, entryID, attachmentID int) (storage.JournalAttachment, error) {
|
|
ctx, cancel := contextWithTimeout()
|
|
defer cancel()
|
|
var a storage.JournalAttachment
|
|
err := m.DB.QueryRowContext(ctx, `SELECT a.id,a.journal_entry_id,a.file_name,a.media_type,a.size,a.created_at,COALESCE(i.data,a.data),a.image_id FROM journal_attachments a JOIN journal_entries e ON e.id=a.journal_entry_id LEFT JOIN images i ON i.id=a.image_id WHERE e.garden_id=$1 AND e.id=$2 AND a.id=$3`, gardenID, entryID, attachmentID).Scan(&a.ID, &a.EntryID, &a.FileName, &a.MediaType, &a.Size, &a.CreatedAt, &a.Data, &a.ImageID)
|
|
if err != nil {
|
|
return storage.JournalAttachment{}, recordError(err)
|
|
}
|
|
return a, nil
|
|
}
|
|
|
|
// DeleteAttachment removes an attachment within its garden and entry.
|
|
func (m JournalModel) DeleteAttachment(gardenID, entryID, attachmentID int) error {
|
|
return deleteByID(m.DB, `DELETE FROM journal_attachments a USING journal_entries e WHERE a.journal_entry_id=e.id AND e.garden_id=$1 AND e.id=$2 AND a.id=$3`, gardenID, entryID, attachmentID)
|
|
}
|
|
|
|
func (m JournalModel) attachments(ctx context.Context, entryID int) ([]storage.JournalAttachment, error) {
|
|
rows, err := m.DB.QueryContext(ctx, `SELECT id,journal_entry_id,file_name,media_type,size,created_at,image_id FROM journal_attachments WHERE journal_entry_id=$1 ORDER BY id`, entryID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
result := []storage.JournalAttachment{}
|
|
for rows.Next() {
|
|
var a storage.JournalAttachment
|
|
if err = rows.Scan(&a.ID, &a.EntryID, &a.FileName, &a.MediaType, &a.Size, &a.CreatedAt, &a.ImageID); err != nil {
|
|
return nil, err
|
|
}
|
|
result = append(result, a)
|
|
}
|
|
return result, rows.Err()
|
|
}
|