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:
2026-09-05 13:29:01 +05:00
parent 32eba016b3
commit bc524f3d98
11 changed files with 1366 additions and 34 deletions

View File

@@ -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