feat(db): add storage backend metadata columns #135

Merged
forbes merged 1 commits from feat-file-storage-metadata into main 2026-02-17 18:32:06 +00:00
3 changed files with 63 additions and 38 deletions

View File

@@ -8,13 +8,14 @@ import (
// ItemFile represents a file attachment on an item. // ItemFile represents a file attachment on an item.
type ItemFile struct { type ItemFile struct {
ID string ID string
ItemID string ItemID string
Filename string Filename string
ContentType string ContentType string
Size int64 Size int64
ObjectKey string ObjectKey string
CreatedAt time.Time StorageBackend string // "minio" or "filesystem"
CreatedAt time.Time
} }
// ItemFileRepository provides item_files database operations. // ItemFileRepository provides item_files database operations.
@@ -29,11 +30,14 @@ func NewItemFileRepository(db *DB) *ItemFileRepository {
// Create inserts a new item file record. // Create inserts a new item file record.
func (r *ItemFileRepository) Create(ctx context.Context, f *ItemFile) error { func (r *ItemFileRepository) Create(ctx context.Context, f *ItemFile) error {
if f.StorageBackend == "" {
f.StorageBackend = "minio"
}
err := r.db.pool.QueryRow(ctx, err := r.db.pool.QueryRow(ctx,
`INSERT INTO item_files (item_id, filename, content_type, size, object_key) `INSERT INTO item_files (item_id, filename, content_type, size, object_key, storage_backend)
VALUES ($1, $2, $3, $4, $5) VALUES ($1, $2, $3, $4, $5, $6)
RETURNING id, created_at`, RETURNING id, created_at`,
f.ItemID, f.Filename, f.ContentType, f.Size, f.ObjectKey, f.ItemID, f.Filename, f.ContentType, f.Size, f.ObjectKey, f.StorageBackend,
).Scan(&f.ID, &f.CreatedAt) ).Scan(&f.ID, &f.CreatedAt)
if err != nil { if err != nil {
return fmt.Errorf("creating item file: %w", err) return fmt.Errorf("creating item file: %w", err)
@@ -44,7 +48,8 @@ func (r *ItemFileRepository) Create(ctx context.Context, f *ItemFile) error {
// ListByItem returns all file attachments for an item. // ListByItem returns all file attachments for an item.
func (r *ItemFileRepository) ListByItem(ctx context.Context, itemID string) ([]*ItemFile, error) { func (r *ItemFileRepository) ListByItem(ctx context.Context, itemID string) ([]*ItemFile, error) {
rows, err := r.db.pool.Query(ctx, rows, err := r.db.pool.Query(ctx,
`SELECT id, item_id, filename, content_type, size, object_key, created_at `SELECT id, item_id, filename, content_type, size, object_key,
COALESCE(storage_backend, 'minio'), created_at
FROM item_files WHERE item_id = $1 ORDER BY created_at`, FROM item_files WHERE item_id = $1 ORDER BY created_at`,
itemID, itemID,
) )
@@ -56,7 +61,7 @@ func (r *ItemFileRepository) ListByItem(ctx context.Context, itemID string) ([]*
var files []*ItemFile var files []*ItemFile
for rows.Next() { for rows.Next() {
f := &ItemFile{} f := &ItemFile{}
if err := rows.Scan(&f.ID, &f.ItemID, &f.Filename, &f.ContentType, &f.Size, &f.ObjectKey, &f.CreatedAt); err != nil { if err := rows.Scan(&f.ID, &f.ItemID, &f.Filename, &f.ContentType, &f.Size, &f.ObjectKey, &f.StorageBackend, &f.CreatedAt); err != nil {
return nil, fmt.Errorf("scanning item file: %w", err) return nil, fmt.Errorf("scanning item file: %w", err)
} }
files = append(files, f) files = append(files, f)
@@ -68,10 +73,11 @@ func (r *ItemFileRepository) ListByItem(ctx context.Context, itemID string) ([]*
func (r *ItemFileRepository) Get(ctx context.Context, id string) (*ItemFile, error) { func (r *ItemFileRepository) Get(ctx context.Context, id string) (*ItemFile, error) {
f := &ItemFile{} f := &ItemFile{}
err := r.db.pool.QueryRow(ctx, err := r.db.pool.QueryRow(ctx,
`SELECT id, item_id, filename, content_type, size, object_key, created_at `SELECT id, item_id, filename, content_type, size, object_key,
COALESCE(storage_backend, 'minio'), created_at
FROM item_files WHERE id = $1`, FROM item_files WHERE id = $1`,
id, id,
).Scan(&f.ID, &f.ItemID, &f.Filename, &f.ContentType, &f.Size, &f.ObjectKey, &f.CreatedAt) ).Scan(&f.ID, &f.ItemID, &f.Filename, &f.ContentType, &f.Size, &f.ObjectKey, &f.StorageBackend, &f.CreatedAt)
if err != nil { if err != nil {
return nil, fmt.Errorf("getting item file: %w", err) return nil, fmt.Errorf("getting item file: %w", err)
} }

View File

@@ -35,11 +35,12 @@ type Revision struct {
ItemID string ItemID string
RevisionNumber int RevisionNumber int
Properties map[string]any Properties map[string]any
FileKey *string FileKey *string
FileVersion *string FileVersion *string
FileChecksum *string FileChecksum *string
FileSize *int64 FileSize *int64
ThumbnailKey *string FileStorageBackend string // "minio" or "filesystem"
ThumbnailKey *string
CreatedAt time.Time CreatedAt time.Time
CreatedBy *string CreatedBy *string
Comment *string Comment *string
@@ -306,16 +307,20 @@ func (r *ItemRepository) CreateRevision(ctx context.Context, rev *Revision) erro
return fmt.Errorf("marshaling properties: %w", err) return fmt.Errorf("marshaling properties: %w", err)
} }
if rev.FileStorageBackend == "" {
rev.FileStorageBackend = "minio"
}
err = r.db.pool.QueryRow(ctx, ` err = r.db.pool.QueryRow(ctx, `
INSERT INTO revisions ( INSERT INTO revisions (
item_id, revision_number, properties, file_key, file_version, item_id, revision_number, properties, file_key, file_version,
file_checksum, file_size, thumbnail_key, created_by, comment file_checksum, file_size, file_storage_backend, thumbnail_key, created_by, comment
) )
SELECT $1, current_revision + 1, $2, $3, $4, $5, $6, $7, $8, $9 SELECT $1, current_revision + 1, $2, $3, $4, $5, $6, $7, $8, $9, $10
FROM items WHERE id = $1 FROM items WHERE id = $1
RETURNING id, revision_number, created_at RETURNING id, revision_number, created_at
`, rev.ItemID, propsJSON, rev.FileKey, rev.FileVersion, `, rev.ItemID, propsJSON, rev.FileKey, rev.FileVersion,
rev.FileChecksum, rev.FileSize, rev.ThumbnailKey, rev.CreatedBy, rev.Comment, rev.FileChecksum, rev.FileSize, rev.FileStorageBackend, rev.ThumbnailKey, rev.CreatedBy, rev.Comment,
).Scan(&rev.ID, &rev.RevisionNumber, &rev.CreatedAt) ).Scan(&rev.ID, &rev.RevisionNumber, &rev.CreatedAt)
if err != nil { if err != nil {
return fmt.Errorf("inserting revision: %w", err) return fmt.Errorf("inserting revision: %w", err)
@@ -342,7 +347,8 @@ func (r *ItemRepository) GetRevisions(ctx context.Context, itemID string) ([]*Re
if hasStatusColumn { if hasStatusColumn {
rows, err = r.db.pool.Query(ctx, ` rows, err = r.db.pool.Query(ctx, `
SELECT id, item_id, revision_number, properties, file_key, file_version, SELECT id, item_id, revision_number, properties, file_key, file_version,
file_checksum, file_size, thumbnail_key, created_at, created_by, comment, file_checksum, file_size, COALESCE(file_storage_backend, 'minio'),
thumbnail_key, created_at, created_by, comment,
COALESCE(status, 'draft') as status, COALESCE(labels, ARRAY[]::TEXT[]) as labels COALESCE(status, 'draft') as status, COALESCE(labels, ARRAY[]::TEXT[]) as labels
FROM revisions FROM revisions
WHERE item_id = $1 WHERE item_id = $1
@@ -369,7 +375,8 @@ func (r *ItemRepository) GetRevisions(ctx context.Context, itemID string) ([]*Re
if hasStatusColumn { if hasStatusColumn {
err = rows.Scan( err = rows.Scan(
&rev.ID, &rev.ItemID, &rev.RevisionNumber, &propsJSON, &rev.FileKey, &rev.FileVersion, &rev.ID, &rev.ItemID, &rev.RevisionNumber, &propsJSON, &rev.FileKey, &rev.FileVersion,
&rev.FileChecksum, &rev.FileSize, &rev.ThumbnailKey, &rev.CreatedAt, &rev.CreatedBy, &rev.Comment, &rev.FileChecksum, &rev.FileSize, &rev.FileStorageBackend,
&rev.ThumbnailKey, &rev.CreatedAt, &rev.CreatedBy, &rev.Comment,
&rev.Status, &rev.Labels, &rev.Status, &rev.Labels,
) )
} else { } else {
@@ -379,6 +386,7 @@ func (r *ItemRepository) GetRevisions(ctx context.Context, itemID string) ([]*Re
) )
rev.Status = "draft" rev.Status = "draft"
rev.Labels = []string{} rev.Labels = []string{}
rev.FileStorageBackend = "minio"
} }
if err != nil { if err != nil {
return nil, fmt.Errorf("scanning revision: %w", err) return nil, fmt.Errorf("scanning revision: %w", err)
@@ -412,13 +420,15 @@ func (r *ItemRepository) GetRevision(ctx context.Context, itemID string, revisio
if hasStatusColumn { if hasStatusColumn {
err = r.db.pool.QueryRow(ctx, ` err = r.db.pool.QueryRow(ctx, `
SELECT id, item_id, revision_number, properties, file_key, file_version, SELECT id, item_id, revision_number, properties, file_key, file_version,
file_checksum, file_size, thumbnail_key, created_at, created_by, comment, file_checksum, file_size, COALESCE(file_storage_backend, 'minio'),
thumbnail_key, created_at, created_by, comment,
COALESCE(status, 'draft') as status, COALESCE(labels, ARRAY[]::TEXT[]) as labels COALESCE(status, 'draft') as status, COALESCE(labels, ARRAY[]::TEXT[]) as labels
FROM revisions FROM revisions
WHERE item_id = $1 AND revision_number = $2 WHERE item_id = $1 AND revision_number = $2
`, itemID, revisionNumber).Scan( `, itemID, revisionNumber).Scan(
&rev.ID, &rev.ItemID, &rev.RevisionNumber, &propsJSON, &rev.FileKey, &rev.FileVersion, &rev.ID, &rev.ItemID, &rev.RevisionNumber, &propsJSON, &rev.FileKey, &rev.FileVersion,
&rev.FileChecksum, &rev.FileSize, &rev.ThumbnailKey, &rev.CreatedAt, &rev.CreatedBy, &rev.Comment, &rev.FileChecksum, &rev.FileSize, &rev.FileStorageBackend,
&rev.ThumbnailKey, &rev.CreatedAt, &rev.CreatedBy, &rev.Comment,
&rev.Status, &rev.Labels, &rev.Status, &rev.Labels,
) )
} else { } else {
@@ -433,6 +443,7 @@ func (r *ItemRepository) GetRevision(ctx context.Context, itemID string, revisio
) )
rev.Status = "draft" rev.Status = "draft"
rev.Labels = []string{} rev.Labels = []string{}
rev.FileStorageBackend = "minio"
} }
if err == pgx.ErrNoRows { if err == pgx.ErrNoRows {
@@ -606,15 +617,16 @@ func (r *ItemRepository) CreateRevisionFromExisting(ctx context.Context, itemID
// Create new revision with copied properties (and optionally file reference) // Create new revision with copied properties (and optionally file reference)
newRev := &Revision{ newRev := &Revision{
ItemID: itemID, ItemID: itemID,
Properties: source.Properties, Properties: source.Properties,
FileKey: source.FileKey, FileKey: source.FileKey,
FileVersion: source.FileVersion, FileVersion: source.FileVersion,
FileChecksum: source.FileChecksum, FileChecksum: source.FileChecksum,
FileSize: source.FileSize, FileSize: source.FileSize,
ThumbnailKey: source.ThumbnailKey, FileStorageBackend: source.FileStorageBackend,
CreatedBy: createdBy, ThumbnailKey: source.ThumbnailKey,
Comment: &comment, CreatedBy: createdBy,
Comment: &comment,
} }
// Insert the new revision // Insert the new revision
@@ -626,13 +638,13 @@ func (r *ItemRepository) CreateRevisionFromExisting(ctx context.Context, itemID
err = r.db.pool.QueryRow(ctx, ` err = r.db.pool.QueryRow(ctx, `
INSERT INTO revisions ( INSERT INTO revisions (
item_id, revision_number, properties, file_key, file_version, item_id, revision_number, properties, file_key, file_version,
file_checksum, file_size, thumbnail_key, created_by, comment, status file_checksum, file_size, file_storage_backend, thumbnail_key, created_by, comment, status
) )
SELECT $1, current_revision + 1, $2, $3, $4, $5, $6, $7, $8, $9, 'draft' SELECT $1, current_revision + 1, $2, $3, $4, $5, $6, $7, $8, $9, $10, 'draft'
FROM items WHERE id = $1 FROM items WHERE id = $1
RETURNING id, revision_number, created_at RETURNING id, revision_number, created_at
`, newRev.ItemID, propsJSON, newRev.FileKey, newRev.FileVersion, `, newRev.ItemID, propsJSON, newRev.FileKey, newRev.FileVersion,
newRev.FileChecksum, newRev.FileSize, newRev.ThumbnailKey, newRev.CreatedBy, newRev.Comment, newRev.FileChecksum, newRev.FileSize, newRev.FileStorageBackend, newRev.ThumbnailKey, newRev.CreatedBy, newRev.Comment,
).Scan(&newRev.ID, &newRev.RevisionNumber, &newRev.CreatedAt) ).Scan(&newRev.ID, &newRev.RevisionNumber, &newRev.CreatedAt)
if err != nil { if err != nil {
return nil, fmt.Errorf("inserting revision: %w", err) return nil, fmt.Errorf("inserting revision: %w", err)

View File

@@ -0,0 +1,7 @@
-- Track which storage backend holds each attached file.
ALTER TABLE item_files
ADD COLUMN IF NOT EXISTS storage_backend TEXT NOT NULL DEFAULT 'minio';
-- Track which storage backend holds each revision file.
ALTER TABLE revisions
ADD COLUMN IF NOT EXISTS file_storage_backend TEXT NOT NULL DEFAULT 'minio';