Scope knowledge and image libraries by brand
This commit is contained in:
@@ -158,9 +158,13 @@ func (s *ImageService) Upload(ctx context.Context, fileName string, content []by
|
||||
}
|
||||
|
||||
tenantID := middleware.TenantIDFromCtx(ctx)
|
||||
brandID, err := requireCurrentBrandID(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
actor := auth.MustActor(ctx)
|
||||
if folderID != nil {
|
||||
if err := s.ensureFolderExists(ctx, tenantID, *folderID); err != nil {
|
||||
if err := s.ensureFolderExists(ctx, tenantID, brandID, *folderID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
@@ -181,7 +185,7 @@ func (s *ImageService) Upload(ctx context.Context, fileName string, content []by
|
||||
|
||||
sizeBytes := int64(len(webpBytes))
|
||||
contentHash := hashImageContent(webpBytes)
|
||||
existingAsset, err := s.findActiveImageAssetByContentHash(ctx, tenantID, contentHash)
|
||||
existingAsset, err := s.findActiveImageAssetByContentHash(ctx, tenantID, brandID, contentHash)
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50012, "image_asset_lookup_failed", "failed to check existing image asset")
|
||||
}
|
||||
@@ -219,9 +223,9 @@ func (s *ImageService) Upload(ctx context.Context, fileName string, content []by
|
||||
return nil, response.ErrInternal(50015, "image_quota_reserve_failed", "failed to reserve image storage quota")
|
||||
}
|
||||
|
||||
assetID, err := s.insertPendingImageAsset(ctx, tx, tenantID, folderID, objectKey, displayName, sizeBytes, contentHash, actor.UserID)
|
||||
assetID, err := s.insertPendingImageAsset(ctx, tx, tenantID, brandID, folderID, objectKey, displayName, sizeBytes, contentHash, actor.UserID)
|
||||
if err != nil {
|
||||
if isConstraintError(err, "fk_image_assets_folder") {
|
||||
if isConstraintError(err, "fk_image_assets_folder") || isConstraintError(err, "fk_image_assets_folder_brand") {
|
||||
return nil, response.ErrNotFound(40402, "image_folder_not_found", "folder not found")
|
||||
}
|
||||
return nil, response.ErrInternal(50016, "image_asset_create_failed", "failed to create image asset")
|
||||
@@ -241,7 +245,7 @@ func (s *ImageService) Upload(ctx context.Context, fileName string, content []by
|
||||
}
|
||||
|
||||
imageRepo := repository.NewImageRepository(s.pool)
|
||||
if err := imageRepo.UpdateStatus(ctx, tenantID, assetID, "active"); err != nil {
|
||||
if err := imageRepo.UpdateStatus(ctx, tenantID, brandID, assetID, "active"); err != nil {
|
||||
s.logError("failed to mark image active after upload", zap.Int64("tenant_id", tenantID), zap.Int64("asset_id", assetID), zap.Error(err))
|
||||
if queueErr := s.enqueueFinalizeUploadTask(tenantID, assetID); queueErr == nil {
|
||||
return &ImageUploadResult{
|
||||
@@ -282,9 +286,14 @@ func (s *ImageService) List(ctx context.Context, folderID *int64, query *string,
|
||||
}
|
||||
|
||||
tenantID := middleware.TenantIDFromCtx(ctx)
|
||||
brandID, err := requireCurrentBrandID(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
repo := repository.NewImageRepository(s.pool)
|
||||
assets, total, err := repo.List(ctx, repository.ImageAssetListParams{
|
||||
TenantID: tenantID,
|
||||
BrandID: brandID,
|
||||
FolderID: folderID,
|
||||
SearchQuery: searchQuery,
|
||||
PageSize: pageSize,
|
||||
@@ -323,9 +332,13 @@ func (s *ImageService) Update(ctx context.Context, id int64, req ImageUpdateRequ
|
||||
}
|
||||
|
||||
tenantID := middleware.TenantIDFromCtx(ctx)
|
||||
brandID, err := requireCurrentBrandID(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
repo := repository.NewImageRepository(s.pool)
|
||||
|
||||
asset, err := repo.Get(ctx, tenantID, id)
|
||||
asset, err := repo.Get(ctx, tenantID, brandID, id)
|
||||
if err != nil {
|
||||
return mapImageNotFound(err)
|
||||
}
|
||||
@@ -341,7 +354,7 @@ func (s *ImageService) Update(ctx context.Context, id int64, req ImageUpdateRequ
|
||||
if len(name) > 255 {
|
||||
return response.ErrBadRequest(40002, "invalid_name", "name must be at most 255 characters")
|
||||
}
|
||||
if err := repo.UpdateName(ctx, tenantID, id, name); err != nil {
|
||||
if err := repo.UpdateName(ctx, tenantID, brandID, id, name); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -352,12 +365,12 @@ func (s *ImageService) Update(ctx context.Context, id int64, req ImageUpdateRequ
|
||||
return err
|
||||
}
|
||||
if folderID != nil {
|
||||
if err := s.ensureFolderExists(ctx, tenantID, *folderID); err != nil {
|
||||
if err := s.ensureFolderExists(ctx, tenantID, brandID, *folderID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := repo.UpdateFolder(ctx, tenantID, id, folderID); err != nil {
|
||||
if isConstraintError(err, "fk_image_assets_folder") {
|
||||
if err := repo.UpdateFolder(ctx, tenantID, brandID, id, folderID); err != nil {
|
||||
if isConstraintError(err, "fk_image_assets_folder") || isConstraintError(err, "fk_image_assets_folder_brand") {
|
||||
return response.ErrNotFound(40402, "image_folder_not_found", "folder not found")
|
||||
}
|
||||
return err
|
||||
@@ -374,7 +387,7 @@ type existingImageAsset struct {
|
||||
SizeBytes int64
|
||||
}
|
||||
|
||||
func (s *ImageService) findActiveImageAssetByContentHash(ctx context.Context, tenantID int64, contentHash string) (*existingImageAsset, error) {
|
||||
func (s *ImageService) findActiveImageAssetByContentHash(ctx context.Context, tenantID, brandID int64, contentHash string) (*existingImageAsset, error) {
|
||||
if strings.TrimSpace(contentHash) == "" {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -384,12 +397,13 @@ func (s *ImageService) findActiveImageAssetByContentHash(ctx context.Context, te
|
||||
SELECT id, object_key, name, size_bytes
|
||||
FROM image_assets
|
||||
WHERE tenant_id = $1
|
||||
AND content_hash = $2
|
||||
AND brand_id = $2
|
||||
AND content_hash = $3
|
||||
AND status = 'active'
|
||||
AND deleted_at IS NULL
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
`, tenantID, contentHash).Scan(&asset.ID, &asset.ObjectKey, &asset.Name, &asset.SizeBytes)
|
||||
`, tenantID, brandID, contentHash).Scan(&asset.ID, &asset.ObjectKey, &asset.Name, &asset.SizeBytes)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, nil
|
||||
@@ -404,6 +418,7 @@ func (s *ImageService) insertPendingImageAsset(
|
||||
ctx context.Context,
|
||||
tx pgx.Tx,
|
||||
tenantID int64,
|
||||
brandID int64,
|
||||
folderID *int64,
|
||||
objectKey string,
|
||||
name string,
|
||||
@@ -412,10 +427,10 @@ func (s *ImageService) insertPendingImageAsset(
|
||||
createdBy int64,
|
||||
) (int64, error) {
|
||||
row := tx.QueryRow(ctx, `
|
||||
INSERT INTO image_assets (tenant_id, folder_id, object_key, name, size_bytes, status, content_hash, created_by)
|
||||
VALUES ($1, $2, $3, $4, $5, 'pending_upload', $6, $7)
|
||||
INSERT INTO image_assets (tenant_id, brand_id, folder_id, object_key, name, size_bytes, status, content_hash, created_by)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, 'pending_upload', $7, $8)
|
||||
RETURNING id
|
||||
`, tenantID, nullableImageFolderValue(folderID), objectKey, name, sizeBytes, contentHash, createdBy)
|
||||
`, tenantID, brandID, nullableImageFolderValue(folderID), objectKey, name, sizeBytes, contentHash, createdBy)
|
||||
|
||||
var assetID int64
|
||||
if err := row.Scan(&assetID); err != nil {
|
||||
@@ -426,10 +441,14 @@ RETURNING id
|
||||
|
||||
func (s *ImageService) GetReferences(ctx context.Context, imageID int64) (*ImageReferenceDetail, error) {
|
||||
tenantID := middleware.TenantIDFromCtx(ctx)
|
||||
brandID, err := requireCurrentBrandID(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
imageRepo := repository.NewImageRepository(s.pool)
|
||||
refRepo := repository.NewImageReferenceRepository(s.pool)
|
||||
|
||||
asset, err := imageRepo.Get(ctx, tenantID, imageID)
|
||||
asset, err := imageRepo.Get(ctx, tenantID, brandID, imageID)
|
||||
if err != nil {
|
||||
return nil, mapImageNotFound(err)
|
||||
}
|
||||
@@ -494,10 +513,14 @@ func (s *ImageService) Delete(ctx context.Context, id int64, force bool) error {
|
||||
}
|
||||
|
||||
tenantID := middleware.TenantIDFromCtx(ctx)
|
||||
brandID, err := requireCurrentBrandID(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
imageRepo := repository.NewImageRepository(s.pool)
|
||||
refRepo := repository.NewImageReferenceRepository(s.pool)
|
||||
|
||||
asset, err := imageRepo.Get(ctx, tenantID, id)
|
||||
asset, err := imageRepo.Get(ctx, tenantID, brandID, id)
|
||||
if err != nil {
|
||||
return mapImageNotFound(err)
|
||||
}
|
||||
@@ -515,7 +538,7 @@ func (s *ImageService) Delete(ctx context.Context, id int64, force bool) error {
|
||||
}
|
||||
}
|
||||
|
||||
if err := imageRepo.UpdateStatus(ctx, tenantID, id, "pending_delete"); err != nil {
|
||||
if err := imageRepo.UpdateStatus(ctx, tenantID, brandID, id, "pending_delete"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -556,7 +579,13 @@ func (s *ImageService) rollbackUpload(ctx context.Context, tenantID, assetID, si
|
||||
imageRepo := repository.NewImageRepository(s.pool)
|
||||
usageRepo := repository.NewImageUsageRepository(s.pool)
|
||||
|
||||
if err := imageRepo.UpdateStatus(ctx, tenantID, assetID, "upload_failed"); err != nil {
|
||||
brandID, err := s.lookupImageAssetBrandID(ctx, tenantID, assetID)
|
||||
if err != nil {
|
||||
s.logError("failed to resolve image brand after upload rollback", zap.Int64("tenant_id", tenantID), zap.Int64("asset_id", assetID), zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
if err := imageRepo.UpdateStatus(ctx, tenantID, brandID, assetID, "upload_failed"); err != nil {
|
||||
s.logError("failed to mark image upload as failed", zap.Int64("tenant_id", tenantID), zap.Int64("asset_id", assetID), zap.Error(err))
|
||||
}
|
||||
if err := usageRepo.Release(ctx, tenantID, sizeBytes); err != nil {
|
||||
@@ -605,11 +634,11 @@ func (s *ImageService) ProcessFinalizeUpload(ctx context.Context, tenantID, asse
|
||||
|
||||
imageRepoTx := repository.NewImageRepository(tx)
|
||||
if exists {
|
||||
if err := imageRepoTx.UpdateStatus(ctx, tenantID, assetID, "active"); err != nil {
|
||||
if err := imageRepoTx.UpdateStatus(ctx, tenantID, lockedAsset.BrandID, assetID, "active"); err != nil {
|
||||
return response.ErrInternal(50033, "image_upload_finalize_failed", "failed to activate uploaded image")
|
||||
}
|
||||
} else {
|
||||
if err := imageRepoTx.UpdateStatus(ctx, tenantID, assetID, "upload_failed"); err != nil {
|
||||
if err := imageRepoTx.UpdateStatus(ctx, tenantID, lockedAsset.BrandID, assetID, "upload_failed"); err != nil {
|
||||
return response.ErrInternal(50034, "image_upload_finalize_failed", "failed to mark image upload as failed")
|
||||
}
|
||||
usageRepoTx := repository.NewImageUsageRepository(tx)
|
||||
@@ -680,7 +709,7 @@ WHERE tenant_id = $1 AND image_asset_id = $2
|
||||
|
||||
imageRepoTx := repository.NewImageRepository(tx)
|
||||
usageRepoTx := repository.NewImageUsageRepository(tx)
|
||||
if err := imageRepoTx.MarkDeleted(ctx, tenantID, assetID); err != nil {
|
||||
if err := imageRepoTx.MarkDeleted(ctx, tenantID, lockedAsset.BrandID, assetID); err != nil {
|
||||
return response.ErrInternal(50044, "image_delete_finalize_failed", "failed to finalize image deletion")
|
||||
}
|
||||
if err := usageRepoTx.Release(ctx, tenantID, lockedAsset.SizeBytes); err != nil {
|
||||
@@ -725,10 +754,15 @@ func (s *ImageService) enqueueFinalizeDeleteTask(tenantID, assetID int64) error
|
||||
}
|
||||
|
||||
func (s *ImageService) loadImageAsset(ctx context.Context, tenantID, assetID int64) (*repository.ImageAsset, error) {
|
||||
return repository.NewImageRepository(s.pool).Get(ctx, tenantID, assetID)
|
||||
brandID, err := s.lookupImageAssetBrandID(ctx, tenantID, assetID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return repository.NewImageRepository(s.pool).Get(ctx, tenantID, brandID, assetID)
|
||||
}
|
||||
|
||||
type lockedImageAsset struct {
|
||||
BrandID int64
|
||||
ObjectKey string
|
||||
SizeBytes int64
|
||||
Status string
|
||||
@@ -739,11 +773,11 @@ func (s *ImageService) lockImageAsset(ctx context.Context, tx pgx.Tx, tenantID,
|
||||
var asset lockedImageAsset
|
||||
var deletedAt *time.Time
|
||||
err := tx.QueryRow(ctx, `
|
||||
SELECT object_key, size_bytes, status, deleted_at
|
||||
SELECT brand_id, object_key, size_bytes, status, deleted_at
|
||||
FROM image_assets
|
||||
WHERE tenant_id = $1 AND id = $2
|
||||
FOR UPDATE
|
||||
`, tenantID, assetID).Scan(&asset.ObjectKey, &asset.SizeBytes, &asset.Status, &deletedAt)
|
||||
`, tenantID, assetID).Scan(&asset.BrandID, &asset.ObjectKey, &asset.SizeBytes, &asset.Status, &deletedAt)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, false, nil
|
||||
@@ -754,14 +788,26 @@ FOR UPDATE
|
||||
return &asset, true, nil
|
||||
}
|
||||
|
||||
func (s *ImageService) ensureFolderExists(ctx context.Context, tenantID, folderID int64) error {
|
||||
func (s *ImageService) ensureFolderExists(ctx context.Context, tenantID, brandID, folderID int64) error {
|
||||
folderRepo := repository.NewImageFolderRepository(s.pool)
|
||||
if _, err := folderRepo.Get(ctx, tenantID, folderID); err != nil {
|
||||
if _, err := folderRepo.Get(ctx, tenantID, brandID, folderID); err != nil {
|
||||
return mapImageFolderNotFound(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ImageService) lookupImageAssetBrandID(ctx context.Context, tenantID, assetID int64) (int64, error) {
|
||||
var brandID int64
|
||||
if err := s.pool.QueryRow(ctx, `
|
||||
SELECT brand_id
|
||||
FROM image_assets
|
||||
WHERE tenant_id = $1 AND id = $2
|
||||
`, tenantID, assetID).Scan(&brandID); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return brandID, nil
|
||||
}
|
||||
|
||||
func (s *ImageService) lookupImageQuotaBytes(ctx context.Context, tenantID int64) (int64, error) {
|
||||
var quotaBytes int64
|
||||
err := s.pool.QueryRow(ctx, `
|
||||
|
||||
Reference in New Issue
Block a user