feat(dataset-groups): CRUD, latest-in-group, group SLD with dataset fallback, CreateRasterFromFile
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -86,6 +86,12 @@ type categoryReader interface {
|
||||
GetByCode(ctx context.Context, code string) (domain.Category, error)
|
||||
}
|
||||
|
||||
// groupReader lets the dataset service verify a dataset group exists before a
|
||||
// dataset is filed under it, and resolve its shared SLD as a fallback.
|
||||
type groupReader interface {
|
||||
GetByID(ctx context.Context, id uuid.UUID) (domain.DatasetGroup, error)
|
||||
}
|
||||
|
||||
// JobEnqueuer schedules asynchronous dataset jobs.
|
||||
type JobEnqueuer interface {
|
||||
EnqueueParse(ctx context.Context, datasetID uuid.UUID) error
|
||||
@@ -138,6 +144,7 @@ type DatasetService struct {
|
||||
repo DatasetRepository
|
||||
store ObjectStore
|
||||
categories categoryReader
|
||||
groups groupReader
|
||||
jobs JobEnqueuer
|
||||
parseColumns ColumnParser
|
||||
parseRows RowParser
|
||||
@@ -145,12 +152,14 @@ type DatasetService struct {
|
||||
}
|
||||
|
||||
// NewDatasetService wires the dataset repository, object store, category reader
|
||||
// (for parent validation), the job enqueuer, the column/row parsers, and the
|
||||
// raster converter.
|
||||
// (for parent validation), the dataset-group reader (for group validation and
|
||||
// the SLD fallback), the job enqueuer, the column/row parsers, and the raster
|
||||
// converter.
|
||||
func NewDatasetService(
|
||||
repo DatasetRepository,
|
||||
store ObjectStore,
|
||||
categories categoryReader,
|
||||
groups groupReader,
|
||||
jobs JobEnqueuer,
|
||||
parseColumns ColumnParser,
|
||||
parseRows RowParser,
|
||||
@@ -160,6 +169,7 @@ func NewDatasetService(
|
||||
repo: repo,
|
||||
store: store,
|
||||
categories: categories,
|
||||
groups: groups,
|
||||
jobs: jobs,
|
||||
parseColumns: parseColumns,
|
||||
parseRows: parseRows,
|
||||
@@ -196,6 +206,9 @@ func (s *DatasetService) Upload(ctx context.Context, in UploadInput) (domain.Dat
|
||||
if err := s.ensureCategoryExists(ctx, in.CategoryID); err != nil {
|
||||
return domain.Dataset{}, err
|
||||
}
|
||||
if err := s.ensureGroupExists(ctx, in.DatasetGroupID); err != nil {
|
||||
return domain.Dataset{}, err
|
||||
}
|
||||
if err := domain.ValidateDatePeriod(in.StartDate, in.EndDate); err != nil {
|
||||
return domain.Dataset{}, err
|
||||
}
|
||||
@@ -311,7 +324,23 @@ func (s *DatasetService) DownloadSLD(ctx context.Context, id uuid.UUID) (domain.
|
||||
return domain.Dataset{}, nil, err
|
||||
}
|
||||
if dataset.SldStorageKey == nil {
|
||||
return domain.Dataset{}, nil, ErrSLDNotFound
|
||||
// Fall back to the dataset group's shared style, if the group has one.
|
||||
if dataset.DatasetGroupID == nil {
|
||||
return domain.Dataset{}, nil, ErrSLDNotFound
|
||||
}
|
||||
group, err := s.groups.GetByID(ctx, *dataset.DatasetGroupID)
|
||||
if err != nil {
|
||||
if errors.Is(err, domain.ErrNotFound) {
|
||||
return domain.Dataset{}, nil, ErrSLDNotFound
|
||||
}
|
||||
return domain.Dataset{}, nil, err
|
||||
}
|
||||
if group.SldStorageKey == nil {
|
||||
return domain.Dataset{}, nil, ErrSLDNotFound
|
||||
}
|
||||
dataset.SldStorageKey = group.SldStorageKey
|
||||
dataset.SldFilename = group.SldFilename
|
||||
dataset.SldContentType = group.SldContentType
|
||||
}
|
||||
obj, err := s.store.Get(ctx, *dataset.SldStorageKey)
|
||||
if err != nil {
|
||||
@@ -320,6 +349,48 @@ func (s *DatasetService) DownloadSLD(ctx context.Context, id uuid.UUID) (domain.
|
||||
return dataset, obj, nil
|
||||
}
|
||||
|
||||
// CreateRasterInput describes a raster file already on local disk that should
|
||||
// become a dataset (used by the Landscape Program to publish results).
|
||||
type CreateRasterInput struct {
|
||||
CategoryID uuid.UUID
|
||||
DatasetGroupID *uuid.UUID
|
||||
Name string
|
||||
Filename string // stored filename, e.g. "lp-z-integrated.tif"
|
||||
Path string // local path to read from
|
||||
StartDate *string
|
||||
EndDate *string
|
||||
Meta json.RawMessage
|
||||
Automated bool
|
||||
}
|
||||
|
||||
// CreateRasterFromFile stores a local GeoTIFF as a new raster dataset and
|
||||
// enqueues the standard COG conversion, exactly like an uploaded raster.
|
||||
func (s *DatasetService) CreateRasterFromFile(ctx context.Context, in CreateRasterInput) (domain.Dataset, error) {
|
||||
f, err := os.Open(in.Path)
|
||||
if err != nil {
|
||||
return domain.Dataset{}, err
|
||||
}
|
||||
defer f.Close()
|
||||
info, err := f.Stat()
|
||||
if err != nil {
|
||||
return domain.Dataset{}, err
|
||||
}
|
||||
return s.Upload(ctx, UploadInput{
|
||||
CategoryID: in.CategoryID,
|
||||
DatasetGroupID: in.DatasetGroupID,
|
||||
Name: in.Name,
|
||||
Meta: in.Meta,
|
||||
Automated: in.Automated,
|
||||
Filename: in.Filename,
|
||||
FileType: domain.FileTypeRaster,
|
||||
ContentType: "image/tiff",
|
||||
Size: info.Size(),
|
||||
Reader: f,
|
||||
StartDate: in.StartDate,
|
||||
EndDate: in.EndDate,
|
||||
})
|
||||
}
|
||||
|
||||
// deriveSLDKey places a dataset's SLD style file alongside the original under
|
||||
// an sld/ prefix, named after the uploaded SLD file.
|
||||
func deriveSLDKey(storageKey, filename string) string {
|
||||
@@ -1099,6 +1170,9 @@ func (s *DatasetService) Update(ctx context.Context, id uuid.UUID, in DatasetUpd
|
||||
if err := s.ensureCategoryExists(ctx, in.CategoryID); err != nil {
|
||||
return domain.Dataset{}, err
|
||||
}
|
||||
if err := s.ensureGroupExists(ctx, in.DatasetGroupID); err != nil {
|
||||
return domain.Dataset{}, err
|
||||
}
|
||||
if err := domain.ValidateDatePeriod(in.StartDate, in.EndDate); err != nil {
|
||||
return domain.Dataset{}, err
|
||||
}
|
||||
@@ -1163,6 +1237,9 @@ func (s *DatasetService) Patch(ctx context.Context, id uuid.UUID, in DatasetPatc
|
||||
groupID := current.DatasetGroupID
|
||||
if in.DatasetGroupIDSet {
|
||||
groupID = in.DatasetGroupID
|
||||
if err := s.ensureGroupExists(ctx, groupID); err != nil {
|
||||
return domain.Dataset{}, err
|
||||
}
|
||||
}
|
||||
startDate := current.StartDate
|
||||
if in.StartDateSet {
|
||||
@@ -1199,6 +1276,21 @@ func (s *DatasetService) ensureCategoryExists(ctx context.Context, categoryID uu
|
||||
return nil
|
||||
}
|
||||
|
||||
// ensureGroupExists validates that a non-nil dataset group id references an
|
||||
// existing group. A nil id (no group) is always valid.
|
||||
func (s *DatasetService) ensureGroupExists(ctx context.Context, id *uuid.UUID) error {
|
||||
if id == nil {
|
||||
return nil
|
||||
}
|
||||
if _, err := s.groups.GetByID(ctx, *id); err != nil {
|
||||
if errors.Is(err, domain.ErrNotFound) {
|
||||
return fmt.Errorf("%w: dataset group %s not found", domain.ErrValidation, id)
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Status-wait bounds and polling cadence for long polling.
|
||||
const (
|
||||
DefaultStatusWait = 25 * time.Second
|
||||
|
||||
Reference in New Issue
Block a user