Skip to content

Commit

Permalink
feat(core): ability to patch sensors
Browse files Browse the repository at this point in the history
  • Loading branch information
TimVosch committed Nov 7, 2024
1 parent 571b06d commit 5709e58
Show file tree
Hide file tree
Showing 8 changed files with 258 additions and 18 deletions.
48 changes: 48 additions & 0 deletions services/core/devices/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,54 @@ func (s *Service) GetSensor(ctx context.Context, id int64) (*Sensor, error) {
return s.store.GetSensor(ctx, id)
}

type UpdateSensorOpts struct {
Description *string `json:"description"`
Brand *string `json:"brand"`
ArchiveTime *int `json:"archive_time"`
ExternalID *string `json:"external_id"`
IsFallback *bool `json:"is_fallback"`
Properties json.RawMessage `json:"properties"`
}

func (s *Service) UpdateSensor(ctx context.Context, device *Device, sensor *Sensor, opt UpdateSensorOpts) error {
if err := auth.MustHavePermissions(ctx, auth.Permissions{auth.WRITE_DEVICES}); err != nil {
return err
}

if opt.Description != nil {
sensor.Description = *opt.Description
}
if opt.Brand != nil {
sensor.Brand = *opt.Brand
}
if opt.ArchiveTime != nil {
if *opt.ArchiveTime == 0 {
sensor.ArchiveTime = nil
} else {
sensor.ArchiveTime = opt.ArchiveTime
}
}
if opt.ExternalID != nil {
sensor.ExternalID = *opt.ExternalID
}
if opt.IsFallback != nil {
sensor.IsFallback = *opt.IsFallback
}
if opt.Properties != nil {
sensor.Properties = opt.Properties
}

if err := device.UpdateSensor(sensor); err != nil {
return err
}

if err := s.store.Save(ctx, device); err != nil {
return err
}

return nil
}

