Skip to content

Commit

Permalink
API implementation and SQL Boiler
Browse files Browse the repository at this point in the history
  • Loading branch information
andreas-goebel committed Sep 1, 2022
1 parent f06fb2f commit fc69b8e
Show file tree
Hide file tree
Showing 35 changed files with 3,562 additions and 402 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
*.iml
.idea/**
apiserver/.openapi-generator/**
apiserver/api/**
apiserver/api/**
apiserver/*_service.go
59 changes: 26 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ This app collects the data from configurable Hailo FDS endpoints. For each endpo

## Configuration

The app needs environment variables and database tables for configuration.
The app needs environment variables and database tables for configuration. To edit the database tables the app provides an own API access.

### Eliona ###

Expand Down Expand Up @@ -58,31 +58,13 @@ export LOG_LEVEL=debug # optionally, default is 'info'

The app requires configuration data that remains in the database. To do this, the app creates its own database schema `hailo` during initialization. Some data in this schema should be made editable by Eliona frontend. This allows the app to be configured by the user without direct database access.

To modify and handle the configuration data the Hailo app provides an API access. Have a look at the [API specification](https://eliona-smart-building-assistant.github.io/open-api-docs/?https://raw.githubusercontent.com/eliona-smart-building-assistant/hailo-app/develop/openapi.yaml) ([openapi.yaml](https://raw.githubusercontent.com/eliona-smart-building-assistant/hailo-app/develop/openapi.yaml)) how the configuration tables should be used. The API is available at `http://servername:80/v1`. The port can be configured with the `API_SERVER_PORT` environment variable.

#### hailo.config

The database table `hailo.config` contains Hailo FDS endpoints. Each row stands for one endpoint with configurable timeouts and polling intervals. This table should be editable by the Eliona frontend. The table is filled during the initialization to demonstrate the configuration. This demo data contains no real endpoint and must be change for proper working.

| Column | Description |
|-------------------|:-----------------------------------------------------------------------------------|
| `app_id` | Id to identify the configured endpoint (automatically by sequence) |
| `config` | JSON string to configure the endpoint (see below) |
| `enable` | Flag to enable or disable the endpoint |
| `description` | Description of the endpoint (optional) |
| `asset_id` | Id of an parent asset with groups all device assets (optional) |
| `interval_sec` | Interval in seconds for collecting data from endpoint |
| `auth_timeout` | Timeout in seconds for authentication server (default `5`s) |
| `request_timeout` | Timeout in seconds for FDS server (default `120`s) |
| `active` | Set to `true` by the app when running and to `false` when app is stopped |
| `proj_ids` | List of Eliona project ids for which this endpoint should collect data (see below) |

The `config` column have to contain a JSON to configure the Hailo FDS endpoint:

{
"username": "Login for auth endpoint"
"password": "Password for auth endpoint"
"auth_server": "Url to Hailo auth endpoint"
"fds_server": "Url to Hailo FDS endpoint"
}
The database table `hailo.config` contains Hailo FDS endpoints. Each row stands for one endpoint with configurable timeouts and polling intervals. This table should be editable by the Eliona frontend. The table is filled during the initialization to demonstrate the configuration. This demo data contains no real endpoint and must be change for proper working.

For a detailed description have a look at the [API specification](https://eliona-smart-building-assistant.github.io/open-api-docs/?https://raw.githubusercontent.com/eliona-smart-building-assistant/hailo-app/develop/openapi.yaml) ([openapi.yaml](https://raw.githubusercontent.com/eliona-smart-building-assistant/hailo-app/develop/openapi.yaml)).

Insert in the `proj_ids` column all Eliona project ids for which the endpoint should create assets and collect data from all Hailo smart devices. For example, if the column contains `{1, 99}` the app does the following:

Expand All @@ -92,16 +74,9 @@ Insert in the `proj_ids` column all Eliona project ids for which the endpoint sh

#### hailo.asset

The table `hailo.asset` maps each Hailo smart device to an Eliona asset. For different Eliona projects different assets are used. The app collect and writes data separate for each configured project (see column `proj_ids` above).
The table `hailo.asset` maps each Hailo smart device to an Eliona asset. For different Eliona projects different assets are used. The app collect and writes data separate for each configured project (see column `proj_ids` above). The mapping is created automatically by the app. So this table does not necessarily have to be editable via the frontend or displayed in the frontend.

The mapping is created automatically by the app. So this table does not necessarily have to be editable via the frontend or displayed in the frontend.

| Column | Description |
|-------------|:----------------------------------------------------------------------------------|
| `config_id` | References the configured endpoint (column `app_id` in table `hailo.config`) |
| `device_id` | References to the Hailo smart device (`device_id` from Hailo Smart Hub) |
| `asset_id` | References the asset id from Eliona an maps the device id to an Eliona asset |
| `proj_id` | The project id for which the Eliona asset is created (see column `proj_ids` above |
For a detailed description have a look at the [API specification](https://eliona-smart-building-assistant.github.io/open-api-docs/?https://raw.githubusercontent.com/eliona-smart-building-assistant/hailo-app/develop/openapi.yaml) ([openapi.yaml](https://raw.githubusercontent.com/eliona-smart-building-assistant/hailo-app/develop/openapi.yaml)).

If specification of a Hailo smart device is read (at the first time or if new devices are added later) the app looks if there is already a mapping. If so, the app uses the mapped asset id for writing the data. If not, the app creates a new Eliona asset or updates an existing one and inserts the mapping in this table for further use.

Expand All @@ -120,6 +95,24 @@ Versions of this app prior 2.0 use different mapping of assets and Hailo smart d
where
asset_type like 'Hailo%'

## Generation and Implementation ##

The Hailo app uses generators to create API server capabilities and database access.

For the API server the [OpenAPI Generator](https://openapi-generator.tech/docs/generators/openapi-yaml) for go-server is used. The easiest way to generate the server files is to use one of the predefined generation script which use the OpenAPI Generator Docker image.

```
.\generate-api-server.cmd # Windows
./generate-api-server.sh # Linux
```

For the database access [SQLBoiler](https://github.com/volatiletech/sqlboiler) is used. The easiest way to generate the database files is to use one of the predefined generation script which use the SQLBoiler implementation. Please note that the database connection in the `sqlboiler.toml` file have to be configured.

```
.\generate-db.cmd # Windows
./generate-db.sh # Linux
```

## API Reference

The hailo app writes data for each Hailo smart device. The data is structured into different subtypes of Eliona assets. See [eliona/heaps.go](eliona/heaps.go) for details. The following subtypes are defined:
Expand Down
2 changes: 0 additions & 2 deletions apiserver/.openapi-generator-ignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,4 @@
# If the API changes please remove these lines and merge the generated files with the existing ones.

api/**
#api_asset_mapping_service.go
#api_configuration_service.go
README.md
16 changes: 10 additions & 6 deletions apiserver/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,32 +18,36 @@ import (
// The AssetMappingApiRouter implementation should parse necessary information from the http request,
// pass the data to a AssetMappingApiServicer to perform the required actions, then write the service results to the http response.
type AssetMappingApiRouter interface {
GetAssetMappingsByConfig(http.ResponseWriter, *http.Request)
GetAssetMappings(http.ResponseWriter, *http.Request)
}

// ConfigurationApiRouter defines the required methods for binding the api requests to a responses for the ConfigurationApi
// The ConfigurationApiRouter implementation should parse necessary information from the http request,
// pass the data to a ConfigurationApiServicer to perform the required actions, then write the service results to the http response.
type ConfigurationApiRouter interface {
GetConfiguration(http.ResponseWriter, *http.Request)
DeleteConfigurationById(http.ResponseWriter, *http.Request)
GetConfigurationById(http.ResponseWriter, *http.Request)
GetConfigurations(http.ResponseWriter, *http.Request)
PutConfiguration(http.ResponseWriter, *http.Request)
PostConfiguration(http.ResponseWriter, *http.Request)
PutConfigurationById(http.ResponseWriter, *http.Request)
}

// AssetMappingApiServicer defines the api actions for the AssetMappingApi service
// This interface intended to stay up to date with the openapi yaml used to generate it,
// while the service implementation can be ignored with the .openapi-generator-ignore file
// and updated with the logic required for the API.
type AssetMappingApiServicer interface {
GetAssetMappingsByConfig(context.Context, int32) (ImplResponse, error)
GetAssetMappings(context.Context, int64) (ImplResponse, error)
}

// ConfigurationApiServicer defines the api actions for the ConfigurationApi service
// This interface intended to stay up to date with the openapi yaml used to generate it,
// while the service implementation can be ignored with the .openapi-generator-ignore file
// and updated with the logic required for the API.
type ConfigurationApiServicer interface {
GetConfiguration(context.Context, int32) (ImplResponse, error)
DeleteConfigurationById(context.Context, int64) (ImplResponse, error)
GetConfigurationById(context.Context, int64) (ImplResponse, error)
GetConfigurations(context.Context) (ImplResponse, error)
PutConfiguration(context.Context, int32) (ImplResponse, error)
PostConfiguration(context.Context, Configuration) (ImplResponse, error)
PutConfigurationById(context.Context, int64, Configuration) (ImplResponse, error)
}
14 changes: 7 additions & 7 deletions apiserver/api_asset_mapping.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,23 +48,23 @@ func NewAssetMappingApiController(s AssetMappingApiServicer, opts ...AssetMappin
func (c *AssetMappingApiController) Routes() Routes {
return Routes{
{
"GetAssetMappingsByConfig",
"GetAssetMappings",
strings.ToUpper("Get"),
"/v1/asset/mapping/",
c.GetAssetMappingsByConfig,
"/v1/asset-mappings/",
c.GetAssetMappings,
},
}
}

// GetAssetMappingsByConfig -
func (c *AssetMappingApiController) GetAssetMappingsByConfig(w http.ResponseWriter, r *http.Request) {
// GetAssetMappings -
func (c *AssetMappingApiController) GetAssetMappings(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
configIdParam, err := parseInt32Parameter(query.Get("config-id"), false)
configIdParam, err := parseInt64Parameter(query.Get("config-id"), false)
if err != nil {
c.errorHandler(w, r, &ParsingError{Err: err}, nil)
return
}
result, err := c.service.GetAssetMappingsByConfig(r.Context(), configIdParam)
result, err := c.service.GetAssetMappings(r.Context(), configIdParam)
// If an error occurred, encode the error with the status code
if err != nil {
c.errorHandler(w, r, err, &result)
Expand Down
38 changes: 0 additions & 38 deletions apiserver/api_asset_mapping_service.go

This file was deleted.

98 changes: 83 additions & 15 deletions apiserver/api_configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
package apiserver

import (
"encoding/json"
"net/http"
"strings"

Expand Down Expand Up @@ -50,36 +51,68 @@ func NewConfigurationApiController(s ConfigurationApiServicer, opts ...Configura
func (c *ConfigurationApiController) Routes() Routes {
return Routes{
{
"GetConfiguration",
"DeleteConfigurationById",
strings.ToUpper("Delete"),
"/v1/configs/{config-id}",
c.DeleteConfigurationById,
},
{
"GetConfigurationById",
strings.ToUpper("Get"),
"/v1/config/{config-id}",
c.GetConfiguration,
"/v1/configs/{config-id}",
c.GetConfigurationById,
},
{
"GetConfigurations",
strings.ToUpper("Get"),
"/v1/config",
"/v1/configs",
c.GetConfigurations,
},
{
"PutConfiguration",
"PostConfiguration",
strings.ToUpper("Post"),
"/v1/configs",
c.PostConfiguration,
},
{
"PutConfigurationById",
strings.ToUpper("Put"),
"/v1/config/{config-id}",
c.PutConfiguration,
"/v1/configs/{config-id}",
c.PutConfigurationById,
},
}
}

// GetConfiguration - Get FDS endpoint
func (c *ConfigurationApiController) GetConfiguration(w http.ResponseWriter, r *http.Request) {
// DeleteConfigurationById - Deletes a FDS endpoint
func (c *ConfigurationApiController) DeleteConfigurationById(w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
configIdParam, err := parseInt64Parameter(params["config-id"], true)
if err != nil {
c.errorHandler(w, r, &ParsingError{Err: err}, nil)
return
}

result, err := c.service.DeleteConfigurationById(r.Context(), configIdParam)
// If an error occurred, encode the error with the status code
if err != nil {
c.errorHandler(w, r, err, &result)
return
}
// If no error, encode the body and the result code
EncodeJSONResponse(result.Body, &result.Code, w)

}

// GetConfigurationById - Get FDS endpoint
func (c *ConfigurationApiController) GetConfigurationById(w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
configIdParam, err := parseInt32Parameter(params["config-id"], true)
configIdParam, err := parseInt64Parameter(params["config-id"], true)
if err != nil {
c.errorHandler(w, r, &ParsingError{Err: err}, nil)
return
}

result, err := c.service.GetConfiguration(r.Context(), configIdParam)
result, err := c.service.GetConfigurationById(r.Context(), configIdParam)
// If an error occurred, encode the error with the status code
if err != nil {
c.errorHandler(w, r, err, &result)
Expand All @@ -103,16 +136,51 @@ func (c *ConfigurationApiController) GetConfigurations(w http.ResponseWriter, r

}

// PutConfiguration - Creates or update an FDS endpoint
func (c *ConfigurationApiController) PutConfiguration(w http.ResponseWriter, r *http.Request) {
// PostConfiguration - Creates an FDS endpoint
func (c *ConfigurationApiController) PostConfiguration(w http.ResponseWriter, r *http.Request) {
configurationParam := Configuration{}
d := json.NewDecoder(r.Body)
d.DisallowUnknownFields()
if err := d.Decode(&configurationParam); err != nil {
c.errorHandler(w, r, &ParsingError{Err: err}, nil)
return
}
if err := AssertConfigurationRequired(configurationParam); err != nil {
c.errorHandler(w, r, err, nil)
return
}
result, err := c.service.PostConfiguration(r.Context(), configurationParam)
// If an error occurred, encode the error with the status code
if err != nil {
c.errorHandler(w, r, err, &result)
return
}
// If no error, encode the body and the result code
EncodeJSONResponse(result.Body, &result.Code, w)

}

// PutConfigurationById - Updates an FDS endpoint
func (c *ConfigurationApiController) PutConfigurationById(w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
configIdParam, err := parseInt32Parameter(params["config-id"], true)
configIdParam, err := parseInt64Parameter(params["config-id"], true)
if err != nil {
c.errorHandler(w, r, &ParsingError{Err: err}, nil)
return
}

result, err := c.service.PutConfiguration(r.Context(), configIdParam)
configurationParam := Configuration{}
d := json.NewDecoder(r.Body)
d.DisallowUnknownFields()
if err := d.Decode(&configurationParam); err != nil {
c.errorHandler(w, r, &ParsingError{Err: err}, nil)
return
}
if err := AssertConfigurationRequired(configurationParam); err != nil {
c.errorHandler(w, r, err, nil)
return
}
result, err := c.service.PutConfigurationById(r.Context(), configIdParam, configurationParam)
// If an error occurred, encode the error with the status code
if err != nil {
c.errorHandler(w, r, err, &result)
Expand Down
Loading

0 comments on commit fc69b8e

Please sign in to comment.