feat(db): add storage backend metadata columns #135
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
7
migrations/017_file_storage_metadata.sql
Normal file
7
migrations/017_file_storage_metadata.sql
Normal 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';
|
||||||
Reference in New Issue
Block a user