func (s *Service) CreateSensorGroup(ctx context.Context, name, description string) (*SensorGroup, error) {
if err := auth.MustHavePermissions(ctx, auth.Permissions{auth.WRITE_DEVICES}); err != nil {
return nil, err
Expand Down
13 changes: 13 additions & 0 deletions services/core/devices/devices.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,19 @@ func (d *Device) DeleteSensorByID(id int64) error {
return ErrSensorNotFound
}

func (d *Device) UpdateSensor(sensor *Sensor) error {
if sensor == nil {
return nil
}
for ix := range d.Sensors {
if d.Sensors[ix].ID == sensor.ID {
d.Sensors[ix] = *sensor
return nil
}
}
return fmt.Errorf("cant update sensor: %w", ErrSensorNotFound)
}

func (d *Device) ClearLocation() {
d.Latitude = nil
d.Longitude = nil
Expand Down
34 changes: 33 additions & 1 deletion services/core/transport/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,17 @@ import (
"github.com/go-chi/chi/v5"

"sensorbucket.nl/sensorbucket/internal/web"
"sensorbucket.nl/sensorbucket/services/core/devices"
)

type middleware = func(http.Handler) http.Handler

type ctxKey string

var ctxDeviceKey ctxKey = "device"
var (
ctxDeviceKey ctxKey = "device"
ctxSensorKey ctxKey = "sensor"
)

func (t *CoreTransport) useDeviceResolver() middleware {
return func(next http.Handler) http.Handler {
Expand Down Expand Up @@ -46,6 +50,34 @@ func (t *CoreTransport) useDeviceResolver() middleware {
}
}

func (t *CoreTransport) useSensorResolver() middleware {
return func(next http.Handler) http.Handler {
mw := func(rw http.ResponseWriter, r *http.Request) {
device, ok := r.Context().Value(ctxDeviceKey).(*devices.Device)
if !ok {
web.HTTPError(rw, devices.ErrDeviceNotFound)
return
}
code := chi.URLParam(r, "sensor_code")

sensor, err := device.GetSensorByCode(code)
if err != nil {
web.HTTPError(rw, err)
return
}

r = r.WithContext(context.WithValue(
r.Context(),
ctxSensorKey,
sensor,
))

next.ServeHTTP(rw, r)
}
return http.HandlerFunc(mw)
}
}

func urlParamInt64(r *http.Request, name string) (int64, error) {
q := strings.Trim(chi.URLParam(r, name), " \r\n")
if q == "" {
Expand Down
11 changes: 9 additions & 2 deletions services/core/transport/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,19 @@ func (t *CoreTransport) routes() {
r.Route("/sensors", func(r chi.Router) {
r.Get("/", t.httpListDeviceSensors())
r.Post("/", t.httpAddSensor())
r.Delete("/{sensor_code}", t.httpDeleteSensor())
r.Route("/{sensor_code}", func(r chi.Router) {
r.Use(t.useSensorResolver())
r.Get("/", t.httpGetSensor())
r.Delete("/", t.httpDeleteSensor())
r.Patch("/", t.httpUpdateSensor())
})
})
})

r.Get("/sensors", t.httpListSensors())
r.Get("/sensors/{id}", t.httpGetSensor())
r.Route("/sensors/{sensor_id}", func(r chi.Router) {
r.Get("/", t.httpGetSensor())
})
r.Route("/sensor-groups", func(r chi.Router) {
r.Post("/", t.httpCreateSensorGroup())
r.Get("/", t.httpListSensorGroups())
Expand Down
61 changes: 47 additions & 14 deletions services/core/transport/sensors.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ import (
"sensorbucket.nl/sensorbucket/services/core/devices"
)

var ErrHTTPSensorIDInvalid = web.NewError(
http.StatusBadRequest,
"Sensor ID must be an integer",
"SENSOR_ID_INVALID",
)

func (t *CoreTransport) httpListDeviceSensors() http.HandlerFunc {
return func(rw http.ResponseWriter, r *http.Request) {
device := r.Context().Value(ctxDeviceKey).(*devices.Device)
Expand Down Expand Up @@ -46,21 +52,16 @@ func (t *CoreTransport) httpAddSensor() http.HandlerFunc {

func (t *CoreTransport) httpDeleteSensor() http.HandlerFunc {
return func(rw http.ResponseWriter, r *http.Request) {
dev := r.Context().Value(ctxDeviceKey).(*devices.Device)

sensor, err := dev.GetSensorByCode(chi.URLParam(r, "sensor_code"))
if err != nil {
web.HTTPError(rw, err)
return
}
device := r.Context().Value(ctxDeviceKey).(*devices.Device)
sensor := r.Context().Value(ctxSensorKey).(*devices.Sensor)

if err := t.deviceService.DeleteSensor(r.Context(), dev, sensor); err != nil {
if err := t.deviceService.DeleteSensor(r.Context(), device, sensor); err != nil {
web.HTTPError(rw, err)
return
}

web.HTTPResponse(rw, http.StatusOK, &web.APIResponseAny{
Message: "Deleted sensor from device",
Message: "Deleted sensor",
})
}
}
Expand All @@ -83,22 +84,54 @@ func (t *CoreTransport) httpListSensors() http.HandlerFunc {

func (t *CoreTransport) httpGetSensor() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
sensorIDQ := chi.URLParam(r, "id")
sensorID, err := strconv.ParseInt(sensorIDQ, 10, 64)
sensorCTX := r.Context().Value(ctxSensorKey)
if sensorCTX != nil {
web.HTTPResponse(w, http.StatusOK, web.APIResponseAny{
Message: "Fetched sensor",
Data: sensorCTX.(*devices.Sensor),
})
return
}
sensorIDString := chi.URLParam(r, "sensor_id")
if sensorIDString == "" {
web.HTTPError(w, ErrHTTPSensorIDInvalid)
return
}
sensorID, err := strconv.ParseInt(sensorIDString, 10, 64)
if err != nil {
web.HTTPError(w, web.NewError(http.StatusBadRequest, "invalid sensor id", ""))
web.HTTPError(w, ErrHTTPDeviceIDInvalid)
return
}

sensor, err := t.deviceService.GetSensor(r.Context(), sensorID)
if err != nil {
web.HTTPError(w, err)
return
}

web.HTTPResponse(w, http.StatusOK, web.APIResponseAny{
Message: "Fetched sensor",
Data: sensor,
})
}
}

func (t *CoreTransport) httpUpdateSensor() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
dev := r.Context().Value(ctxDeviceKey).(*devices.Device)
sensor := r.Context().Value(ctxSensorKey).(*devices.Sensor)

var dto devices.UpdateSensorOpts
if err := web.DecodeJSON(r, &dto); err != nil {
web.HTTPError(w, err)
return
}

if err := t.deviceService.UpdateSensor(r.Context(), dev, sensor, dto); err != nil {
web.HTTPError(w, err)
return
}

web.HTTPResponse(w, http.StatusOK, &web.APIResponseAny{
Message: "Updated sensor",
})
}
}
2 changes: 1 addition & 1 deletion tools/openapi/api.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
openapi: 3.0.0
info:
title: Sensorbucket API
version: '1.2.2'
version: '1.2.5'
license:
name: EUPLv1.2
url: 'https://joinup.ec.europa.eu/sites/default/files/custom-page/attachment/2020-03/EUPL-1.2%20EN.txt'
Expand Down
83 changes: 83 additions & 0 deletions tools/openapi/path-devices-by-sensors-id.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,86 @@
get:
operationId: GetSensor
summary: Get sensor
description: |
Get the sensor with the given identifier.
The returned sensor will also include the full model of the sensors attached to that sensor.
tags: ["Devices"]
parameters:
- name: device_id
description: The identifier of the device
in: path
required: true
schema:
type: integer
- name: sensor_code
description: The code of the sensor
in: path
required: true
schema:
type: string
responses:
'200':
description: Fetched sensor
content:
application/json:
schema:
type: object
properties:
message:
type: string
example: Fetched sensor
data:
$ref: 'schemas.yaml#/sensor'
'401':
$ref: './responses.yaml#/401'
'403':
$ref: './responses.yaml#/403'
'404':
$ref: './responses.yaml#/404'
patch:
operationId: UpdateSensor
summary: Update sensor properties
description: |
Update a some properties of the sensor with the given identifier.
The request body should contain one or more modifiable properties of the Sensor.
tags: ["Devices"]
parameters:
- name: device_id
description: The identifier of the device
in: path
required: true
schema:
type: integer
- name: sensor_code
description: The code of the sensor
in: path
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
$ref: 'schemas.yaml#/updateSensorRequest'
responses:
'200':
description: Updated sensor properties
content:
application/json:
schema:
type: object
properties:
message:
type: string
example: Updated sensor properties
'401':
$ref: './responses.yaml#/401'
'403':
$ref: './responses.yaml#/403'
'404':
$ref: './responses.yaml#/404'
delete:
operationId: DeleteDeviceSensor
summary: Delete sensor
Expand Down
24 changes: 24 additions & 0 deletions tools/openapi/schemas.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,25 @@ createSensorRequest:
archive_time:
type: integer
example: 7
updateSensorRequest:
type: object
properties:
description:
type: string
example: Pressure sensor at 5 meters depth
external_id:
type: string
example: "5"
brand:
type: string
example: "sensor brand ABC"
properties:
type: object
example:
mount_height: 15cm
archive_time:
type: integer
example: 7

sensor:
type: object
Expand Down Expand Up @@ -373,6 +392,7 @@ trace_step:
measurement:
type: object
required:
- measurement_id
- uplink_message_id
- device_id
- device_code
Expand All @@ -387,6 +407,10 @@ measurement:
- measurement_value
- measurement_expiration
properties:
measurement_id:
type: integer
format: int64
example: 458412
uplink_message_id:
type: string
example: ca29e28e-eeb6-4662-922c-6cf6a36ccb6e
Expand Down

0 comments on commit 5709e58

Please sign in to comment.