diff --git a/engine/config.go b/engine/config.go index 3f074337..9ba24070 100644 --- a/engine/config.go +++ b/engine/config.go @@ -254,6 +254,14 @@ type CollectionEntryFeatures struct { // Optional collection specific datasources. Mutually exclusive with top-level defined datasources. Datasources *Datasources `yaml:"datasources"` + + Filters struct { + // OAF Part 1: filter on feature properties + Properties []PropertyFilter `yaml:"properties" validate:"dive"` + + // OAF Part 3: add config for complex/CQL filters here + // placeholder + } `yaml:"filters"` } type CollectionEntryMaps struct { @@ -311,6 +319,15 @@ func (oaf OgcAPIFeatures) ProjectionsForCollection(collectionID string) []string return result } +func (oaf OgcAPIFeatures) PropertyFiltersForCollection(collectionID string) ([]PropertyFilter, error) { + for _, coll := range oaf.Collections { + if coll.ID == collectionID && coll.Features != nil && coll.Features.Filters.Properties != nil { + return coll.Features.Filters.Properties, nil + } + } + return nil, errors.New("not found") +} + type OgcAPIMaps struct { Collections GeoSpatialCollections `yaml:"collections"` } @@ -394,6 +411,11 @@ type GeoPackageCloud struct { Cache *string `yaml:"cache" validate:"omitempty,dir"` } +type PropertyFilter struct { + Name string `yaml:"name" validate:"required"` + Description string `yaml:"description"` +} + type SupportedSrs struct { Srs string `yaml:"srs" validate:"required,startswith=EPSG:"` ZoomLevelRange ZoomLevelRange `yaml:"zoomLevelRange" validate:"required"` diff --git a/engine/templates/openapi/features.go.json b/engine/templates/openapi/features.go.json index 66f25c47..9b8332ca 100644 --- a/engine/templates/openapi/features.go.json +++ b/engine/templates/openapi/features.go.json @@ -41,16 +41,16 @@ } {{ if and $.Params $.Params.PropertyFiltersByCollection }} {{- range $pf := $.Params.PropertyFiltersByCollection -}} - {{- range $propFilterName, $propFilterType := $pf -}} + {{- range $propFilter := $pf -}} ,{ - "name": "{{ $propFilterName }}", + "name": "{{ $propFilter.Name }}", "in": "query", - "description": "The optional blaat.", + "description": "{{ $propFilter.Description }}", "required": false, "style": "form", "explode": false, "schema": { - "type": "{{ $propFilterType }}" + "type": "{{ $propFilter.DataType }}" } } {{ end }} diff --git a/ogc/features/main.go b/ogc/features/main.go index 3caddb74..2cd52a28 100644 --- a/ogc/features/main.go +++ b/ogc/features/main.go @@ -43,23 +43,18 @@ type Features struct { json *jsonFeatures } -type PropertyNamesWithType map[string]string - func NewFeatures(e *engine.Engine, router *chi.Mux) *Features { + collections = cacheCollectionsMetadata(e) + datasources := configureDatasources(e) + + rebuildOpenAPIForFeatures(e, datasources) + f := &Features{ engine: e, - datasources: configureDatasources(e), + datasources: datasources, html: newHTMLFeatures(e), json: newJSONFeatures(e), } - collections = f.cacheCollectionsMetadata() - - // Rebuild OpenAPI spec with additional info from datasources - e.RebuildOpenAPI(struct { - PropertyFiltersByCollection map[string]PropertyNamesWithType - }{ - PropertyFiltersByCollection: f.createPropertyFiltersByCollection(), - }) router.Get(geospatial.CollectionsPath+"/{collectionId}/items", f.CollectionContent()) router.Get(geospatial.CollectionsPath+"/{collectionId}/items/{featureId}", f.Feature()) @@ -77,6 +72,11 @@ func (f *Features) CollectionContent(_ ...any) http.HandlerFunc { } collectionID := chi.URLParam(r, "collectionId") + if _, ok := collections[collectionID]; !ok { + log.Printf("collection %s doesn't exist in this features service", collectionID) + http.NotFound(w, r) + return + } url := featureCollectionURL{*cfg.BaseURL.URL, r.URL.Query(), cfg.OgcAPI.Features.Limit} encodedCursor, limit, inputSRID, outputSRID, contentCrs, bbox, err := url.parse() if err != nil { @@ -87,11 +87,6 @@ func (f *Features) CollectionContent(_ ...any) http.HandlerFunc { http.Error(w, err.Error(), http.StatusBadRequest) return } - if _, ok := collections[collectionID]; !ok { - log.Printf("collection %s doesn't exist in this features service", collectionID) - http.NotFound(w, r) - return - } w.Header().Add(engine.HeaderContentCrs, contentCrs.ToLink()) var newCursor domain.Cursors @@ -165,12 +160,16 @@ func (f *Features) Feature() http.HandlerFunc { } collectionID := chi.URLParam(r, "collectionId") + if _, ok := collections[collectionID]; !ok { + log.Printf("collection %s doesn't exist in this features service", collectionID) + http.NotFound(w, r) + return + } featureID, err := strconv.Atoi(chi.URLParam(r, "featureId")) if err != nil { http.Error(w, "feature ID must be a number", http.StatusBadRequest) return } - url := featureURL{*f.engine.Config.BaseURL.URL, r.URL.Query()} outputSRID, contentCrs, err := url.parse() if err != nil { @@ -181,11 +180,6 @@ func (f *Features) Feature() http.HandlerFunc { http.Error(w, err.Error(), http.StatusBadRequest) return } - if _, ok := collections[collectionID]; !ok { - log.Printf("collection %s doesn't exist in this features service", collectionID) - http.NotFound(w, r) - return - } w.Header().Add(engine.HeaderContentCrs, contentCrs.ToLink()) datasource := f.datasources[DatasourceKey{srid: outputSRID.GetOrDefault(), collectionID: collectionID}] @@ -218,41 +212,14 @@ func (f *Features) Feature() http.HandlerFunc { } } -func (f *Features) cacheCollectionsMetadata() map[string]*engine.GeoSpatialCollectionMetadata { +func cacheCollectionsMetadata(e *engine.Engine) map[string]*engine.GeoSpatialCollectionMetadata { result := make(map[string]*engine.GeoSpatialCollectionMetadata) - for _, collection := range f.engine.Config.OgcAPI.Features.Collections { + for _, collection := range e.Config.OgcAPI.Features.Collections { result[collection.ID] = collection.Metadata } return result } -func (f *Features) createPropertyFiltersByCollection() map[string]PropertyNamesWithType { - propertyFiltersByCollection := make(map[string]PropertyNamesWithType) - for k, v := range f.datasources { - metadata, err := v.GetFeatureTableMetadata(k.collectionID) - if err != nil { - continue - } - propertyFilters := make(PropertyNamesWithType) - for name, dataType := range metadata.ColumnsWithDataType() { - // translate database data types to OpenAPI data types - switch dataType { - case "INTEGER": - dataType = "integer" - case "REAL": - dataType = "number" - case "TEXT": - dataType = "string" - default: - dataType = "string" - } - propertyFilters[name] = dataType - } - propertyFiltersByCollection[k.collectionID] = propertyFilters - } - return propertyFiltersByCollection -} - func configureDatasources(e *engine.Engine) map[DatasourceKey]ds.Datasource { result := make(map[DatasourceKey]ds.Datasource, len(e.Config.OgcAPI.Features.Collections)) diff --git a/ogc/features/openapi.go b/ogc/features/openapi.go new file mode 100644 index 00000000..1f5cf2b9 --- /dev/null +++ b/ogc/features/openapi.go @@ -0,0 +1,72 @@ +package features + +import ( + "strings" + + "github.com/PDOK/gokoala/engine" + ds "github.com/PDOK/gokoala/ogc/features/datasources" +) + +type OpenAPIPropertyFilter struct { + Name string + Description string + DataType string +} + +// rebuildOpenAPIForFeatures Rebuild OpenAPI spec with additional info from given datasources +func rebuildOpenAPIForFeatures(e *engine.Engine, datasources map[DatasourceKey]ds.Datasource) { + e.RebuildOpenAPI(struct { + PropertyFiltersByCollection map[string][]OpenAPIPropertyFilter + }{ + PropertyFiltersByCollection: createPropertyFiltersByCollection(e.Config.OgcAPI.Features, datasources), + }) +} + +func createPropertyFiltersByCollection( + oafConfig *engine.OgcAPIFeatures, + datasources map[DatasourceKey]ds.Datasource) map[string][]OpenAPIPropertyFilter { + + result := make(map[string][]OpenAPIPropertyFilter) + for k, datasource := range datasources { + filtersConfig, err := oafConfig.PropertyFiltersForCollection(k.collectionID) + if err != nil { + continue + } + featTable, err := datasource.GetFeatureTableMetadata(k.collectionID) + if err != nil { + continue + } + featTableColumns := featTable.ColumnsWithDataType() + propertyFilters := make([]OpenAPIPropertyFilter, 0, len(featTableColumns)) + for name, dataType := range featTableColumns { + for _, fc := range filtersConfig { + if fc.Name == name { + dataType = datasourceToOpenAPI(dataType) + propertyFilters = append(propertyFilters, OpenAPIPropertyFilter{ + Name: name, + Description: fc.Description, + DataType: dataType, + }) + } + } + + } + result[k.collectionID] = propertyFilters + } + return result +} + +// translate database data types to OpenAPI data types +func datasourceToOpenAPI(dataType string) string { + switch strings.ToUpper(dataType) { + case "INTEGER": + dataType = "integer" + case "REAL", "NUMERIC": + dataType = "number" + case "TEXT", "VARCHAR": + dataType = "string" + default: + dataType = "string" + } + return dataType +}