diff --git a/API.md b/API.md index 937720e0..b2ad5153 100644 --- a/API.md +++ b/API.md @@ -5,6 +5,7 @@ * [OGC API for Features version 1.0](http://docs.opengeospatial.org/is/17-069r3/17-069r3.html) * [OGC API - Features - Part 3: Filtering and the Common Query Language (CQL)](https://portal.ogc.org/files/96288) * [OpenAPI Specifcation version 3.0.2](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md) +* [OGC API - Features - Part 4: Create, Replace, Update and Delete](https://docs.ogc.org/DRAFTS/20-002.html) ## Notes @@ -63,10 +64,12 @@ JSON document containing feature collection metadata. * items - `/collections/{cid}/items.html` - Features as HTML ## Features +Access to features in a collection. +### GET Produces a dataset of items from the collection (as GeoJSON) -### Request +#### Request Path: `/collections/{cid}/items` ### Parameters @@ -88,7 +91,7 @@ Usually used with an aggregate `transform` function. * `limit=N` - limits the number of features in the response. * `offset=N` - starts the response at an offset. -### Response +#### Response GeoJSON document containing the features resulting from the request query. @@ -99,22 +102,63 @@ GeoJSON document containing the features resulting from the request query. * next - TBD * prev - TBD +### POST +Create a feature in collection. + +#### Request +Path: `/collections/{cid}/items` +Content: JSON document representing a geojson feature. + +#### Response +Empty response with 201 HTTP Status Code. + ## Feature +Provides access to one collection feature. -### Request +### GET +Get one collection feature. + +#### Request Path: `/collections/{cid}/items/{fid}` -#### Parameters +##### Parameters * `properties=PROP-LIST`- return only the given properties (comma-separated) * `transform` - transform the feature geometry by the given geometry function pipeline -### Response +#### Response -#### Links +##### Links * self - `/collections/{cid}/items/{fid}.json` - This document as JSON * alternate - `/collections/{cid}/items/{fid}.html` - This document as HTML * collection - `/collections/{cid}` - The collection document +### PUT +Replace one collection feature. +#### Request +Path: `/collections/{cid}/items/{fid}` +Content: JSON document representing a geojson feature. + +#### Response +Empty response with 200 HTTP Status Code. + +### PATCH +Update one collection feature. +#### Request +Path: `/collections/{cid}/items/{fid}` +Content: JSON document representing a geojson feature. + +#### Response +Empty response with 200 HTTP Status Code. + +### DELETE +Delete one collection feature. + +#### Request +Path: `/collections/{cid}/items/{fid}` + +#### Response +Empty response with 200 HTTP Status Code. + ## Functions Lists the functions provided by the service. diff --git a/FEATURES.md b/FEATURES.md index 2d9a72b6..16463bf8 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -87,13 +87,19 @@ It includes [*OGC API - Features*](http://docs.opengeospatial.org/is/17-069r3/17 ### Output formats - [x] GeoJSON +- [ ] GML - [x] JSON for metadata - [x] JSON for non-geometry functions - [ ] `next` link - [ ] `prev` link +### Input formats +- [x] GeoJSON +- [ ] GML + ### Transactions -- [ ] Support POST, PUT, PATCH, DELETE... TBD +- [X] Support POST, PUT, PATCH, DELETE on tables with primary key +- [ ] Support Optimistic locking ## User Interface (HTML) - [x] `/home.html` landing page diff --git a/README.md b/README.md index 148e82e0..ac4140a9 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,9 @@ See also our companion project [`pg_tileserv`](https://github.com/CrunchyData/pg * Standard query parameters: `limit`, `bbox`, `bbox-crs`, property filtering, `sortby`, `crs` * Query parameters `filter` and `filter-crs` allow [CQL filtering](https://portal.ogc.org/files/96288), with spatial support * Extended query parameters: `offset`, `properties`, `transform`, `precision`, `groupby` + * Transactions (Create, Update, Replace, Delete) * Data responses are formatted in JSON and [GeoJSON](https://www.rfc-editor.org/rfc/rfc7946.txt) +* Request content for transactions supports [GeoJSON](https://www.rfc-editor.org/rfc/rfc7946.txt) * Provides a simple HTML user interface, with web maps to view spatial data * Uses the power of PostgreSQL to reduce the amount of code and to make data definition easy and familiar. @@ -46,6 +48,8 @@ For a full list of software capabilities see [FEATURES](FEATURES.md). * [*OGC API - Features - Part 2: Coordinate Reference Systems by Reference*](https://docs.ogc.org/is/18-058/18-058.html) * [**DRAFT** *OGC API - Features - Part 3: Filtering*](http://docs.ogc.org/DRAFTS/19-079r1.html) * [**DRAFT** *Common Query Language (CQL2)*](https://docs.ogc.org/DRAFTS/21-065.html) +* [**DRAFT** *OGC API - Features - Part 4: Create, Replace, Update and Delete*](https://docs.ogc.org/DRAFTS/20-002.html) + * [*GeoJSON*](https://www.rfc-editor.org/rfc/rfc7946.txt) ## Download diff --git a/hugo/content/usage/_index.md b/hugo/content/usage/_index.md index 181895f7..b02f8a9f 100644 --- a/hugo/content/usage/_index.md +++ b/hugo/content/usage/_index.md @@ -10,5 +10,6 @@ This section describes how to use `pg_featureserv`. It covers the following topi * How the [Web Service API](./api/) works * How to publish [feature collections](./collections/) backed by PostGIS tables or views * How to [query features](./query_data/) from feature collections +* How to [create update replace delete feature](./create_update_replace_delete_feature/) in feature collections * How to publish database [functions](./functions/) * How to [execute functions](./query_function/) diff --git a/hugo/content/usage/create_update_replace_delete_feature.md b/hugo/content/usage/create_update_replace_delete_feature.md new file mode 100644 index 00000000..13a6b0fc --- /dev/null +++ b/hugo/content/usage/create_update_replace_delete_feature.md @@ -0,0 +1,63 @@ +--- +title: "Create Replace Delete Feature" +date: +draft: false +weight: 150 +--- + +Transaction on Feature collections is supported. + +## Create feature + +POST query to the path `/collections/{collid}/items` allows to create +a new feature in a feature collection. + +The geojson feature must be part of the request body. +If the geometry geometry crs is different from the storage crs, the geometry will be transformed. +Missing properties will be ignored and the table default value for the column will be applied. +The id specified in the body is ignored and the database default value is used to create the feature. + +#### Example +``` +curl -i --request "POST" 'http://localhost:9000/collections/public.tramway_stations/items' -d '{"type":"Feature","id":"129","geometry":{"type":"Point","coordinates":[-71.222868058,46.836016945,0]},"properties":{"description":null,"diffusion":"Publique","niveau_rstc":"Tramway","nom":"Hôpital Enfant-Jésus","objectid":129,"type_station":"Reguliere"}}' +``` +## Update feature + +PATCH query to the path `/collections/{collid}/items/{fid}` allows to update +a feature in a feature collection. + +The geojson feature must be part of the request body. +If the geometry geometry crs is different from the storage crs, the geometry will be transformed. +Missing properties will not be updated. +The id specified in the body is ignored. + +#### Example +``` +curl -i --request "PUT" 'http://localhost:9000/collections/public.tramway_stations/items/129.json' -d '{"type":"Feature","id":"129","geometry":{"type":"Point","coordinates":[-71.222868058,46.836016945,0]},"properties":{"description":null,"diffusion":"Publique","niveau_rstc":"Tramway","nom":"Hôpital Enfant-Jésus","objectid":129,"type_station":"Reguliere"}}' +``` + +## Replace feature + +PUT query to the path `/collections/{collid}/items/{fid}` allows to replace +a feature in a feature collection. + +The geojson feature must be part of the request body. +If the geometry geometry crs is different from the storage crs, the geometry will be transformed. +Missing properties will be replaced with null (unless a database trigger is applied) +The id specified in the body is ignored. + +#### Example +``` +curl -i --request "PUT" 'http://localhost:9000/collections/public.tramway_stations/items/129.json' -d '{"type":"Feature","id":"129","geometry":{"type":"Point","coordinates":[-71.222868058,46.836016945,0]},"properties":{"description":null,"diffusion":"Publique","niveau_rstc":"Tramway","nom":"Hôpital Enfant-Jésus","objectid":129,"type_station":"Reguliere"}}' +``` + +## Delete feature + +DELETE query to the path `/collections/{collid}/items/{fid}` allows to delete +a feature in a feature collection. + +#### Example +``` +curl -i --request "Delete" 'http://localhost:9000/collections/public.tramway_stations/items/129.json' +``` + diff --git a/internal/api/api.go b/internal/api/api.go index 92c83a02..45b5a8bf 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -482,6 +482,9 @@ var conformance = Conformance{ "http://www.opengis.net/spec/ogcapi-common-1/1.0/conf/oas30", "http://www.opengis.net/spec/ogcapi-common-2/1.0/conf/collections", "http://www.opengis.net/spec/ogcapi-common-2/1.0/conf/simple-query", + "http://www.opengis.net/spec/ogcapi-features-4/1.0/conf/create-replace-delete", + "http://www.opengis.net/spec/ogcapi-features-4/1.0/conf/update", + "http://www.opengis.net/spec/ogcapi-features-4/1.0/conf/features", }, } diff --git a/internal/api/openapi.go b/internal/api/openapi.go index 263f70da..03ffa22f 100644 --- a/internal/api/openapi.go +++ b/internal/api/openapi.go @@ -355,10 +355,36 @@ func GetOpenAPIContent(urlBase string) *openapi3.Swagger { }, }, }, + Post: &openapi3.Operation{ + OperationID: "createCollectionFeature", + Parameters: openapi3.Parameters{ + ¶mCollectionID, + }, + RequestBody: &openapi3.RequestBodyRef{ + Value: &openapi3.RequestBody{ + Description: "Feature", + Required: true, + /* + // TODO: create schema for input? + Content: openapi3.NewContentWithJSONSchemaRef( + &openapi3.SchemaRef{ + Ref: "http://geojson.org/schema/Feature.json", + }, + ), + */ + }, + }, + Responses: openapi3.Responses{ + "201": &openapi3.ResponseRef{ + Value: &openapi3.Response{ + Description: "Created"}, + }, + }, + }, }, apiBase + "collections/{collectionId}/items/{featureId}": &openapi3.PathItem{ - Summary: "Single feature data from collection", - Description: "Provides access to a single feature identitfied by {featureId} from the specified collection", + Summary: "Feature in collection", + Description: "Gets, Replaces or Deletes Single Feature in collection.", Get: &openapi3.Operation{ OperationID: "getCollectionFeature", Parameters: openapi3.Parameters{ @@ -393,6 +419,100 @@ func GetOpenAPIContent(urlBase string) *openapi3.Swagger { }, }, }, + Patch: &openapi3.Operation{ + OperationID: "updateCollectionFeature", + Parameters: openapi3.Parameters{ + ¶mCollectionID, + &openapi3.ParameterRef{ + Value: &openapi3.Parameter{ + Name: "featureId", + Description: "ID of feature in collection to update.", + In: "path", + Required: true, + Schema: &openapi3.SchemaRef{Value: openapi3.NewStringSchema()}, + AllowEmptyValue: false, + }, + }, + }, + RequestBody: &openapi3.RequestBodyRef{ + Value: &openapi3.RequestBody{ + Description: "Feature", + Required: true, + /* + // TODO: create schema for input? + Content: openapi3.NewContentWithJSONSchemaRef( + &openapi3.SchemaRef{ + Ref: "http://geojson.org/schema/Feature.json", + }, + ), + */ + }, + }, + Responses: openapi3.Responses{ + "204": &openapi3.ResponseRef{ + Value: &openapi3.Response{ + Description: "No Content"}, + }, + }, + }, + Put: &openapi3.Operation{ + OperationID: "replaceCollectionFeature", + Parameters: openapi3.Parameters{ + ¶mCollectionID, + &openapi3.ParameterRef{ + Value: &openapi3.Parameter{ + Name: "featureId", + Description: "ID of feature in collection to replace.", + In: "path", + Required: true, + Schema: &openapi3.SchemaRef{Value: openapi3.NewStringSchema()}, + AllowEmptyValue: false, + }, + }, + }, + RequestBody: &openapi3.RequestBodyRef{ + Value: &openapi3.RequestBody{ + Description: "Feature", + Required: true, + /* + // TODO: create schema for input? + Content: openapi3.NewContentWithJSONSchemaRef( + &openapi3.SchemaRef{ + Ref: "http://geojson.org/schema/Feature.json", + }, + ), + */ + }, + }, + Responses: openapi3.Responses{ + "204": &openapi3.ResponseRef{ + Value: &openapi3.Response{ + Description: "No Content"}, + }, + }, + }, + Delete: &openapi3.Operation{ + OperationID: "deleteCollectionFeature", + Parameters: openapi3.Parameters{ + ¶mCollectionID, + &openapi3.ParameterRef{ + Value: &openapi3.Parameter{ + Name: "featureId", + Description: "ID of feature in collection to delete.", + In: "path", + Required: true, + Schema: &openapi3.SchemaRef{Value: openapi3.NewStringSchema()}, + AllowEmptyValue: false, + }, + }, + }, + Responses: openapi3.Responses{ + "204": &openapi3.ResponseRef{ + Value: &openapi3.Response{ + Description: "No Content"}, + }, + }, + }, }, apiBase + "functions": &openapi3.PathItem{ Summary: "Functions metadata", diff --git a/internal/data/catalog.go b/internal/data/catalog.go index b56590f6..ae9ddf85 100644 --- a/internal/data/catalog.go +++ b/internal/data/catalog.go @@ -47,6 +47,18 @@ type Catalog interface { // It returns an empty string if the table or feature does not exist TableFeature(ctx context.Context, name string, id string, param *QueryParam) (string, error) + // ReplaceTableFeature replaces a feature + ReplaceTableFeature(ctx context.Context, name string, id string, feature Feature) error + + // UpdateTableFeature updates a feature + UpdateTableFeature(ctx context.Context, name string, id string, feature Feature) error + + // CreateTableFeature creates a feature + CreateTableFeature(ctx context.Context, name string, feature Feature) error + + // DeleteTableFeature deletes a feature + DeleteTableFeature(ctx context.Context, name string, id string) error + Functions() ([]*Function, error) // FunctionByName returns the function with given name. @@ -163,3 +175,17 @@ func FunctionQualifiedId(name string) string { } return SchemaPostGISFTW + "." + name } + +type Geometry struct { + Type string `json:"type"` + Coordinates interface{} `json:"coordinates"` + CRS map[string]interface{} `json:"crs,omitempty"` +} + +// A Feature corresponds to GeoJSON feature object +type Feature struct { + ID interface{} `json:"id,omitempty"` + Type string `json:"type"` + Geometry *Geometry `json:"geometry"` + Properties map[string]interface{} `json:"properties"` +} diff --git a/internal/data/catalog_db.go b/internal/data/catalog_db.go index acbc1107..7e64f732 100644 --- a/internal/data/catalog_db.go +++ b/internal/data/catalog_db.go @@ -234,6 +234,78 @@ func (cat *catalogDB) TableFeature(ctx context.Context, name string, id string, return features[0], nil } +func (cat *catalogDB) CreateTableFeature(ctx context.Context, name string, feature Feature) error { + tbl, err := cat.TableByName(name) + if err != nil { + return err + } + sql, argValues, err := sqlCreateFeature(tbl, feature) + log.Debug("Create feature query: " + sql) + result, err := cat.dbconn.Exec(ctx, sql, argValues...) + if err != nil { + return err + } + rows := result.RowsAffected() + if rows != 1 { + return fmt.Errorf("expected to affect 1 row, affected %d", rows) + } + return nil +} + +func (cat *catalogDB) ReplaceTableFeature(ctx context.Context, name string, id string, feature Feature) error { + tbl, err := cat.TableByName(name) + if err != nil { + return err + } + sql, argValues, err := sqlReplaceFeature(tbl, id, feature) + log.Debug("Replace feature query: " + sql) + result, err := cat.dbconn.Exec(ctx, sql, argValues...) + if err != nil { + return err + } + rows := result.RowsAffected() + if rows != 1 { + return fmt.Errorf("expected to affect 1 row, affected %d", rows) + } + return nil +} + +func (cat *catalogDB) UpdateTableFeature(ctx context.Context, name string, id string, feature Feature) error { + tbl, err := cat.TableByName(name) + if err != nil { + return err + } + sql, argValues, err := sqlUpdateFeature(tbl, id, feature) + log.Debug("Update feature query: " + sql) + result, err := cat.dbconn.Exec(ctx, sql, argValues...) + if err != nil { + return err + } + rows := result.RowsAffected() + if rows != 1 { + return fmt.Errorf("expected to affect 1 row, affected %d", rows) + } + return nil +} + +func (cat *catalogDB) DeleteTableFeature(ctx context.Context, name string, id string) error { + tbl, err := cat.TableByName(name) + if err != nil { + return err + } + sql, argValues := sqlDeleteFeature(tbl, id) + log.Debug("Delete feature query: " + sql) + result, err := cat.dbconn.Exec(ctx, sql, argValues...) + if err != nil { + return err + } + rows := result.RowsAffected() + if rows != 1 { + return fmt.Errorf("expected to affect 1 row, affected %d", rows) + } + return nil +} + func (cat *catalogDB) refreshTables(force bool) { // TODO: refresh on timed basis? if force || isStartup { diff --git a/internal/data/catalog_mock.go b/internal/data/catalog_mock.go index f5d339d1..57639230 100644 --- a/internal/data/catalog_mock.go +++ b/internal/data/catalog_mock.go @@ -242,6 +242,22 @@ func (cat *CatalogMock) TableFeature(ctx context.Context, name string, id string return features[index].toJSON(propNames), nil } +func (cat *CatalogMock) CreateTableFeature(ctx context.Context, name string, feature Feature) error { + return nil +} + +func (cat *CatalogMock) ReplaceTableFeature(ctx context.Context, name string, id string, feature Feature) error { + return nil +} + +func (cat *CatalogMock) UpdateTableFeature(ctx context.Context, name string, id string, feature Feature) error { + return nil +} + +func (cat *CatalogMock) DeleteTableFeature(ctx context.Context, name string, id string) error { + return nil +} + func (cat *CatalogMock) Functions() ([]*Function, error) { return cat.FunctionDefs, nil } diff --git a/internal/data/db_sql.go b/internal/data/db_sql.go index be9a456f..0c266051 100644 --- a/internal/data/db_sql.go +++ b/internal/data/db_sql.go @@ -1,6 +1,7 @@ package data import ( + "encoding/json" "fmt" "strconv" "strings" @@ -22,7 +23,6 @@ import ( */ const forceTextTSVECTOR = "tsvector" - const sqlTables = `SELECT Format('%s.%s', n.nspname, c.relname) AS id, n.nspname AS schema, @@ -57,6 +57,7 @@ AND has_table_privilege(c.oid, 'select') AND postgis_typmod_srid(a.atttypmod) > 0 ORDER BY id ` + const sqlFunctionsTemplate = `WITH proargs AS ( SELECT p.oid, @@ -191,6 +192,110 @@ func sqlFeature(tbl *Table, param *QueryParam) string { return sql } +func getColumnValues(tbl *Table, feature Feature, includeOnlySetProperties bool) ([]string, []string, []interface{}) { + var columnNames, columnIndex []string + var columnValues []interface{} + var i = 2 + + for _, column := range tbl.Columns { + val, ok := feature.Properties[column] + + if !includeOnlySetProperties || (ok && val != nil) { + columnNames = append(columnNames, column) + columnIndex = append(columnIndex, fmt.Sprintf("$%v", i)) + columnValues = append(columnValues, val) + i++ + } + } + + return columnNames, columnIndex, columnValues +} + +func buildGeometrySQL(tbl *Table) string { + if len(tbl.Columns) > 0 { + return fmt.Sprintf("ST_Transform(ST_GeomFromGeoJSON($1),%v)", tbl.Srid) + } + return fmt.Sprintf("ST_Transform(ST_GeomFromGeoJSON($1),%v)", tbl.Srid) +} + +func buildUpdateSetClause(columnNames []string, columnIndex []string) string { + var setClause string + for index := 0; index < len(columnNames); index++ { + setClause += fmt.Sprintf(", %s=%s", columnNames[index], columnIndex[index]) + } + return setClause +} + +func sqlCreateFeature(tbl *Table, feature Feature) (string, []interface{}, error) { + columnNames, columnIndex, columnValues := getColumnValues(tbl, feature, true) + + columnNamesStr := strings.Join(columnNames, ",") + columnIndexStr := strings.Join(columnIndex, ",") + geomSQL := buildGeometrySQL(tbl) + + sql := fmt.Sprintf("INSERT INTO \"%s\".\"%s\" (%s, %s) VALUES (%s, %v);", tbl.Schema, tbl.Table, columnNamesStr, tbl.GeometryColumn, columnIndexStr, geomSQL) + + var err error + argValues := make([]interface{}, len(columnValues)+1) + argValues[0], err = json.Marshal(feature.Geometry) + if err != nil { + return "", nil, err + } + + copy(argValues[1:], columnValues) + + return sql, argValues, nil +} + +func sqlReplaceFeature(tbl *Table, id string, feature Feature) (string, []interface{}, error) { + columnNames, columnIndex, columnValues := getColumnValues(tbl, feature, false) + + geomSQL := buildGeometrySQL(tbl) + setClause := buildUpdateSetClause(columnNames, columnIndex) + + sql := fmt.Sprintf("UPDATE \"%s\".\"%s\" SET %s=%v%s WHERE \"%v\" = $%v;", tbl.Schema, tbl.Table, tbl.GeometryColumn, geomSQL, setClause, tbl.IDColumn, len(tbl.Columns)+2) + + var err error + argValues := make([]interface{}, len(columnValues)+2) + argValues[0], err = json.Marshal(feature.Geometry) + if err != nil { + return "", nil, err + } + + copy(argValues[1:], columnValues) + argValues[len(columnValues)+1] = id + + return sql, argValues, nil +} + +func sqlUpdateFeature(tbl *Table, id string, feature Feature) (string, []interface{}, error) { + columnNames, columnIndex, columnValues := getColumnValues(tbl, feature, true) + + geomSQL := buildGeometrySQL(tbl) + setClause := buildUpdateSetClause(columnNames, columnIndex) + + sql := fmt.Sprintf("UPDATE \"%s\".\"%s\" SET %s=%v%s WHERE \"%v\" = $%v;", tbl.Schema, tbl.Table, tbl.GeometryColumn, geomSQL, setClause, tbl.IDColumn, len(tbl.Columns)+2) + + var err error + argValues := make([]interface{}, len(columnValues)+2) + argValues[0], err = json.Marshal(feature.Geometry) + if err != nil { + return "", nil, err + } + + copy(argValues[1:], columnValues) + argValues[len(columnValues)+1] = id + + return sql, argValues, nil +} + +func sqlDeleteFeature(tbl *Table, id string) (string, []interface{}) { + sql := fmt.Sprintf("DELETE FROM \"%s\".\"%s\" WHERE \"%v\" = $1;", tbl.Schema, tbl.Table, tbl.IDColumn) + argValues := make([]interface{}, 1) + argValues[0] = id + return sql, argValues +} + func sqlCqlFilter(sql string) string { //log.Debug("SQL = " + sql) if len(sql) == 0 { diff --git a/internal/service/handler.go b/internal/service/handler.go index 32c82a5d..8ab64222 100644 --- a/internal/service/handler.go +++ b/internal/service/handler.go @@ -16,7 +16,9 @@ package service import ( "bytes" "context" + "encoding/json" "fmt" + "io/ioutil" "net/http" "strings" @@ -262,32 +264,52 @@ func handleCollectionItems(w http.ResponseWriter, r *http.Request) *appError { //--- extract request parameters name := getRequestVar(routeVarID, r) - reqParam, err := parseRequestParams(r) - if err != nil { - return appErrorMsg(err, err.Error(), http.StatusBadRequest) - } + ctx := r.Context() + switch r.Method { + case http.MethodGet: + reqParam, err := parseRequestParams(r) + if err != nil { + return appErrorMsg(err, err.Error(), http.StatusBadRequest) + } - tbl, err1 := catalogInstance.TableByName(name) - if err1 != nil { - return appErrorInternalFmt(err1, api.ErrMsgCollectionAccess, name) - } - if tbl == nil { - return appErrorNotFoundFmt(err1, api.ErrMsgCollectionNotFound, name) - } - param, err := createQueryParams(&reqParam, tbl.Columns, tbl.Srid) - if err != nil { - return appErrorBadRequest(err, err.Error()) - } - param.Filter = parseFilter(reqParam.Values, tbl.DbTypes) + tbl, err1 := catalogInstance.TableByName(name) + if err1 != nil { + return appErrorInternalFmt(err1, api.ErrMsgCollectionAccess, name) + } + if tbl == nil { + return appErrorNotFoundFmt(err1, api.ErrMsgCollectionNotFound, name) + } + param, err := createQueryParams(&reqParam, tbl.Columns, tbl.Srid) + if err != nil { + return appErrorBadRequest(err, err.Error()) + } + param.Filter = parseFilter(reqParam.Values, tbl.DbTypes) - ctx := r.Context() - switch format { - case api.FormatJSON: - return writeItemsJSON(ctx, w, name, param, urlBase) - case api.FormatHTML: - return writeItemsHTML(w, tbl, name, query, urlBase) + switch format { + case api.FormatJSON: + return writeItemsJSON(ctx, w, name, param, urlBase) + case api.FormatHTML: + return writeItemsHTML(w, tbl, name, query, urlBase) + } + return nil + case http.MethodPost: + body, err := ioutil.ReadAll(r.Body) + if err != nil { + return appErrorInternalFmt(err, api.ErrMsgInvalidQuery) + } + print(body) + var feature data.Feature + err = json.Unmarshal([]byte(body), &feature) + if err != nil { + return appErrorInternalFmt(err, api.ErrMsgInvalidQuery) + } + catalogInstance.CreateTableFeature(ctx, name, feature) + w.WriteHeader(http.StatusCreated) + return nil + + default: + return appErrorInternalFmt(fmt.Errorf("Method not allowed: %s", r.Method), "") } - return nil } func writeItemsHTML(w http.ResponseWriter, tbl *data.Table, name string, query string, urlBase string) *appError { @@ -357,20 +379,69 @@ func handleItem(w http.ResponseWriter, r *http.Request) *appError { if tbl == nil { return appErrorNotFoundFmt(err1, api.ErrMsgCollectionNotFound, name) } + + ctx := r.Context() param, errQuery := createQueryParams(&reqParam, tbl.Columns, tbl.Srid) - if errQuery == nil { - ctx := r.Context() - switch format { - case api.FormatJSON: - return writeItemJSON(ctx, w, name, fid, param, urlBase) - case api.FormatHTML: - return writeItemHTML(w, tbl, name, fid, query, urlBase) - default: - return nil + feature, err := catalogInstance.TableFeature(ctx, name, fid, param) + if err != nil { + return appErrorInternalFmt(err, api.ErrMsgDataReadError, name) + } + if len(feature) == 0 { + return appErrorNotFoundFmt(nil, api.ErrMsgFeatureNotFound, fid) + } + + switch r.Method { + case http.MethodGet: + if errQuery == nil { + switch format { + case api.FormatJSON: + return writeItemJSON(ctx, w, feature, urlBase) + case api.FormatHTML: + return writeItemHTML(w, tbl, name, fid, query, urlBase) + default: + return nil + } + } else { + return appErrorInternalFmt(errQuery, api.ErrMsgInvalidQuery) } - } else { - return appErrorInternalFmt(errQuery, api.ErrMsgInvalidQuery) + case http.MethodPut: + body, err := ioutil.ReadAll(r.Body) + if err != nil { + return appErrorInternalFmt(err, api.ErrMsgInvalidQuery) + } + print(body) + var inputFeature data.Feature + err = json.Unmarshal([]byte(body), &inputFeature) + if err != nil { + return appErrorInternalFmt(err, api.ErrMsgInvalidQuery) + } + catalogInstance.ReplaceTableFeature(ctx, name, fid, inputFeature) + w.WriteHeader(http.StatusNoContent) + return nil + case http.MethodPatch: + body, err := ioutil.ReadAll(r.Body) + if err != nil { + return appErrorInternalFmt(err, api.ErrMsgInvalidQuery) + } + print(body) + var inputFeature data.Feature + err = json.Unmarshal([]byte(body), &inputFeature) + if err != nil { + return appErrorInternalFmt(err, api.ErrMsgInvalidQuery) + } + catalogInstance.UpdateTableFeature(ctx, name, fid, inputFeature) + w.WriteHeader(http.StatusNoContent) + return nil + case http.MethodDelete: + err := catalogInstance.DeleteTableFeature(ctx, name, fid) + if err != nil { + return appErrorInternalFmt(err, api.ErrMsgInvalidQuery) + } + w.WriteHeader(http.StatusNoContent) + return nil + default: + return appErrorInternalFmt(fmt.Errorf("Method not allowed: %s", r.Method), "") } } @@ -394,16 +465,7 @@ func writeItemHTML(w http.ResponseWriter, tbl *data.Table, name string, fid stri return writeHTML(w, nil, context, ui.PageItem()) } -func writeItemJSON(ctx context.Context, w http.ResponseWriter, name string, fid string, param *data.QueryParam, urlBase string) *appError { - //--- query data for request - feature, err := catalogInstance.TableFeature(ctx, name, fid, param) - if err != nil { - return appErrorInternalFmt(err, api.ErrMsgDataReadError, name) - } - if len(feature) == 0 { - return appErrorNotFoundFmt(nil, api.ErrMsgFeatureNotFound, fid) - } - +func writeItemJSON(ctx context.Context, w http.ResponseWriter, feature string, urlBase string) *appError { //--- assemble resonse //content := feature // for now can't add links to feature JSON