From 822283e7978431f72725ad777968250850e44872 Mon Sep 17 00:00:00 2001 From: Dennis Smith Date: Wed, 13 Dec 2023 13:27:28 -0500 Subject: [PATCH] Feature/multiple equivalency table v2 (#198) * multiple equivalency table feature progress * use unknown param and unit domains if not present * fix PUT default param/unit id * rolling sum over elevation ascending * add timout to http.Client; set default search_path for local database * route datalogger table error and previews; reference 'datalogger' name consistently * fix dcsloader url logging * correctly set encoded parameters to RawQuery before setting URL * fix depth based instrument v2 migrations * add statement timeout for runaway queries * fix depth based instrument v2 migration * fix slow GetHome sql query * remove background context from pubsub service * fix build error * use dataloggerID for error rounting * trigger repeatable migration to run after instruments view dropped * update ipi fields; fix inconsistencies in depth instruments seed data * fix bug with duplicate initial time in saa/ipi * fix linting (type hinting) in python script * add tests for datalogger and equivalency table * fix unexported DataloggerTable struct fields * fix preview url trailing space * fix trailing spaces in urls * fix telemetry api; fix urls in tests; fix wrong http res codes * auto create/delete datalogger table on eq table creation/deletion * respond with 404 when preview not found --- api/docs/docs.go | 207 ++++++++++++++++-- api/docs/swagger.json | 207 ++++++++++++++++-- api/docs/swagger.yaml | 148 +++++++++++-- api/internal/handler/alert_config.go | 6 - api/internal/handler/datalogger.go | 53 ++++- api/internal/handler/datalogger_telemetry.go | 69 ++++-- api/internal/handler/datalogger_test.go | 83 ++++++- api/internal/handler/equivalency_table.go | 106 +++++++-- .../handler/equivalency_table_test.go | 65 +++++- api/internal/model/datalogger.go | 133 +++++++---- api/internal/model/datalogger_telemetry.go | 30 ++- api/internal/model/equivalency_table.go | 92 ++++---- api/internal/server/api.go | 12 +- api/internal/service/datalogger.go | 34 ++- api/internal/service/datalogger_telemetry.go | 11 +- api/internal/service/equivalency_table.go | 20 +- migrate/common/R__01_roles.sql | 2 + migrate/common/R__10_datalogger.sql | 63 +++--- .../V1.5.0__multiple_equivalency_tables.sql | 42 ++++ migrate/local/V999.1.0__seed_data.sql | 3 +- migrate/local/V999.4.0__seed_datalogger.sql | 16 +- mock/telemetry/Dockerfile | 2 +- mock/telemetry/telemetry_post_tester.py | 36 ++- 23 files changed, 1166 insertions(+), 274 deletions(-) create mode 100644 migrate/common/V1.5.0__multiple_equivalency_tables.sql diff --git a/api/docs/docs.go b/api/docs/docs.go index 33f2eb0d..59134610 100644 --- a/api/docs/docs.go +++ b/api/docs/docs.go @@ -507,6 +507,120 @@ const docTemplate = `{ } }, "/datalogger/{datalogger_id}/equivalency_table": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "equivalency-table" + ], + "summary": "creates an equivalency table for a datalogger and auto create data logger table if not exists", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "datalogger uuid", + "name": "datalogger_id", + "in": "path", + "required": true + }, + { + "description": "equivalency table payload", + "name": "equivalency_table", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_USACE_instrumentation-api_api_internal_model.EquivalencyTable" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/echo.HTTPError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/echo.HTTPError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/echo.HTTPError" + } + } + } + } + }, + "/datalogger/{datalogger_id}/key": { + "put": { + "security": [ + { + "Bearer": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "datalogger" + ], + "summary": "deletes and recreates a datalogger api key", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "datalogger uuid", + "name": "datalogger_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_USACE_instrumentation-api_api_internal_model.DataloggerWithKey" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/echo.HTTPError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/echo.HTTPError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/echo.HTTPError" + } + } + } + } + }, + "/datalogger/{datalogger_id}/tables/{datalogger_table_id}/equivalency_table": { "get": { "security": [ { @@ -528,6 +642,14 @@ const docTemplate = `{ "name": "datalogger_id", "in": "path", "required": true + }, + { + "type": "string", + "format": "uuid", + "description": "datalogger table uuid", + "name": "datalogger_table_id", + "in": "path", + "required": true } ], "responses": { @@ -582,6 +704,14 @@ const docTemplate = `{ "in": "path", "required": true }, + { + "type": "string", + "format": "uuid", + "description": "datalogger table uuid", + "name": "datalogger_table_id", + "in": "path", + "required": true + }, { "description": "equivalency table payload", "name": "equivalency_table", @@ -631,7 +761,7 @@ const docTemplate = `{ "tags": [ "equivalency-table" ], - "summary": "creates an equivalency table for a datalogger", + "summary": "creates an equivalency table for a datalogger and auto create data logger table if not exists", "parameters": [ { "type": "string", @@ -641,6 +771,14 @@ const docTemplate = `{ "in": "path", "required": true }, + { + "type": "string", + "format": "uuid", + "description": "datalogger table uuid", + "name": "datalogger_table_id", + "in": "path", + "required": true + }, { "description": "equivalency table payload", "name": "equivalency_table", @@ -691,7 +829,7 @@ const docTemplate = `{ "tags": [ "equivalency-table" ], - "summary": "deletes an equivalency table", + "summary": "deletes an equivalency table and corresponding datalogger table", "parameters": [ { "type": "string", @@ -731,7 +869,7 @@ const docTemplate = `{ } } }, - "/datalogger/{datalogger_id}/equivalency_table/row": { + "/datalogger/{datalogger_id}/tables/{datalogger_table_id}/equivalency_table/row/{row_id}": { "delete": { "security": [ { @@ -758,8 +896,8 @@ const docTemplate = `{ "type": "string", "format": "uuid", "description": "equivalency table row uuid", - "name": "id", - "in": "query", + "name": "row_id", + "in": "path", "required": true } ], @@ -792,7 +930,7 @@ const docTemplate = `{ } } }, - "/datalogger/{datalogger_id}/key": { + "/datalogger/{datalogger_id}/tables/{datalogger_table_id}/name": { "put": { "security": [ { @@ -805,7 +943,7 @@ const docTemplate = `{ "tags": [ "datalogger" ], - "summary": "deletes and recreates a datalogger api key", + "summary": "resets a datalogger table name to be renamed by incoming telemetry", "parameters": [ { "type": "string", @@ -814,13 +952,21 @@ const docTemplate = `{ "name": "datalogger_id", "in": "path", "required": true + }, + { + "type": "string", + "format": "uuid", + "description": "datalogger table uuid", + "name": "datalogger_table_id", + "in": "path", + "required": true } ], "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_USACE_instrumentation-api_api_internal_model.DataloggerWithKey" + "$ref": "#/definitions/github_com_USACE_instrumentation-api_api_internal_model.DataloggerPreview" } }, "400": { @@ -844,7 +990,7 @@ const docTemplate = `{ } } }, - "/datalogger/{datalogger_id}/preview": { + "/datalogger/{datalogger_id}/tables/{datalogger_table_id}/preview": { "get": { "security": [ { @@ -866,6 +1012,14 @@ const docTemplate = `{ "name": "datalogger_id", "in": "path", "required": true + }, + { + "type": "string", + "format": "uuid", + "description": "datalogger table uuid", + "name": "datalogger_table_id", + "in": "path", + "required": true } ], "responses": { @@ -7987,6 +8141,12 @@ const docTemplate = `{ "sn": { "type": "string" }, + "tables": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_USACE_instrumentation-api_api_internal_model.DataloggerTable" + } + }, "update_date": { "type": "string" }, @@ -8001,19 +8161,24 @@ const docTemplate = `{ "github_com_USACE_instrumentation-api_api_internal_model.DataloggerPreview": { "type": "object", "properties": { - "datalogger_id": { - "type": "string" - }, - "model": { + "datalogger_table_id": { "type": "string" }, "preview": { "$ref": "#/definitions/pgtype.JSON" }, - "sn": { + "update_date": { + "type": "string" + } + } + }, + "github_com_USACE_instrumentation-api_api_internal_model.DataloggerTable": { + "type": "object", + "properties": { + "id": { "type": "string" }, - "update_date": { + "table_name": { "type": "string" } } @@ -8060,6 +8225,12 @@ const docTemplate = `{ "sn": { "type": "string" }, + "tables": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_USACE_instrumentation-api_api_internal_model.DataloggerTable" + } + }, "update_date": { "type": "string" }, @@ -8172,6 +8343,12 @@ const docTemplate = `{ "datalogger_id": { "type": "string" }, + "datalogger_table_id": { + "type": "string" + }, + "datalogger_table_name": { + "type": "string" + }, "rows": { "type": "array", "items": { diff --git a/api/docs/swagger.json b/api/docs/swagger.json index c6f737d7..7245eb72 100644 --- a/api/docs/swagger.json +++ b/api/docs/swagger.json @@ -499,6 +499,120 @@ } }, "/datalogger/{datalogger_id}/equivalency_table": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "equivalency-table" + ], + "summary": "creates an equivalency table for a datalogger and auto create data logger table if not exists", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "datalogger uuid", + "name": "datalogger_id", + "in": "path", + "required": true + }, + { + "description": "equivalency table payload", + "name": "equivalency_table", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_USACE_instrumentation-api_api_internal_model.EquivalencyTable" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/echo.HTTPError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/echo.HTTPError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/echo.HTTPError" + } + } + } + } + }, + "/datalogger/{datalogger_id}/key": { + "put": { + "security": [ + { + "Bearer": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "datalogger" + ], + "summary": "deletes and recreates a datalogger api key", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "datalogger uuid", + "name": "datalogger_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_USACE_instrumentation-api_api_internal_model.DataloggerWithKey" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/echo.HTTPError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/echo.HTTPError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/echo.HTTPError" + } + } + } + } + }, + "/datalogger/{datalogger_id}/tables/{datalogger_table_id}/equivalency_table": { "get": { "security": [ { @@ -520,6 +634,14 @@ "name": "datalogger_id", "in": "path", "required": true + }, + { + "type": "string", + "format": "uuid", + "description": "datalogger table uuid", + "name": "datalogger_table_id", + "in": "path", + "required": true } ], "responses": { @@ -574,6 +696,14 @@ "in": "path", "required": true }, + { + "type": "string", + "format": "uuid", + "description": "datalogger table uuid", + "name": "datalogger_table_id", + "in": "path", + "required": true + }, { "description": "equivalency table payload", "name": "equivalency_table", @@ -623,7 +753,7 @@ "tags": [ "equivalency-table" ], - "summary": "creates an equivalency table for a datalogger", + "summary": "creates an equivalency table for a datalogger and auto create data logger table if not exists", "parameters": [ { "type": "string", @@ -633,6 +763,14 @@ "in": "path", "required": true }, + { + "type": "string", + "format": "uuid", + "description": "datalogger table uuid", + "name": "datalogger_table_id", + "in": "path", + "required": true + }, { "description": "equivalency table payload", "name": "equivalency_table", @@ -683,7 +821,7 @@ "tags": [ "equivalency-table" ], - "summary": "deletes an equivalency table", + "summary": "deletes an equivalency table and corresponding datalogger table", "parameters": [ { "type": "string", @@ -723,7 +861,7 @@ } } }, - "/datalogger/{datalogger_id}/equivalency_table/row": { + "/datalogger/{datalogger_id}/tables/{datalogger_table_id}/equivalency_table/row/{row_id}": { "delete": { "security": [ { @@ -750,8 +888,8 @@ "type": "string", "format": "uuid", "description": "equivalency table row uuid", - "name": "id", - "in": "query", + "name": "row_id", + "in": "path", "required": true } ], @@ -784,7 +922,7 @@ } } }, - "/datalogger/{datalogger_id}/key": { + "/datalogger/{datalogger_id}/tables/{datalogger_table_id}/name": { "put": { "security": [ { @@ -797,7 +935,7 @@ "tags": [ "datalogger" ], - "summary": "deletes and recreates a datalogger api key", + "summary": "resets a datalogger table name to be renamed by incoming telemetry", "parameters": [ { "type": "string", @@ -806,13 +944,21 @@ "name": "datalogger_id", "in": "path", "required": true + }, + { + "type": "string", + "format": "uuid", + "description": "datalogger table uuid", + "name": "datalogger_table_id", + "in": "path", + "required": true } ], "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_USACE_instrumentation-api_api_internal_model.DataloggerWithKey" + "$ref": "#/definitions/github_com_USACE_instrumentation-api_api_internal_model.DataloggerPreview" } }, "400": { @@ -836,7 +982,7 @@ } } }, - "/datalogger/{datalogger_id}/preview": { + "/datalogger/{datalogger_id}/tables/{datalogger_table_id}/preview": { "get": { "security": [ { @@ -858,6 +1004,14 @@ "name": "datalogger_id", "in": "path", "required": true + }, + { + "type": "string", + "format": "uuid", + "description": "datalogger table uuid", + "name": "datalogger_table_id", + "in": "path", + "required": true } ], "responses": { @@ -7979,6 +8133,12 @@ "sn": { "type": "string" }, + "tables": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_USACE_instrumentation-api_api_internal_model.DataloggerTable" + } + }, "update_date": { "type": "string" }, @@ -7993,19 +8153,24 @@ "github_com_USACE_instrumentation-api_api_internal_model.DataloggerPreview": { "type": "object", "properties": { - "datalogger_id": { - "type": "string" - }, - "model": { + "datalogger_table_id": { "type": "string" }, "preview": { "$ref": "#/definitions/pgtype.JSON" }, - "sn": { + "update_date": { + "type": "string" + } + } + }, + "github_com_USACE_instrumentation-api_api_internal_model.DataloggerTable": { + "type": "object", + "properties": { + "id": { "type": "string" }, - "update_date": { + "table_name": { "type": "string" } } @@ -8052,6 +8217,12 @@ "sn": { "type": "string" }, + "tables": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_USACE_instrumentation-api_api_internal_model.DataloggerTable" + } + }, "update_date": { "type": "string" }, @@ -8164,6 +8335,12 @@ "datalogger_id": { "type": "string" }, + "datalogger_table_id": { + "type": "string" + }, + "datalogger_table_name": { + "type": "string" + }, "rows": { "type": "array", "items": { diff --git a/api/docs/swagger.yaml b/api/docs/swagger.yaml index 8fbc2232..eb8ff896 100644 --- a/api/docs/swagger.yaml +++ b/api/docs/swagger.yaml @@ -216,6 +216,10 @@ definitions: type: string sn: type: string + tables: + items: + $ref: '#/definitions/github_com_USACE_instrumentation-api_api_internal_model.DataloggerTable' + type: array update_date: type: string updater: @@ -225,17 +229,20 @@ definitions: type: object github_com_USACE_instrumentation-api_api_internal_model.DataloggerPreview: properties: - datalogger_id: - type: string - model: + datalogger_table_id: type: string preview: $ref: '#/definitions/pgtype.JSON' - sn: - type: string update_date: type: string type: object + github_com_USACE_instrumentation-api_api_internal_model.DataloggerTable: + properties: + id: + type: string + table_name: + type: string + type: object github_com_USACE_instrumentation-api_api_internal_model.DataloggerWithKey: properties: create_date: @@ -264,6 +271,10 @@ definitions: type: string sn: type: string + tables: + items: + $ref: '#/definitions/github_com_USACE_instrumentation-api_api_internal_model.DataloggerTable' + type: array update_date: type: string updater: @@ -337,6 +348,10 @@ definitions: properties: datalogger_id: type: string + datalogger_table_id: + type: string + datalogger_table_name: + type: string rows: items: $ref: '#/definitions/github_com_USACE_instrumentation-api_api_internal_model.EquivalencyTableRow' @@ -1392,6 +1407,80 @@ paths: tags: - datalogger /datalogger/{datalogger_id}/equivalency_table: + post: + parameters: + - description: datalogger uuid + format: uuid + in: path + name: datalogger_id + required: true + type: string + - description: equivalency table payload + in: body + name: equivalency_table + required: true + schema: + $ref: '#/definitions/github_com_USACE_instrumentation-api_api_internal_model.EquivalencyTable' + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/echo.HTTPError' + "404": + description: Not Found + schema: + $ref: '#/definitions/echo.HTTPError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/echo.HTTPError' + security: + - Bearer: [] + summary: creates an equivalency table for a datalogger and auto create data + logger table if not exists + tags: + - equivalency-table + /datalogger/{datalogger_id}/key: + put: + parameters: + - description: datalogger uuid + format: uuid + in: path + name: datalogger_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_USACE_instrumentation-api_api_internal_model.DataloggerWithKey' + "400": + description: Bad Request + schema: + $ref: '#/definitions/echo.HTTPError' + "404": + description: Not Found + schema: + $ref: '#/definitions/echo.HTTPError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/echo.HTTPError' + security: + - Bearer: [] + summary: deletes and recreates a datalogger api key + tags: + - datalogger + /datalogger/{datalogger_id}/tables/{datalogger_table_id}/equivalency_table: delete: parameters: - description: datalogger uuid @@ -1422,7 +1511,7 @@ paths: $ref: '#/definitions/echo.HTTPError' security: - Bearer: [] - summary: deletes an equivalency table + summary: deletes an equivalency table and corresponding datalogger table tags: - equivalency-table get: @@ -1433,6 +1522,12 @@ paths: name: datalogger_id required: true type: string + - description: datalogger table uuid + format: uuid + in: path + name: datalogger_table_id + required: true + type: string produces: - application/json responses: @@ -1467,6 +1562,12 @@ paths: name: datalogger_id required: true type: string + - description: datalogger table uuid + format: uuid + in: path + name: datalogger_table_id + required: true + type: string - description: equivalency table payload in: body name: equivalency_table @@ -1495,7 +1596,8 @@ paths: $ref: '#/definitions/echo.HTTPError' security: - Bearer: [] - summary: creates an equivalency table for a datalogger + summary: creates an equivalency table for a datalogger and auto create data + logger table if not exists tags: - equivalency-table put: @@ -1506,6 +1608,12 @@ paths: name: datalogger_id required: true type: string + - description: datalogger table uuid + format: uuid + in: path + name: datalogger_table_id + required: true + type: string - description: equivalency table payload in: body name: equivalency_table @@ -1536,7 +1644,7 @@ paths: summary: updates an equivalency table for a datalogger tags: - equivalency-table - /datalogger/{datalogger_id}/equivalency_table/row: + /datalogger/{datalogger_id}/tables/{datalogger_table_id}/equivalency_table/row/{row_id}: delete: parameters: - description: datalogger uuid @@ -1547,8 +1655,8 @@ paths: type: string - description: equivalency table row uuid format: uuid - in: query - name: id + in: path + name: row_id required: true type: string produces: @@ -1576,7 +1684,7 @@ paths: summary: deletes an equivalency table row tags: - equivalency-table - /datalogger/{datalogger_id}/key: + /datalogger/{datalogger_id}/tables/{datalogger_table_id}/name: put: parameters: - description: datalogger uuid @@ -1585,13 +1693,19 @@ paths: name: datalogger_id required: true type: string + - description: datalogger table uuid + format: uuid + in: path + name: datalogger_table_id + required: true + type: string produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/github_com_USACE_instrumentation-api_api_internal_model.DataloggerWithKey' + $ref: '#/definitions/github_com_USACE_instrumentation-api_api_internal_model.DataloggerPreview' "400": description: Bad Request schema: @@ -1606,10 +1720,10 @@ paths: $ref: '#/definitions/echo.HTTPError' security: - Bearer: [] - summary: deletes and recreates a datalogger api key + summary: resets a datalogger table name to be renamed by incoming telemetry tags: - datalogger - /datalogger/{datalogger_id}/preview: + /datalogger/{datalogger_id}/tables/{datalogger_table_id}/preview: get: parameters: - description: datalogger uuid @@ -1618,6 +1732,12 @@ paths: name: datalogger_id required: true type: string + - description: datalogger table uuid + format: uuid + in: path + name: datalogger_table_id + required: true + type: string produces: - application/json responses: diff --git a/api/internal/handler/alert_config.go b/api/internal/handler/alert_config.go index 5620cff6..407ca676 100644 --- a/api/internal/handler/alert_config.go +++ b/api/internal/handler/alert_config.go @@ -45,9 +45,6 @@ func (h *ApiHandler) GetAllAlertConfigsForProject(c echo.Context) error { return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } } - if len(aa) == 0 { - return echo.NewHTTPError(http.StatusNotFound, message.NotFound) - } return c.JSON(http.StatusOK, aa) } @@ -72,9 +69,6 @@ func (h *ApiHandler) ListInstrumentAlertConfigs(c echo.Context) error { if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } - if len(aa) == 0 { - return echo.NewHTTPError(http.StatusNotFound, message.NotFound) - } return c.JSON(http.StatusOK, aa) } diff --git a/api/internal/handler/datalogger.go b/api/internal/handler/datalogger.go index e7ae085b..1aa5e485 100644 --- a/api/internal/handler/datalogger.go +++ b/api/internal/handler/datalogger.go @@ -1,6 +1,8 @@ package handler import ( + "database/sql" + "errors" "fmt" "net/http" "time" @@ -254,29 +256,62 @@ func (h *ApiHandler) DeleteDatalogger(c echo.Context) error { return c.JSON(http.StatusOK, map[string]interface{}{"id": dlID}) } -// GetDataloggerPreview godoc +// GetDataloggerTablePreview godoc // // @Summary gets the most recent datalogger preview by by datalogger id // @Tags datalogger // @Produce json // @Param datalogger_id path string true "datalogger uuid" Format(uuid) +// @Param datalogger_table_id path string true "datalogger table uuid" Format(uuid) // @Success 200 {object} model.DataloggerPreview // @Failure 400 {object} echo.HTTPError // @Failure 404 {object} echo.HTTPError // @Failure 500 {object} echo.HTTPError -// @Router /datalogger/{datalogger_id}/preview [get] +// @Router /datalogger/{datalogger_id}/tables/{datalogger_table_id}/preview [get] // @Security Bearer -func (h *ApiHandler) GetDataloggerPreview(c echo.Context) error { - dlID, err := uuid.Parse(c.Param("datalogger_id")) +func (h *ApiHandler) GetDataloggerTablePreview(c echo.Context) error { + _, err := uuid.Parse(c.Param("datalogger_id")) if err != nil { - return err + return echo.NewHTTPError(http.StatusBadRequest, message.MissingQueryParameter("datalogger_id")) } - - // Get preview from c.Request().Context() - preview, err := h.DataloggerService.GetDataloggerPreview(c.Request().Context(), dlID) + dataloggerTableID, err := uuid.Parse(c.Param("datalogger_table_id")) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, message.MissingQueryParameter("datalogger_table_id")) + } + preview, err := h.DataloggerService.GetDataloggerTablePreview(c.Request().Context(), dataloggerTableID) if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return echo.NewHTTPError(http.StatusNotFound, message.NotFound) + } return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } - return c.JSON(http.StatusOK, preview) } + +// ResetDataloggerTableName godoc +// +// @Summary resets a datalogger table name to be renamed by incoming telemetry +// @Tags datalogger +// @Produce json +// @Param datalogger_id path string true "datalogger uuid" Format(uuid) +// @Param datalogger_table_id path string true "datalogger table uuid" Format(uuid) +// @Success 200 {object} model.DataloggerPreview +// @Failure 400 {object} echo.HTTPError +// @Failure 404 {object} echo.HTTPError +// @Failure 500 {object} echo.HTTPError +// @Router /datalogger/{datalogger_id}/tables/{datalogger_table_id}/name [put] +// @Security Bearer +func (h *ApiHandler) ResetDataloggerTableName(c echo.Context) error { + _, err := uuid.Parse(c.Param("datalogger_id")) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, message.MissingQueryParameter("datalogger_id")) + } + dataloggerTableID, err := uuid.Parse(c.Param("datalogger_table_id")) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, message.MissingQueryParameter("datalogger_table_id")) + } + if err := h.DataloggerService.ResetDataloggerTableName(c.Request().Context(), dataloggerTableID); err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + return c.JSON(http.StatusOK, map[string]interface{}{"datalogger_table_id": dataloggerTableID}) +} diff --git a/api/internal/handler/datalogger_telemetry.go b/api/internal/handler/datalogger_telemetry.go index 46a09173..8bd0c953 100644 --- a/api/internal/handler/datalogger_telemetry.go +++ b/api/internal/handler/datalogger_telemetry.go @@ -1,8 +1,11 @@ package handler import ( + "database/sql" "encoding/json" + "errors" "fmt" + "log" "math" "net/http" "time" @@ -12,6 +15,8 @@ import ( "github.com/labstack/echo/v4" ) +const preparse = "preparse" + // CreateOrUpdateDataloggerMeasurements creates or updates measurements for a timeseries using an equivalency table func (h *TelemetryHandler) CreateOrUpdateDataloggerMeasurements(c echo.Context) error { modelName := c.Param("model") @@ -23,8 +28,10 @@ func (h *TelemetryHandler) CreateOrUpdateDataloggerMeasurements(c echo.Context) return echo.NewHTTPError(http.StatusBadRequest, "missing route param `sn`") } - // Make sure data logger is active - dl, err := h.DataloggerTelemetryService.GetDataloggerByModelSN(c.Request().Context(), modelName, sn) + ctx := c.Request().Context() + + // Make sure datalogger is active + dl, err := h.DataloggerTelemetryService.GetDataloggerByModelSN(ctx, modelName, sn) if err != nil { return echo.NewHTTPError(http.StatusBadRequest, err.Error()) } @@ -39,15 +46,23 @@ func (h *TelemetryHandler) CreateOrUpdateDataloggerMeasurements(c echo.Context) return echo.NewHTTPError(http.StatusBadRequest, err.Error()) } - prv := model.DataloggerPreview{DataloggerID: dl.ID} + var prv model.DataloggerPreview if err := prv.Preview.Set(rawJSON); err != nil { return err } prv.UpdateDate = time.Now() - err = h.DataloggerTelemetryService.UpdateDataloggerPreview(c.Request().Context(), prv) + err = h.DataloggerTelemetryService.UpdateDataloggerTablePreview(ctx, dl.ID, preparse, prv) if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + if !errors.Is(err, sql.ErrNoRows) { + return echo.NewHTTPError(http.StatusInternalServerError, message.InternalServerError) + } + if _, err := h.DataloggerService.GetOrCreateDataloggerTable(ctx, dl.ID, preparse); err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, message.InternalServerError) + } + if err = h.DataloggerTelemetryService.UpdateDataloggerTablePreview(ctx, dl.ID, preparse, prv); err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, message.InternalServerError) + } } if modelName == "CR6" || modelName == "CR1000X" { @@ -66,16 +81,18 @@ func (h *TelemetryHandler) CreateOrUpdateDataloggerMeasurements(c echo.Context) // HTTPPost: https://help.campbellsci.com/crbasic/cr350/#Instructions/httppost.htm?Highlight=httppost func getCR6Handler(h *TelemetryHandler, dl model.Datalogger, rawJSON []byte) echo.HandlerFunc { return func(c echo.Context) error { - // Errors are cellected and sent to data logger preview for debugging since data logger clients cannot parse responses + // Errors are cellected and sent to datalogger preview for debugging since datalogger clients cannot parse responses em := make([]string, 0) ctx := c.Request().Context() + tn := "preparse" - // The error returned from this function is not particularly relevant. Since these actual HTTP responses - // will be returned to data logger clients, this operates on a "best effort" basis, to collect logs to - // be previewed in the core web application. Additionally, the error code returned to the client data logger + // Since these HTTP responses will be returned to datalogger clients, this operates on a "best effort" basis + // to collect logs to be previewed in the core web application. The error code returned to the client datalogger // will sill be relavent to the arm of control flow that raised it. defer func() { - h.DataloggerTelemetryService.UpdateDataloggerError(ctx, &model.DataloggerError{DataloggerID: dl.ID, Errors: em}) + if err := h.DataloggerTelemetryService.UpdateDataloggerTableError(ctx, dl.ID, &tn, &model.DataloggerError{Errors: em}); err != nil { + log.Printf(err.Error()) + } }() // Upload Datalogger Measurements @@ -96,11 +113,30 @@ func getCR6Handler(h *TelemetryHandler, dl model.Datalogger, rawJSON []byte) ech return echo.NewHTTPError(http.StatusBadRequest, message.BadRequest) } - fields := pl.Head.Fields - eqt, err := h.EquivalencyTableService.GetEquivalencyTable(ctx, dl.ID) + // reroute deferred errors and previews to respective table + tn = pl.Head.Environment.TableName + + var prv model.DataloggerPreview + if err := prv.Preview.Set(rawJSON); err != nil { + return err + } + prv.UpdateDate = time.Now() + + if err := h.DataloggerTelemetryService.UpdateDataloggerTablePreview(ctx, dl.ID, tn, prv); err != nil { + em = append(em, fmt.Sprintf("%d: %s", http.StatusInternalServerError, err.Error())) + return echo.NewHTTPError(http.StatusInternalServerError, message.InternalServerError) + } + + dataloggerTableID, err := h.DataloggerService.GetOrCreateDataloggerTable(ctx, dl.ID, tn) + if err != nil { + em = append(em, fmt.Sprintf("%d: %s", http.StatusInternalServerError, err.Error())) + return echo.NewHTTPError(http.StatusInternalServerError, message.InternalServerError) + } + + eqt, err := h.EquivalencyTableService.GetEquivalencyTable(ctx, dataloggerTableID) if err != nil { em = append(em, fmt.Sprintf("%d: %s", http.StatusInternalServerError, err.Error())) - return echo.NewHTTPError(http.StatusNotFound, message.NotFound) + return echo.NewHTTPError(http.StatusInternalServerError, message.InternalServerError) } eqtFields := make(map[string]model.EquivalencyTableRow) @@ -111,6 +147,7 @@ func getCR6Handler(h *TelemetryHandler, dl model.Datalogger, rawJSON []byte) ech } } + fields := pl.Head.Fields mcs := make([]model.MeasurementCollection, len(fields)) // Error if there is no field name in equivalency table to map the field name in the raw payload to @@ -119,7 +156,7 @@ func getCR6Handler(h *TelemetryHandler, dl model.Datalogger, rawJSON []byte) ech // Map field to timeseries id row, exists := eqtFields[f.Name] if !exists { - em = append(em, fmt.Sprintf("field '%s' from data logger does not exist in equivalency table", f.Name)) + em = append(em, fmt.Sprintf("field '%s' from datalogger does not exist in equivalency table", f.Name)) continue } if row.InstrumentID == nil { @@ -128,7 +165,7 @@ func getCR6Handler(h *TelemetryHandler, dl model.Datalogger, rawJSON []byte) ech continue } if row.TimeseriesID == nil { - em = append(em, fmt.Sprintf("field '%s' not mapped to time series in equivalency table", f.Name)) + em = append(em, fmt.Sprintf("field '%s' not mapped to timeseries in equivalency table", f.Name)) delete(eqtFields, f.Name) continue } @@ -161,7 +198,7 @@ func getCR6Handler(h *TelemetryHandler, dl model.Datalogger, rawJSON []byte) ech // This map should be empty if all fields are mapped, otherwise the error is added for eqtName := range eqtFields { - em = append(em, fmt.Sprintf("field '%s' in equivalency table does not match any fields from data logger", eqtName)) + em = append(em, fmt.Sprintf("field '%s' in equivalency table does not match any fields from datalogger", eqtName)) } if _, err = h.MeasurementService.CreateOrUpdateTimeseriesMeasurements(ctx, mcs); err != nil { diff --git a/api/internal/handler/datalogger_test.go b/api/internal/handler/datalogger_test.go index 1af76b5c..dbc5f409 100644 --- a/api/internal/handler/datalogger_test.go +++ b/api/internal/handler/datalogger_test.go @@ -9,20 +9,72 @@ import ( "github.com/xeipuuv/gojsonschema" ) -const dataloggerSchema = `{ +const dataloggerTableSchema = `{ "type": "object", "properties": { - "id": { "type": "string" } - }, - "required": ["id"] + "id": { "type": "string" }, + "name": { "type": "string" } + } }` +var dataloggerSchema = fmt.Sprintf(`{ + "type": "object", + "properties": { + "id": { "type": "string" }, + "name": { "type": "string" }, + "sn": { "type": "string" }, + "project_id": { "type": "string" }, + "creator": { "type": "string" }, + "creator_username": { "type": "string" }, + "create_date": { "type": "string" }, + "updater": { "type": ["string", "null"] }, + "updater_username": { "type": "string" }, + "update_date": { "type": ["string", "null"] }, + "slug": { "type": "string" }, + "model_id": { "type": "string" }, + "model": { "type": "string" }, + "errors": { "type": "array", "items": { "type": "string" } }, + "tables": { "type": "array", "items": %s }, + "key": { "type": "string" } + }, + "required": [ + "id", + "name", + "sn", + "project_id", + "creator", + "creator_username", + "create_date", + "slug", + "model_id", + "model", + "errors", + "tables" + ] +}`, dataloggerTableSchema) + var dataloggerObjectLoader = gojsonschema.NewStringLoader(dataloggerSchema) +var dataloggerArrayLoader = gojsonschema.NewStringLoader(fmt.Sprintf(`{ + "type": "array", + "items": %s +}`, dataloggerSchema)) + +const dataloggerPreviewSchema = `{ + "type": "object", + "properties": { + "datalogger_table_id": { "type": "string" }, + "update_date": { "type": "string" }, + "preview": { "type": ["object", "array", "null"] } + } +}` + +var dataloggerPreviewLoader = gojsonschema.NewStringLoader(dataloggerPreviewSchema) // datalogger 1 for read-only tests since it's used with the mock datalogger service const ( - testDataloggerID1 = "83a7345c-62d8-4e29-84db-c2e36f8bc40d" - testDataloggerID2 = "c0b65315-f802-4ca5-a4dd-7e0cfcffd057" + testDataloggerID1 = "83a7345c-62d8-4e29-84db-c2e36f8bc40d" + testDataloggerID2 = "c0b65315-f802-4ca5-a4dd-7e0cfcffd057" + testDataloggerTableID = "98a77c65-e5c4-49ed-8fb4-b0ffd06add4c" ) const createDataloggerBody = `{ @@ -43,6 +95,10 @@ const updateDataloggerBody = `{ func TestDatalogger(t *testing.T) { objSchema, err := gojsonschema.NewSchema(dataloggerObjectLoader) assert.Nil(t, err) + arrSchema, err := gojsonschema.NewSchema(dataloggerArrayLoader) + assert.Nil(t, err) + previewObjSchema, err := gojsonschema.NewSchema(dataloggerPreviewLoader) + assert.Nil(t, err) tests := []HTTPTest{ { @@ -58,18 +114,27 @@ func TestDatalogger(t *testing.T) { URL: fmt.Sprintf("/dataloggers?project_id=%s", testProjectID), Method: http.MethodGet, ExpectedStatus: http.StatusOK, + ExpectedSchema: arrSchema, }, { Name: "GetDatalogger", URL: fmt.Sprintf("/datalogger/%s", testDataloggerID1), Method: http.MethodGet, ExpectedStatus: http.StatusOK, + ExpectedSchema: objSchema, }, { - Name: "GetDataloggerPreview", - URL: fmt.Sprintf("/datalogger/%s/preview", testDataloggerID1), + Name: "GetDataloggerTablePreview", + URL: fmt.Sprintf("/datalogger/%s/tables/%s/preview", testDataloggerID1, testDataloggerTableID), Method: http.MethodGet, ExpectedStatus: http.StatusOK, + ExpectedSchema: previewObjSchema, + }, + { + Name: "ResetDataloggerTableName", + URL: fmt.Sprintf("/datalogger/%s/tables/%s/name", testDataloggerID1, testDataloggerTableID), + Method: http.MethodPut, + ExpectedStatus: http.StatusOK, }, { Name: "UpdateDatalogger", @@ -77,12 +142,14 @@ func TestDatalogger(t *testing.T) { Method: http.MethodPut, Body: updateDataloggerBody, ExpectedStatus: http.StatusOK, + ExpectedSchema: objSchema, }, { Name: "CycleDataloggerKey", URL: fmt.Sprintf("/datalogger/%s/key", testDataloggerID2), Method: http.MethodPut, ExpectedStatus: http.StatusOK, + ExpectedSchema: objSchema, }, { Name: "DeleteDatalogger", diff --git a/api/internal/handler/equivalency_table.go b/api/internal/handler/equivalency_table.go index f4545e43..ae1ab338 100644 --- a/api/internal/handler/equivalency_table.go +++ b/api/internal/handler/equivalency_table.go @@ -1,6 +1,8 @@ package handler import ( + "database/sql" + "errors" "net/http" "github.com/USACE/instrumentation-api/api/internal/message" @@ -15,11 +17,12 @@ import ( // @Tags equivalency-table // @Produce json // @Param datalogger_id path string true "datalogger uuid" Format(uuid) +// @Param datalogger_table_id path string true "datalogger table uuid" Format(uuid) // @Success 200 {array} model.EquivalencyTable // @Failure 400 {object} echo.HTTPError // @Failure 404 {object} echo.HTTPError // @Failure 500 {object} echo.HTTPError -// @Router /datalogger/{datalogger_id}/equivalency_table [get] +// @Router /datalogger/{datalogger_id}/tables/{datalogger_table_id}/equivalency_table [get] // @Security Bearer func (h *ApiHandler) GetEquivalencyTable(c echo.Context) error { dlID, err := uuid.Parse(c.Param("datalogger_id")) @@ -27,13 +30,23 @@ func (h *ApiHandler) GetEquivalencyTable(c echo.Context) error { return echo.NewHTTPError(http.StatusBadRequest, message.MalformedID) } - if err := h.DataloggerService.VerifyDataloggerExists(c.Request().Context(), dlID); err != nil { + dataloggerTableID, err := uuid.Parse(c.Param("datalogger_table_id")) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, message.MalformedID) + } + + ctx := c.Request().Context() + + if err := h.DataloggerService.VerifyDataloggerExists(ctx, dlID); err != nil { return echo.NewHTTPError(http.StatusNotFound, err.Error()) } - t, err := h.EquivalencyTableService.GetEquivalencyTable(c.Request().Context(), dlID) + t, err := h.EquivalencyTableService.GetEquivalencyTable(ctx, dataloggerTableID) if err != nil { - return c.JSON(http.StatusNotFound, t) + if errors.Is(err, sql.ErrNoRows) { + return echo.NewHTTPError(http.StatusNotFound, message.NotFound) + } + return echo.NewHTTPError(http.StatusInternalServerError, message.InternalServerError) } return c.JSON(http.StatusOK, t) @@ -41,15 +54,17 @@ func (h *ApiHandler) GetEquivalencyTable(c echo.Context) error { // CreateEquivalencyTable godoc // -// @Summary creates an equivalency table for a datalogger +// @Summary creates an equivalency table for a datalogger and auto create data logger table if not exists // @Tags equivalency-table // @Produce json // @Param datalogger_id path string true "datalogger uuid" Format(uuid) +// @Param datalogger_table_id path string true "datalogger table uuid" Format(uuid) // @Param equivalency_table body model.EquivalencyTable true "equivalency table payload" // @Success 200 {object} map[string]interface{} // @Failure 400 {object} echo.HTTPError // @Failure 404 {object} echo.HTTPError // @Failure 500 {object} echo.HTTPError +// @Router /datalogger/{datalogger_id}/tables/{datalogger_table_id}/equivalency_table [post] // @Router /datalogger/{datalogger_id}/equivalency_table [post] // @Security Bearer func (h *ApiHandler) CreateEquivalencyTable(c echo.Context) error { @@ -63,19 +78,43 @@ func (h *ApiHandler) CreateEquivalencyTable(c echo.Context) error { return echo.NewHTTPError(http.StatusBadRequest, err.Error()) } + var dataloggerTableID uuid.UUID + tableIDParam := c.Param("datalogger_table_id") + + ctx := c.Request().Context() + + if tableIDParam != "" { + dataloggerTableID, err = uuid.Parse(tableIDParam) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, message.MalformedID) + } + } else { + if t.DataloggerTableName == "" { + return echo.NewHTTPError(http.StatusBadRequest, "payload must contain datalogger_table_name field") + } + dataloggerTableID, err = h.DataloggerService.GetOrCreateDataloggerTable(ctx, dlID, t.DataloggerTableName) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + t.DataloggerTableID = dataloggerTableID + } + if dlID != t.DataloggerID { return echo.NewHTTPError(http.StatusBadRequest, message.MatchRouteParam("`datalogger_id`")) } + if dataloggerTableID != t.DataloggerTableID { + return echo.NewHTTPError(http.StatusBadRequest, message.MatchRouteParam("`datalogger_table_id`")) + } - if err := h.DataloggerService.VerifyDataloggerExists(c.Request().Context(), dlID); err != nil { + if err := h.DataloggerService.VerifyDataloggerExists(ctx, dlID); err != nil { return echo.NewHTTPError(http.StatusNotFound, err.Error()) } - if err := h.EquivalencyTableService.CreateEquivalencyTable(c.Request().Context(), t); err != nil { + if err := h.EquivalencyTableService.CreateEquivalencyTable(ctx, t); err != nil { return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } - return c.JSON(http.StatusCreated, map[string]interface{}{"datalogger_id": dlID}) + return c.JSON(http.StatusCreated, map[string]interface{}{"datalogger_id": dlID, "datalogger_table_id": dataloggerTableID}) } // UpdateEquivalencyTable godoc @@ -84,12 +123,13 @@ func (h *ApiHandler) CreateEquivalencyTable(c echo.Context) error { // @Tags equivalency-table // @Produce json // @Param datalogger_id path string true "datalogger uuid" Format(uuid) +// @Param datalogger_table_id path string true "datalogger table uuid" Format(uuid) // @Param equivalency_table body model.EquivalencyTable true "equivalency table payload" // @Success 200 {object} model.EquivalencyTable // @Failure 400 {object} echo.HTTPError // @Failure 404 {object} echo.HTTPError // @Failure 500 {object} echo.HTTPError -// @Router /datalogger/{datalogger_id}/equivalency_table [put] +// @Router /datalogger/{datalogger_id}/tables/{datalogger_table_id}/equivalency_table [put] // @Security Bearer func (h *ApiHandler) UpdateEquivalencyTable(c echo.Context) error { dlID, err := uuid.Parse(c.Param("datalogger_id")) @@ -97,7 +137,12 @@ func (h *ApiHandler) UpdateEquivalencyTable(c echo.Context) error { return echo.NewHTTPError(http.StatusBadRequest, message.MalformedID) } - t := model.EquivalencyTable{DataloggerID: dlID} + dataloggerTableID, err := uuid.Parse(c.Param("datalogger_table_id")) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, message.MalformedID) + } + + t := model.EquivalencyTable{DataloggerID: dlID, DataloggerTableID: dataloggerTableID} if err := c.Bind(&t); err != nil { return echo.NewHTTPError(http.StatusBadRequest, err.Error()) } @@ -105,6 +150,9 @@ func (h *ApiHandler) UpdateEquivalencyTable(c echo.Context) error { if dlID != t.DataloggerID { return echo.NewHTTPError(http.StatusBadRequest, message.MatchRouteParam("`datalogger_id`")) } + if dataloggerTableID != t.DataloggerTableID { + return echo.NewHTTPError(http.StatusBadRequest, message.MatchRouteParam("`datalogger_table_id`")) + } ctx := c.Request().Context() @@ -112,7 +160,7 @@ func (h *ApiHandler) UpdateEquivalencyTable(c echo.Context) error { return echo.NewHTTPError(http.StatusNotFound, err.Error()) } - eqtUpdated, err := h.EquivalencyTableService.UpdateEquivalencyTable(ctx, dlID, t) + eqtUpdated, err := h.EquivalencyTableService.UpdateEquivalencyTable(ctx, t) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } @@ -122,7 +170,7 @@ func (h *ApiHandler) UpdateEquivalencyTable(c echo.Context) error { // DeleteEquivalencyTable godoc // -// @Summary deletes an equivalency table +// @Summary deletes an equivalency table and corresponding datalogger table // @Tags equivalency-table // @Produce json // @Param datalogger_id path string true "datalogger uuid" Format(uuid) @@ -130,7 +178,7 @@ func (h *ApiHandler) UpdateEquivalencyTable(c echo.Context) error { // @Failure 400 {object} echo.HTTPError // @Failure 404 {object} echo.HTTPError // @Failure 500 {object} echo.HTTPError -// @Router /datalogger/{datalogger_id}/equivalency_table [delete] +// @Router /datalogger/{datalogger_id}/tables/{datalogger_table_id}/equivalency_table [delete] // @Security Bearer func (h *ApiHandler) DeleteEquivalencyTable(c echo.Context) error { dlID, err := uuid.Parse(c.Param("datalogger_id")) @@ -138,15 +186,22 @@ func (h *ApiHandler) DeleteEquivalencyTable(c echo.Context) error { return echo.NewHTTPError(http.StatusBadRequest, message.MalformedID) } - if err := h.DataloggerService.VerifyDataloggerExists(c.Request().Context(), dlID); err != nil { + dataloggerTableID, err := uuid.Parse(c.Param("datalogger_table_id")) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, message.MalformedID) + } + + ctx := c.Request().Context() + + if err := h.DataloggerService.VerifyDataloggerExists(ctx, dlID); err != nil { return echo.NewHTTPError(http.StatusNotFound, err.Error()) } - if err := h.EquivalencyTableService.DeleteEquivalencyTable(c.Request().Context(), dlID); err != nil { + if err := h.DataloggerService.DeleteDataloggerTable(ctx, dataloggerTableID); err != nil { return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } - return c.JSON(http.StatusOK, map[string]interface{}{"datalogger_id": dlID}) + return c.JSON(http.StatusOK, map[string]interface{}{"datalogger_id": dlID, "datalogger_table_id": dataloggerTableID}) } // DeleteEquivalencyTableRow godoc @@ -155,31 +210,36 @@ func (h *ApiHandler) DeleteEquivalencyTable(c echo.Context) error { // @Tags equivalency-table // @Produce json // @Param datalogger_id path string true "datalogger uuid" Format(uuid) -// @Param id query string true "equivalency table row uuid" Format(uuid) +// @Param row_id path string true "equivalency table row uuid" Format(uuid) // @Success 200 {object} map[string]interface{} // @Failure 400 {object} echo.HTTPError // @Failure 404 {object} echo.HTTPError // @Failure 500 {object} echo.HTTPError -// @Router /datalogger/{datalogger_id}/equivalency_table/row [delete] +// @Router /datalogger/{datalogger_id}/tables/{datalogger_table_id}/equivalency_table/row/{row_id} [delete] // @Security Bearer func (h *ApiHandler) DeleteEquivalencyTableRow(c echo.Context) error { dlID, err := uuid.Parse(c.Param("datalogger_id")) if err != nil { return echo.NewHTTPError(http.StatusBadRequest, message.MalformedID) } - - rID, err := uuid.Parse(c.QueryParam("id")) + _, err = uuid.Parse(c.Param("datalogger_table_id")) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, message.MalformedID) + } + rowID, err := uuid.Parse(c.Param("row_id")) if err != nil { return echo.NewHTTPError(http.StatusBadRequest, message.MalformedID) } - if err := h.DataloggerService.VerifyDataloggerExists(c.Request().Context(), dlID); err != nil { + ctx := c.Request().Context() + + if err := h.DataloggerService.VerifyDataloggerExists(ctx, dlID); err != nil { return echo.NewHTTPError(http.StatusNotFound, err.Error()) } - if err := h.EquivalencyTableService.DeleteEquivalencyTableRow(c.Request().Context(), dlID, rID); err != nil { + if err := h.EquivalencyTableService.DeleteEquivalencyTableRow(ctx, rowID); err != nil { return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } - return c.JSON(http.StatusOK, map[string]interface{}{"row_id": rID}) + return c.JSON(http.StatusOK, map[string]interface{}{"row_id": rowID}) } diff --git a/api/internal/handler/equivalency_table_test.go b/api/internal/handler/equivalency_table_test.go index a0444585..e7ba05fe 100644 --- a/api/internal/handler/equivalency_table_test.go +++ b/api/internal/handler/equivalency_table_test.go @@ -4,12 +4,39 @@ import ( "fmt" "net/http" "testing" + + "github.com/stretchr/testify/assert" + "github.com/xeipuuv/gojsonschema" ) +const equivalencyTableRowSchema = `{ + "type": "object", + "properties": { + "id": { "type": "string" }, + "field_name": { "type": "string" }, + "display_name": { "type": "string" }, + "instrument_id": { "type": ["string", "null"] }, + "timeseries_id": { "type": ["string", "null"] } + } +}` + +var equivalencyTableSchema = fmt.Sprintf(`{ + "type": "object", + "properties": { + "datalogger_id" : { "type": "string" }, + "datalogger_table_id": { "type": "string" }, + "rows": { "type": "array", "items": %s } + }, + "required": ["datalogger_id", "datalogger_table_id"] +}`, equivalencyTableRowSchema) + +var equivalencyTableLoader = gojsonschema.NewStringLoader(equivalencyTableSchema) + const testEquivalencyTableRowID = "2f1f7c3d-8b6f-4b11-917e-8f049eb6c62b" const createEquivalencyTableBody = `{ "datalogger_id": "83a7345c-62d8-4e29-84db-c2e36f8bc40d", + "datalogger_table_id": "98a77c65-e5c4-49ed-8fb4-b0ffd06add4c", "rows": [ { "field_name": "new field name", @@ -20,8 +47,22 @@ const createEquivalencyTableBody = `{ ] }` +const createEquivalencyTableNoDataloggerTableBody = `{ + "datalogger_id": "83a7345c-62d8-4e29-84db-c2e36f8bc40d", + "datalogger_table_name": "New Test Table", + "rows": [ + { + "field_name": "other new field name", + "display_name": "test 456", + "instrument_id": "a7540f69-c41e-43b3-b655-6e44097edb7e", + "timeseries_id": "844fb688-e77c-481e-bff9-81a0fff9f3f2" + } + ] +}` + const updateEquivalencyTableBody = `{ "datalogger_id": "83a7345c-62d8-4e29-84db-c2e36f8bc40d", + "datalogger_table_id": "98a77c65-e5c4-49ed-8fb4-b0ffd06add4c", "rows": [ { "id": "40ceff10-cdc3-4715-a4ca-c1e570fe25de", @@ -41,36 +82,50 @@ const updateEquivalencyTableBody = `{ }` func TestEquivalencyTable(t *testing.T) { + objSchema, err := gojsonschema.NewSchema(equivalencyTableLoader) + assert.Nil(t, err) + tests := []HTTPTest{ { Name: "CreateEquivalencyTable", - URL: fmt.Sprintf("/datalogger/%s/equivalency_table", testDataloggerID1), + URL: fmt.Sprintf("/datalogger/%s/tables/%s/equivalency_table", testDataloggerID1, testDataloggerTableID), Method: http.MethodPost, Body: createEquivalencyTableBody, ExpectedStatus: http.StatusCreated, + ExpectedSchema: objSchema, }, { - Name: "GetEquivalencyTable", + Name: "CreateEquivalencyTableAndDataloggerTable", URL: fmt.Sprintf("/datalogger/%s/equivalency_table", testDataloggerID1), + Method: http.MethodPost, + Body: createEquivalencyTableNoDataloggerTableBody, + ExpectedStatus: http.StatusCreated, + ExpectedSchema: objSchema, + }, + { + Name: "GetEquivalencyTable", + URL: fmt.Sprintf("/datalogger/%s/tables/%s/equivalency_table", testDataloggerID1, testDataloggerTableID), Method: http.MethodGet, ExpectedStatus: http.StatusOK, + ExpectedSchema: objSchema, }, { Name: "UpdateEquivalencyTable", - URL: fmt.Sprintf("/datalogger/%s/equivalency_table", testDataloggerID1), + URL: fmt.Sprintf("/datalogger/%s/tables/%s/equivalency_table", testDataloggerID1, testDataloggerTableID), Method: http.MethodPut, Body: updateEquivalencyTableBody, ExpectedStatus: http.StatusOK, + ExpectedSchema: objSchema, }, { Name: "DeleteEquivalencyTableRow", - URL: fmt.Sprintf("/datalogger/%s/equivalency_table/row?id=%s", testDataloggerID1, testEquivalencyTableRowID), + URL: fmt.Sprintf("/datalogger/%s/tables/%s/equivalency_table/row/%s", testDataloggerID1, testDataloggerTableID, testEquivalencyTableRowID), Method: http.MethodDelete, ExpectedStatus: http.StatusOK, }, { Name: "DeleteEquivalencyTable", - URL: fmt.Sprintf("/datalogger/%s/equivalency_table", testDataloggerID1), + URL: fmt.Sprintf("/datalogger/%s/tables/%s/equivalency_table", testDataloggerID1, testDataloggerTableID), Method: http.MethodDelete, ExpectedStatus: http.StatusOK, }} diff --git a/api/internal/model/datalogger.go b/api/internal/model/datalogger.go index 31c37b7a..b8a932de 100644 --- a/api/internal/model/datalogger.go +++ b/api/internal/model/datalogger.go @@ -2,8 +2,6 @@ package model import ( "context" - "database/sql" - "errors" "fmt" "time" @@ -21,22 +19,22 @@ type Telemetry struct { } type Datalogger struct { - ID uuid.UUID `json:"id" db:"id"` - Name string `json:"name" db:"name"` - SN string `json:"sn" db:"sn"` - ProjectID uuid.UUID `json:"project_id" db:"project_id"` - Creator uuid.UUID `json:"creator" db:"creator"` - CreatorUsername string `json:"creator_username" db:"creator_username"` - CreateDate time.Time `json:"create_date" db:"create_date"` - Updater *uuid.UUID `json:"updater" db:"updater"` - UpdaterUsername string `json:"updater_username" db:"updater_username"` - UpdateDate *time.Time `json:"update_date" db:"update_date"` - Slug string `json:"slug" db:"slug"` - ModelID uuid.UUID `json:"model_id" db:"model_id"` - Model *string `json:"model" db:"model"` - Deleted bool `json:"-" db:"deleted"` - Errors []string `json:"errors" db:"-"` - PgErrors pgtype.TextArray `json:"-" db:"errors"` + ID uuid.UUID `json:"id" db:"id"` + Name string `json:"name" db:"name"` + SN string `json:"sn" db:"sn"` + ProjectID uuid.UUID `json:"project_id" db:"project_id"` + Creator uuid.UUID `json:"creator" db:"creator"` + CreatorUsername string `json:"creator_username" db:"creator_username"` + CreateDate time.Time `json:"create_date" db:"create_date"` + Updater *uuid.UUID `json:"updater" db:"updater"` + UpdaterUsername string `json:"updater_username" db:"updater_username"` + UpdateDate *time.Time `json:"update_date" db:"update_date"` + Slug string `json:"slug" db:"slug"` + ModelID uuid.UUID `json:"model_id" db:"model_id"` + Model *string `json:"model" db:"model"` + Errors []string `json:"errors" db:"-"` + PgErrors pgtype.TextArray `json:"-" db:"errors"` + Tables dbJSONSlice[DataloggerTable] `json:"tables" db:"tables"` } type DataloggerWithKey struct { @@ -44,17 +42,20 @@ type DataloggerWithKey struct { Key string `json:"key"` } +type DataloggerTable struct { + ID uuid.UUID `json:"id" db:"id"` + TableName string `json:"table_name" db:"table_name"` +} + type DataloggerPreview struct { - DataloggerID uuid.UUID `json:"datalogger_id" db:"datalogger_id"` - UpdateDate time.Time `json:"update_date" db:"update_date"` - Preview pgtype.JSON `json:"preview" db:"preview"` - Model *string `json:"model,omitempty"` - SN *string `json:"sn,omitempty"` + DataloggerTableID uuid.UUID `json:"datalogger_table_id" db:"datalogger_table_id"` + UpdateDate time.Time `json:"update_date" db:"update_date"` + Preview pgtype.JSON `json:"preview" db:"preview"` } type DataloggerError struct { - DataloggerID uuid.UUID `json:"datalogger_id" db:"datalogger_id"` - Errors []string `json:"errors" db:"errors"` + DataloggerTableID uuid.UUID `json:"datalogger_id" db:"datalogger_id"` + Errors []string `json:"errors" db:"errors"` } const getDataloggerModelName = ` @@ -144,12 +145,12 @@ func (q *Queries) CreateDataloggerHash(ctx context.Context, dataloggerID uuid.UU return key, nil } -const createDataloggerPreview = ` - INSERT INTO datalogger_preview (datalogger_id) VALUES ($1) +const createDataloggerTablePreview = ` + INSERT INTO datalogger_preview (datalogger_table_id) VALUES ($1) ` -func (q *Queries) CreateDataloggerPreview(ctx context.Context, dataloggerID uuid.UUID) error { - _, err := q.db.ExecContext(ctx, createDataloggerPreview, dataloggerID) +func (q *Queries) CreateDataloggerTablePreview(ctx context.Context, dataloggerTableID uuid.UUID) error { + _, err := q.db.ExecContext(ctx, createDataloggerTablePreview, dataloggerTableID) return err } @@ -223,21 +224,6 @@ func (q *Queries) DeleteDatalogger(ctx context.Context, dl Datalogger) error { return err } -const getDataloggerPreview = ` - SELECT * FROM v_datalogger_preview WHERE datalogger_id = $1 -` - -func (q *Queries) GetDataloggerPreview(ctx context.Context, dlID uuid.UUID) (DataloggerPreview, error) { - var dlp DataloggerPreview - if err := q.db.GetContext(ctx, &dlp, getDataloggerPreview, dlID); err != nil { - if errors.Is(err, sql.ErrNoRows) { - return dlp, fmt.Errorf("preview not found") - } - return dlp, err - } - return dlp, nil -} - const listDataloggerSlugs = ` SELECT slug FROM datalogger ` @@ -247,3 +233,62 @@ func (q *Queries) ListDataloggerSlugs(ctx context.Context) ([]string, error) { err := q.db.SelectContext(ctx, &aa, listDataloggerSlugs) return aa, err } + +const getDataloggerTablePreview = ` + SELECT * FROM v_datalogger_preview WHERE datalogger_table_id = $1 +` + +func (q *Queries) GetDataloggerTablePreview(ctx context.Context, dataloggerTableID uuid.UUID) (DataloggerPreview, error) { + var dlp DataloggerPreview + err := q.db.GetContext(ctx, &dlp, getDataloggerTablePreview, dataloggerTableID) + return dlp, err +} + +const resetDataloggerTableName = ` + UPDATE datalogger_table SET table_name = '' WHERE id = $1 +` + +func (q *Queries) ResetDataloggerTableName(ctx context.Context, dataloggerTableID uuid.UUID) error { + _, err := q.db.ExecContext(ctx, resetDataloggerTableName, dataloggerTableID) + return err +} + +const renameEmptyDataloggerTableName = ` + UPDATE datalogger_table + SET table_name = $2 + WHERE table_name = '' AND datalogger_id = $1 + AND NOT EXISTS ( + SELECT 1 FROM datalogger_table WHERE datalogger_id = $1 AND table_name = $2 + ); +` + +func (q *Queries) RenameEmptyDataloggerTableName(ctx context.Context, dataloggerID uuid.UUID, tableName string) error { + _, err := q.db.ExecContext(ctx, renameEmptyDataloggerTableName, dataloggerID, tableName) + return err +} + +const getOrCreateDataloggerTable = ` + WITH dt AS ( + INSERT INTO datalogger_table (datalogger_id, table_name) VALUES ($1, $2) + ON CONFLICT ON CONSTRAINT datalogger_table_datalogger_id_table_name_key DO NOTHING + RETURNING id + ) + SELECT id FROM dt + UNION + SELECT id FROM datalogger_table WHERE datalogger_id = $1 AND table_name = $2 +` + +func (q *Queries) GetOrCreateDataloggerTable(ctx context.Context, dataloggerID uuid.UUID, tableName string) (uuid.UUID, error) { + var tID uuid.UUID + err := q.db.GetContext(ctx, &tID, getOrCreateDataloggerTable, dataloggerID, tableName) + return tID, err +} + +const deleteDataloggerTable = ` + DELETE FROM datalogger_table WHERE id = $1 +` + +func (q *Queries) DeleteDataloggerTable(ctx context.Context, dataloggerTableID uuid.UUID) error { + _, err := q.db.ExecContext(ctx, deleteDataloggerTable, dataloggerTableID) + return err +} diff --git a/api/internal/model/datalogger_telemetry.go b/api/internal/model/datalogger_telemetry.go index f212d15d..0ef54b69 100644 --- a/api/internal/model/datalogger_telemetry.go +++ b/api/internal/model/datalogger_telemetry.go @@ -30,30 +30,36 @@ func (q *Queries) GetDataloggerHashByModelSN(ctx context.Context, modelName, sn return hash, nil } -const updateDataloggerPreview = ` - UPDATE datalogger_preview SET preview = $2, update_date = $3 - WHERE datalogger_id = $1 +const updateDataloggerTablePreview = ` + UPDATE datalogger_preview SET preview = $3, update_date = $4 + WHERE datalogger_table_id IN (SELECT id FROM datalogger_table WHERE datalogger_id = $1 AND table_name = $2) ` -func (q *Queries) UpdateDataloggerPreview(ctx context.Context, dlp DataloggerPreview) error { - _, err := q.db.ExecContext(ctx, updateDataloggerPreview, dlp.DataloggerID, dlp.Preview, dlp.UpdateDate) +func (q *Queries) UpdateDataloggerTablePreview(ctx context.Context, dataloggerID uuid.UUID, tableName string, dlp DataloggerPreview) error { + _, err := q.db.ExecContext(ctx, updateDataloggerTablePreview, dataloggerID, tableName, dlp.Preview, dlp.UpdateDate) return err } -const deleteDataloggerError = ` - DELETE FROM datalogger_error WHERE datalogger_id = $1 +const deleteDataloggerTableError = ` + DELETE FROM datalogger_error + WHERE datalogger_table_id IN (SELECT id FROM datalogger_table WHERE datalogger_id = $1 AND table_name = $2) ` -func (q *Queries) DeleteDataloggerError(ctx context.Context, dataloggerID uuid.UUID) error { - _, err := q.db.ExecContext(ctx, deleteDataloggerError, dataloggerID) +func (q *Queries) DeleteDataloggerTableError(ctx context.Context, dataloggerID uuid.UUID, tableName *string) error { + _, err := q.db.ExecContext(ctx, deleteDataloggerTableError, dataloggerID, tableName) return err } const createDataloggerError = ` - INSERT INTO datalogger_error (datalogger_id, error_message) VALUES ($1, $2) + INSERT INTO datalogger_error (datalogger_table_id, error_message) + SELECT id, $3 FROM datalogger_table + WHERE datalogger_id = $1 AND table_name = $2 + AND NOT EXISTS ( + SELECT 1 FROM datalogger_table WHERE datalogger_id = $1 AND table_name = $2 + ); ` -func (q *Queries) CreateDataloggerError(ctx context.Context, dataloggerID uuid.UUID, errMessage string) error { - _, err := q.db.ExecContext(ctx, createDataloggerError, dataloggerID, errMessage) +func (q *Queries) CreateDataloggerTableError(ctx context.Context, dataloggerID uuid.UUID, tableName *string, errMessage string) error { + _, err := q.db.ExecContext(ctx, createDataloggerError, dataloggerID, tableName, errMessage) return err } diff --git a/api/internal/model/equivalency_table.go b/api/internal/model/equivalency_table.go index 5bca3ce1..bc193bfe 100644 --- a/api/internal/model/equivalency_table.go +++ b/api/internal/model/equivalency_table.go @@ -11,21 +11,38 @@ import ( ) type EquivalencyTable struct { - DataloggerID uuid.UUID `json:"datalogger_id" db:"datalogger_id"` - Rows []EquivalencyTableRow `json:"rows" db:"rows"` + DataloggerID uuid.UUID `json:"datalogger_id" db:"datalogger_id"` + DataloggerTableID uuid.UUID `json:"datalogger_table_id" db:"datalogger_table_id"` + DataloggerTableName string `json:"datalogger_table_name" db:"datalogger_table_name"` + Rows dbJSONSlice[EquivalencyTableRow] `json:"rows" db:"fields"` } type EquivalencyTableRow struct { ID uuid.UUID `json:"id" db:"id"` - DataloggerID uuid.UUID `json:"-" db:"datalogger_id"` - SN string `json:"-" db:"sn"` - Model string `json:"-" db:"model"` FieldName string `json:"field_name" db:"field_name"` DisplayName string `json:"display_name" db:"display_name"` InstrumentID *uuid.UUID `json:"instrument_id" db:"instrument_id"` TimeseriesID *uuid.UUID `json:"timeseries_id" db:"timeseries_id"` } +const getIsValidDataloggerTable = ` + SELECT NOT EXISTS ( + SELECT * FROM datalogger_table WHERE id = $1 AND table_name = 'preparse' + ) +` + +// GetIsValidDataloggerTable verifies that a datalogger table is not "preparse" (read-only) +func (q *Queries) GetIsValidDataloggerTable(ctx context.Context, dataloggerTableID uuid.UUID) error { + var isValid bool + if err := q.db.GetContext(ctx, &isValid, getIsValidDataloggerTable, dataloggerTableID); err != nil { + return err + } + if !isValid { + return fmt.Errorf("table preparse is read only %s", dataloggerTableID) + } + return nil +} + const getIsValidEquivalencyTableTimeseries = ` SELECT NOT EXISTS ( SELECT id FROM v_timeseries_computed @@ -50,46 +67,35 @@ func (q *Queries) GetIsValidEquivalencyTableTimeseries(ctx context.Context, tsID const getEquivalencyTable = ` SELECT - id, datalogger_id, - field_name, - display_name, - instrument_id, - timeseries_id - FROM datalogger_equivalency_table - WHERE datalogger_id = $1 + datalogger_table_id, + fields + FROM v_datalogger_equivalency_table + WHERE datalogger_table_id = $1 ` // GetEquivalencyTable returns a single Datalogger EquivalencyTable -func (q *Queries) GetEquivalencyTable(ctx context.Context, dlID uuid.UUID) (EquivalencyTable, error) { - tr := make([]EquivalencyTableRow, 0) - err := q.db.SelectContext(ctx, &tr, getEquivalencyTable, dlID) - et := EquivalencyTable{ - DataloggerID: dlID, - Rows: tr, - } +func (q *Queries) GetEquivalencyTable(ctx context.Context, dataloggerTableID uuid.UUID) (EquivalencyTable, error) { + var et EquivalencyTable + err := q.db.GetContext(ctx, &et, getEquivalencyTable, dataloggerTableID) return et, err } const createEquivalencyTableRow = ` INSERT INTO datalogger_equivalency_table - (datalogger_id, field_name, display_name, instrument_id, timeseries_id) - VALUES ($1, $2, $3, $4, $5) - ON CONFLICT ON CONSTRAINT unique_datalogger_field DO NOTHING + (datalogger_id, datalogger_table_id, field_name, display_name, instrument_id, timeseries_id) + VALUES ($1, $2, $3, $4, $5, $6) ` -func (q *Queries) CreateEquivalencyTableRow(ctx context.Context, dlID uuid.UUID, tr EquivalencyTableRow) error { +func (q *Queries) CreateEquivalencyTableRow(ctx context.Context, dataloggerID, dataloggerTableID uuid.UUID, tr EquivalencyTableRow) error { if _, err := q.db.ExecContext(ctx, createEquivalencyTableRow, - dlID, + dataloggerID, + dataloggerTableID, tr.FieldName, tr.DisplayName, tr.InstrumentID, tr.TimeseriesID, ); err != nil { - var pgErr *pgconn.PgError - if errors.As(err, &pgErr) && pgErr.Code == pgerrcode.UniqueViolation { - return fmt.Errorf("timeseries_id %s is already mapped to an active datalogger", tr.TimeseriesID) - } return err } return nil @@ -97,17 +103,15 @@ func (q *Queries) CreateEquivalencyTableRow(ctx context.Context, dlID uuid.UUID, const updateEquivalencyTableRow = ` UPDATE datalogger_equivalency_table SET - field_name = $3, - display_name = $4, - instrument_id = $5, - timeseries_id = $6 - WHERE datalogger_id = $1 - AND id = $2 + field_name = $2, + display_name = $3, + instrument_id = $4, + timeseries_id = $5 + WHERE id = $1 ` -func (q *Queries) UpdateEquivalencyTableRow(ctx context.Context, dataloggerID uuid.UUID, tr EquivalencyTableRow) error { +func (q *Queries) UpdateEquivalencyTableRow(ctx context.Context, tr EquivalencyTableRow) error { if _, err := q.db.ExecContext(ctx, updateEquivalencyTableRow, - dataloggerID, tr.ID, tr.FieldName, tr.DisplayName, @@ -124,22 +128,22 @@ func (q *Queries) UpdateEquivalencyTableRow(ctx context.Context, dataloggerID uu } const deleteEquivalencyTable = ` - DELETE FROM datalogger_equivalency_table WHERE datalogger_id = $1 + DELETE FROM datalogger_equivalency_table WHERE datalogger_table_id = $1 ` -// DeleteEquivalencyTable clears all rows of the EquivalencyTable for a Datalogger -func (q *Queries) DeleteEquivalencyTable(ctx context.Context, dlID uuid.UUID) error { - _, err := q.db.ExecContext(ctx, deleteEquivalencyTable, dlID) +// DeleteEquivalencyTable clears all rows of the EquivalencyTable for a datalogger table +func (q *Queries) DeleteEquivalencyTable(ctx context.Context, dataloggerTableID uuid.UUID) error { + _, err := q.db.ExecContext(ctx, deleteEquivalencyTable, dataloggerTableID) return err } const deleteEquivalencyTableRow = ` - DELETE FROM datalogger_equivalency_table WHERE datalogger_id = $1 AND id = $2 + DELETE FROM datalogger_equivalency_table WHERE id = $1 ` // DeleteEquivalencyTableRow deletes a single EquivalencyTable row by row id -func (q *Queries) DeleteEquivalencyTableRow(ctx context.Context, dlID, rID uuid.UUID) error { - res, err := q.db.ExecContext(ctx, deleteEquivalencyTableRow, dlID, rID) +func (q *Queries) DeleteEquivalencyTableRow(ctx context.Context, rowID uuid.UUID) error { + res, err := q.db.ExecContext(ctx, deleteEquivalencyTableRow, rowID) if err != nil { return err } @@ -148,7 +152,7 @@ func (q *Queries) DeleteEquivalencyTableRow(ctx context.Context, dlID, rID uuid. return err } if count == 0 { - return fmt.Errorf("row %s not found for datalogger %s", rID, dlID) + return fmt.Errorf("row not found %s", rowID) } return nil } diff --git a/api/internal/server/api.go b/api/internal/server/api.go index 0030efe1..224a35be 100644 --- a/api/internal/server/api.go +++ b/api/internal/server/api.go @@ -102,7 +102,8 @@ func (r *ApiServer) RegisterRoutes(h *handler.ApiHandler) { r.private.PUT("/datalogger/:datalogger_id", h.UpdateDatalogger) r.private.PUT("/datalogger/:datalogger_id/key", h.CycleDataloggerKey) r.private.DELETE("/datalogger/:datalogger_id", h.DeleteDatalogger) - r.private.GET("/datalogger/:datalogger_id/preview", h.GetDataloggerPreview) + r.private.GET("/datalogger/:datalogger_id/tables/:datalogger_table_id/preview", h.GetDataloggerTablePreview) + r.private.PUT("/datalogger/:datalogger_id/tables/:datalogger_table_id/name", h.ResetDataloggerTableName) // DistrictRollup r.public.GET("/projects/:project_id/district_rollup/evaluation_submittals", h.ListProjectEvaluationDistrictRollup) @@ -112,11 +113,12 @@ func (r *ApiServer) RegisterRoutes(h *handler.ApiHandler) { r.public.GET("/domains", h.GetDomains) // EquivalencyTable - r.private.GET("/datalogger/:datalogger_id/equivalency_table", h.GetEquivalencyTable) + r.private.GET("/datalogger/:datalogger_id/tables/:datalogger_table_id/equivalency_table", h.GetEquivalencyTable) r.private.POST("/datalogger/:datalogger_id/equivalency_table", h.CreateEquivalencyTable) - r.private.PUT("/datalogger/:datalogger_id/equivalency_table", h.UpdateEquivalencyTable) - r.private.DELETE("/datalogger/:datalogger_id/equivalency_table", h.DeleteEquivalencyTable) - r.private.DELETE("/datalogger/:datalogger_id/equivalency_table/row", h.DeleteEquivalencyTableRow) + r.private.POST("/datalogger/:datalogger_id/tables/:datalogger_table_id/equivalency_table", h.CreateEquivalencyTable) + r.private.PUT("/datalogger/:datalogger_id/tables/:datalogger_table_id/equivalency_table", h.UpdateEquivalencyTable) + r.private.DELETE("/datalogger/:datalogger_id/tables/:datalogger_table_id/equivalency_table", h.DeleteEquivalencyTable) + r.private.DELETE("/datalogger/:datalogger_id/tables/:datalogger_table_id/equivalency_table/row/:row_id", h.DeleteEquivalencyTableRow) // Evaluation r.public.GET("/projects/:project_id/evaluations", h.ListProjectEvaluations) diff --git a/api/internal/service/datalogger.go b/api/internal/service/datalogger.go index b95ca723..2d1ade9a 100644 --- a/api/internal/service/datalogger.go +++ b/api/internal/service/datalogger.go @@ -19,7 +19,10 @@ type DataloggerService interface { GetOneDatalogger(ctx context.Context, dataloggerID uuid.UUID) (model.Datalogger, error) UpdateDatalogger(ctx context.Context, u model.Datalogger) (model.Datalogger, error) DeleteDatalogger(ctx context.Context, d model.Datalogger) error - GetDataloggerPreview(ctx context.Context, dlID uuid.UUID) (model.DataloggerPreview, error) + GetDataloggerTablePreview(ctx context.Context, dataloggerTableID uuid.UUID) (model.DataloggerPreview, error) + ResetDataloggerTableName(ctx context.Context, dataloggerTableID uuid.UUID) error + GetOrCreateDataloggerTable(ctx context.Context, dataloggerID uuid.UUID, tableName string) (uuid.UUID, error) + DeleteDataloggerTable(ctx context.Context, dataloggerTableID uuid.UUID) error } type dataloggerService struct { @@ -51,10 +54,6 @@ func (s dataloggerService) CreateDatalogger(ctx context.Context, n model.Datalog return a, err } - if err := qtx.CreateDataloggerPreview(ctx, dataloggerID); err != nil { - return a, err - } - dl, err := qtx.GetOneDatalogger(ctx, dataloggerID) if err != nil { return a, err @@ -133,3 +132,28 @@ func (s dataloggerService) UpdateDatalogger(ctx context.Context, u model.Datalog return dlUpdated, nil } + +func (s dataloggerTelemetryService) GetOrCreateDataloggerTable(ctx context.Context, dataloggerID uuid.UUID, tableName string) (uuid.UUID, error) { + tx, err := s.db.BeginTxx(ctx, nil) + if err != nil { + return uuid.Nil, err + } + defer model.TxDo(tx.Rollback) + + qtx := s.WithTx(tx) + + if err := qtx.RenameEmptyDataloggerTableName(ctx, dataloggerID, tableName); err != nil { + return uuid.Nil, err + } + + dataloggerTableID, err := qtx.GetOrCreateDataloggerTable(ctx, dataloggerID, tableName) + if err != nil { + return uuid.Nil, err + } + + if err := tx.Commit(); err != nil { + return uuid.Nil, err + } + + return dataloggerTableID, nil +} diff --git a/api/internal/service/datalogger_telemetry.go b/api/internal/service/datalogger_telemetry.go index 971db4e1..9a9311c8 100644 --- a/api/internal/service/datalogger_telemetry.go +++ b/api/internal/service/datalogger_telemetry.go @@ -4,13 +4,14 @@ import ( "context" "github.com/USACE/instrumentation-api/api/internal/model" + "github.com/google/uuid" ) type DataloggerTelemetryService interface { GetDataloggerByModelSN(ctx context.Context, modelName, sn string) (model.Datalogger, error) GetDataloggerHashByModelSN(ctx context.Context, modelName, sn string) (string, error) - UpdateDataloggerPreview(ctx context.Context, dlp model.DataloggerPreview) error - UpdateDataloggerError(ctx context.Context, e *model.DataloggerError) error + UpdateDataloggerTablePreview(ctx context.Context, dataloggerID uuid.UUID, tableName string, dlp model.DataloggerPreview) error + UpdateDataloggerTableError(ctx context.Context, dataloggerID uuid.UUID, tableName *string, e *model.DataloggerError) error } type dataloggerTelemetryService struct { @@ -22,7 +23,7 @@ func NewDataloggerTelemetryService(db *model.Database, q *model.Queries) *datalo return &dataloggerTelemetryService{db, q} } -func (s dataloggerTelemetryService) UpdateDataloggerError(ctx context.Context, e *model.DataloggerError) error { +func (s dataloggerTelemetryService) UpdateDataloggerTableError(ctx context.Context, dataloggerID uuid.UUID, tableName *string, e *model.DataloggerError) error { tx, err := s.db.BeginTxx(ctx, nil) if err != nil { return err @@ -31,12 +32,12 @@ func (s dataloggerTelemetryService) UpdateDataloggerError(ctx context.Context, e qtx := s.WithTx(tx) - if err := qtx.DeleteDataloggerError(ctx, e.DataloggerID); err != nil { + if err := qtx.DeleteDataloggerTableError(ctx, dataloggerID, tableName); err != nil { return err } for _, m := range e.Errors { - if err := qtx.CreateDataloggerError(ctx, e.DataloggerID, m); err != nil { + if err := qtx.CreateDataloggerTableError(ctx, dataloggerID, tableName, m); err != nil { return err } } diff --git a/api/internal/service/equivalency_table.go b/api/internal/service/equivalency_table.go index 30040f3f..fed9f36c 100644 --- a/api/internal/service/equivalency_table.go +++ b/api/internal/service/equivalency_table.go @@ -8,11 +8,11 @@ import ( ) type EquivalencyTableService interface { - GetEquivalencyTable(ctx context.Context, dlID uuid.UUID) (model.EquivalencyTable, error) + GetEquivalencyTable(ctx context.Context, dataloggerTableID uuid.UUID) (model.EquivalencyTable, error) CreateEquivalencyTable(ctx context.Context, t model.EquivalencyTable) error - UpdateEquivalencyTable(ctx context.Context, dataloggerID uuid.UUID, t model.EquivalencyTable) (model.EquivalencyTable, error) - DeleteEquivalencyTable(ctx context.Context, dataloggerID uuid.UUID) error - DeleteEquivalencyTableRow(ctx context.Context, dataloggerID, rowID uuid.UUID) error + UpdateEquivalencyTable(ctx context.Context, t model.EquivalencyTable) (model.EquivalencyTable, error) + DeleteEquivalencyTable(ctx context.Context, dataloggerTableID uuid.UUID) error + DeleteEquivalencyTableRow(ctx context.Context, rowID uuid.UUID) error } type equivalencyTableService struct { @@ -35,13 +35,17 @@ func (s equivalencyTableService) CreateEquivalencyTable(ctx context.Context, t m qtx := s.WithTx(tx) + if err := qtx.GetIsValidDataloggerTable(ctx, t.DataloggerTableID); err != nil { + return err + } + for _, r := range t.Rows { if r.TimeseriesID != nil { if err = qtx.GetIsValidEquivalencyTableTimeseries(ctx, *r.TimeseriesID); err != nil { return err } } - if err := qtx.CreateEquivalencyTableRow(ctx, t.DataloggerID, r); err != nil { + if err := qtx.CreateEquivalencyTableRow(ctx, t.DataloggerID, t.DataloggerTableID, r); err != nil { return err } } @@ -49,7 +53,7 @@ func (s equivalencyTableService) CreateEquivalencyTable(ctx context.Context, t m } // UpdateEquivalencyTable updates rows of an EquivalencyTable -func (s equivalencyTableService) UpdateEquivalencyTable(ctx context.Context, dataloggerID uuid.UUID, t model.EquivalencyTable) (model.EquivalencyTable, error) { +func (s equivalencyTableService) UpdateEquivalencyTable(ctx context.Context, t model.EquivalencyTable) (model.EquivalencyTable, error) { tx, err := s.db.BeginTxx(ctx, nil) if err != nil { return model.EquivalencyTable{}, err @@ -64,12 +68,12 @@ func (s equivalencyTableService) UpdateEquivalencyTable(ctx context.Context, dat return model.EquivalencyTable{}, err } } - if err := qtx.UpdateEquivalencyTableRow(ctx, dataloggerID, r); err != nil { + if err := qtx.UpdateEquivalencyTableRow(ctx, r); err != nil { return model.EquivalencyTable{}, err } } - eqt, err := qtx.GetEquivalencyTable(ctx, dataloggerID) + eqt, err := qtx.GetEquivalencyTable(ctx, t.DataloggerTableID) if err := tx.Commit(); err != nil { return model.EquivalencyTable{}, err diff --git a/migrate/common/R__01_roles.sql b/migrate/common/R__01_roles.sql index 1e91e76b..ea9492db 100644 --- a/migrate/common/R__01_roles.sql +++ b/migrate/common/R__01_roles.sql @@ -98,6 +98,7 @@ GRANT SELECT ON aware_parameter, datalogger, datalogger_hash, + datalogger_table, datalogger_preview, datalogger_equivalency_table, datalogger_model, @@ -162,6 +163,7 @@ GRANT INSERT,UPDATE,DELETE ON aware_parameter, datalogger, datalogger_hash, + datalogger_table, datalogger_preview, datalogger_equivalency_table, datalogger_model, diff --git a/migrate/common/R__10_datalogger.sql b/migrate/common/R__10_datalogger.sql index fc3b34c9..113c3590 100644 --- a/migrate/common/R__10_datalogger.sql +++ b/migrate/common/R__10_datalogger.sql @@ -1,4 +1,3 @@ --- v_datalogger CREATE OR REPLACE VIEW v_datalogger AS ( SELECT dl.id AS id, @@ -15,52 +14,60 @@ CREATE OR REPLACE VIEW v_datalogger AS ( m.id AS model_id, m.model AS model, COALESCE(e.errors, '{}'::TEXT[]) AS errors, - dl.deleted AS deleted + COALESCE(t.tables, '[]'::JSON)::TEXT AS tables FROM datalogger dl INNER JOIN profile p1 ON dl.creator = p1.id - INNER JOIN profile p2 ON dl.creator = p2.id + INNER JOIN profile p2 ON dl.updater = p2.id INNER JOIN datalogger_model m ON dl.model_id = m.id LEFT JOIN ( SELECT - datalogger_id, - array_agg(error_message) AS errors - FROM datalogger_error - GROUP BY datalogger_id + de.datalogger_id, + ARRAY_AGG(de.error_message) AS errors + FROM datalogger_error de + INNER JOIN datalogger_table dt ON dt.id = de.datalogger_table_id + WHERE dt.table_name = 'preparse' + GROUP BY de.datalogger_id ) e ON dl.id = e.datalogger_id + LEFT JOIN ( + SELECT + dt.datalogger_id, + JSON_AGG(JSON_BUILD_OBJECT( + 'id', dt.id, + 'table_name', dt.table_name + )) AS tables + FROM datalogger_table dt + GROUP BY dt.datalogger_id + ) t ON dl.id = t.datalogger_id WHERE NOT dl.deleted ); --- v_datalogger_preview CREATE OR REPLACE VIEW v_datalogger_preview AS ( SELECT - p.datalogger_id AS datalogger_id, - p.preview AS preview, - p.update_date AS update_date, - m.model AS model, - dl.sn AS sn + p.datalogger_table_id, + p.preview, + p.update_date FROM datalogger_preview p - INNER JOIN datalogger dl ON p.datalogger_id = dl.id - INNER JOIN datalogger_model m ON dl.model_id = m.id + INNER JOIN datalogger_table dt ON dt.id = p.datalogger_table_id + INNER JOIN datalogger dl ON dl.id = dt.datalogger_id WHERE NOT dl.deleted ); --- v_datalogger_equivalency_table CREATE OR REPLACE VIEW v_datalogger_equivalency_table AS ( SELECT - t.datalogger_id AS datalogger_id, - t.field_name AS field_name, - t.display_name AS display_name, - t.instrument_id AS instrument_id, - t.timeseries_id AS timeseries_id, - m.model AS model, - dl.sn AS sn - FROM datalogger_equivalency_table t - INNER JOIN datalogger dl ON t.datalogger_id = dl.id - INNER JOIN datalogger_model m ON dl.model_id = m.id - WHERE NOT t.datalogger_deleted + dt.datalogger_id AS datalogger_id, + dt.id AS datalogger_table_id, + COALESCE(JSON_AGG(ROW_TO_JSON(eq)) FILTER (WHERE eq IS NOT NULL), '[]'::JSON)::TEXT AS fields + FROM datalogger_table dt + INNER JOIN datalogger dl ON dt.datalogger_id = dl.id + LEFT JOIN LATERAL ( + SELECT id, field_name, display_name, instrument_id, timeseries_id + FROM datalogger_equivalency_table + WHERE datalogger_table_id = dt.id + ) eq ON true + WHERE NOT dl.deleted + GROUP BY dt.datalogger_id, dt.id ); --- v_datalogger_hash CREATE OR REPLACE VIEW v_datalogger_hash AS ( SELECT dh.datalogger_id AS datalogger_id, diff --git a/migrate/common/V1.5.0__multiple_equivalency_tables.sql b/migrate/common/V1.5.0__multiple_equivalency_tables.sql new file mode 100644 index 00000000..7e6337d1 --- /dev/null +++ b/migrate/common/V1.5.0__multiple_equivalency_tables.sql @@ -0,0 +1,42 @@ +CREATE TABLE IF NOT EXISTS datalogger_table ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + datalogger_id UUID NOT NULL REFERENCES datalogger (id), + table_name TEXT NOT NULL, + CONSTRAINT datalogger_table_datalogger_id_table_name_key UNIQUE (datalogger_id, table_name) +); + +-- create datalogger table for pre and post parse +INSERT INTO datalogger_table (datalogger_id, table_name) +SELECT id, '' FROM datalogger +UNION ALL +SELECT id, 'preparse' FROM datalogger; + +ALTER TABLE datalogger_preview +ADD COLUMN datalogger_table_id UUID REFERENCES datalogger_table (id) ON DELETE CASCADE; + +UPDATE datalogger_preview dp SET datalogger_table_id = dt.id +FROM (SELECT id, datalogger_id FROM datalogger_table) dt +WHERE dp.datalogger_id = dt.datalogger_id; + +ALTER TABLE datalogger_preview +ALTER COLUMN datalogger_table_id SET NOT NULL, +ADD CONSTRAINT datalogger_preview_datalogger_table_id_key UNIQUE (datalogger_table_id), +DROP COLUMN datalogger_id; + +ALTER TABLE datalogger_equivalency_table +ADD COLUMN datalogger_table_id UUID REFERENCES datalogger_table (id) ON DELETE CASCADE; + +UPDATE datalogger_equivalency_table deq SET datalogger_table_id = dt.id +FROM (SELECT id, datalogger_id FROM datalogger_table WHERE table_name = '') dt +WHERE deq.datalogger_id = dt.datalogger_id; + +ALTER TABLE datalogger_equivalency_table +DROP CONSTRAINT unique_datalogger_field, +ADD CONSTRAINT datalogger_equivalency_table_datalogger_table_id_field_name_key UNIQUE (datalogger_table_id, field_name); + +ALTER TABLE datalogger_error +ADD COLUMN datalogger_table_id UUID REFERENCES datalogger_table (id) ON DELETE CASCADE; + +UPDATE datalogger_error de SET datalogger_table_id = dt.id +FROM (SELECT id, datalogger_id FROM datalogger_table) dt +WHERE de.datalogger_id = dt.datalogger_id; diff --git a/migrate/local/V999.1.0__seed_data.sql b/migrate/local/V999.1.0__seed_data.sql index cb57c103..c6a71e9a 100644 --- a/migrate/local/V999.1.0__seed_data.sql +++ b/migrate/local/V999.1.0__seed_data.sql @@ -93,7 +93,8 @@ INSERT INTO timeseries (id, instrument_id, parameter_id, unit_id, slug, name) VA ('479d90eb-3454-4f39-be9a-bfd23099a552', 'd8c66ef9-06f0-4d52-9233-f3778e0624f0', '3ea5ed77-c926-4696-a580-a3fde0f9a556', 'ae06a7db-1e18-4994-be41-9d5a408d6cad', 'inclinometer-constant', 'inclinometer-constant'), ('5b6f4f37-7755-4cf9-bd02-94f1e9bc5984', 'a7540f69-c41e-43b3-b655-6e44097edb7e', '2b7f96e1-820f-4f61-ba8f-861640af6232', '4a999277-4cf5-4282-93ce-23b33c65e2c8', 'demo-piezometer-1.formula', 'demo-piezometer-1'), ('5b6f4f37-7755-4cf9-bd02-94f1e9bc5985', '9e8f2ca4-4037-45a4-aaca-d9e598877439', '2b7f96e1-820f-4f61-ba8f-861640af6232', '4a999277-4cf5-4282-93ce-23b33c65e2c8', 'demo-staffgage-1.formula', 'demo-staffgage-1'), -('5b6f4f37-7755-4cf9-bd02-94f1e9bc5986', 'd8c66ef9-06f0-4d52-9233-f3778e0624f0', '068b59b0-aafb-4c98-ae4b-ed0365a6fbac', '4a999277-4cf5-4282-93ce-23b33c65e2c8', 'inclinometer-1.formula', 'inclinometer-1'); +('5b6f4f37-7755-4cf9-bd02-94f1e9bc5986', 'd8c66ef9-06f0-4d52-9233-f3778e0624f0', '068b59b0-aafb-4c98-ae4b-ed0365a6fbac', '4a999277-4cf5-4282-93ce-23b33c65e2c8', 'inclinometer-1.formula', 'inclinometer-1'), +('844fb688-e77c-481e-bff9-81a0fff9f3f2', 'd8c66ef9-06f0-4d52-9233-f3778e0624f0', '068b59b0-aafb-4c98-ae4b-ed0365a6fbac', '4a999277-4cf5-4282-93ce-23b33c65e2c8', 'test-create-datalogger-table-mapping', 'test-create-datalogger-table-mapping'); INSERT INTO calculation (timeseries_id, contents) VALUES ('5b6f4f37-7755-4cf9-bd02-94f1e9bc5984', '[demo-piezometer-1.top-of-riser] - [demo-piezometer-1.distance-to-water]'), diff --git a/migrate/local/V999.4.0__seed_datalogger.sql b/migrate/local/V999.4.0__seed_datalogger.sql index e759a366..0b1f691b 100644 --- a/migrate/local/V999.4.0__seed_datalogger.sql +++ b/migrate/local/V999.4.0__seed_datalogger.sql @@ -7,10 +7,14 @@ INSERT INTO datalogger_hash (datalogger_id, "hash") VALUES ('83a7345c-62d8-4e29-84db-c2e36f8bc40d', '$argon2id$v=19$m=65536,t=3,p=2$Ud3epc4MAsAAN6pCUQxCjh$5mXpCpcc3nc46XUC7pJiAKvK9UzK1qfP6GoYDFH66zv4pjQxt88ybQJ'), ('c0b65315-f802-4ca5-a4dd-7e0cfcffd057', '$argon2id$v=19$m=65536,t=3,p=2$Ud3epc4MAsAAN6pCUQxCjh$5mXpCpcc3nc46XUC7pJiAKvK9UzK1qfP6GoYDFH66zv4pjQxt88ybQJ'); -INSERT INTO datalogger_preview (datalogger_id, update_date, preview) VALUES - ('83a7345c-62d8-4e29-84db-c2e36f8bc40d', '2023-02-16 18:53:00.582812+00', '{"data":[{"no":158,"time":"2023-02-16T18:53:00","vals":[12.13,22.37]},{"no":158,"time":"2023-02-16T18:52:55","vals":[12.25,20.32]}],"head":{"environment":{"model":"CR6","os_version":"CR6.Std.12.01","prog_name":"CPU:Updated_CR6_Sample_Template.CR6","serial_no":"11111","station_name":"6239","table_name":"Test"},"fields":[{"name":"batt_volt_Min","process":"Min","settable":false,"type":"xsd:float","units":"Volts"},{"name":"PanelT","process":"Smp","settable":false,"type":"xsd:float","units":"Deg_C"}],"signature":20883,"transaction":0}}'), - ('c0b65315-f802-4ca5-a4dd-7e0cfcffd057', '2023-02-16 18:53:00.582812+00', NULL); +INSERT INTO datalogger_table (id, datalogger_id, table_name) VALUES + ('98a77c65-e5c4-49ed-8fb4-b0ffd06add4c', '83a7345c-62d8-4e29-84db-c2e36f8bc40d', 'Demo Datalogger Table'), + ('5b47be95-6ba9-488c-bdda-be7bdbca2909', 'c0b65315-f802-4ca5-a4dd-7e0cfcffd057', ''); -INSERT INTO datalogger_equivalency_table (id, datalogger_id, datalogger_deleted, field_name, display_name, instrument_id, timeseries_id) VALUES - ('40ceff10-cdc3-4715-a4ca-c1e570fe25de', '83a7345c-62d8-4e29-84db-c2e36f8bc40d', false, 'batt_volt_Min', 'Battery Voltage', '9e8f2ca4-4037-45a4-aaca-d9e598877439', '8f4ca3a3-5971-4597-bd6f-332d1cf5af7c'), - ('2f1f7c3d-8b6f-4b11-917e-8f049eb6c62b', '83a7345c-62d8-4e29-84db-c2e36f8bc40d', false, 'PanelT', 'Panel Temperature', 'a7540f69-c41e-43b3-b655-6e44097edb7e', 'd9697351-3a38-4194-9ac4-41541927e475'); +INSERT INTO datalogger_preview (datalogger_table_id, update_date, preview) VALUES + ('98a77c65-e5c4-49ed-8fb4-b0ffd06add4c', '2023-02-16 18:53:00.582812+00', '{"data":[{"no":158,"time":"2023-02-16T18:53:00","vals":[12.13,22.37]},{"no":158,"time":"2023-02-16T18:52:55","vals":[12.25,20.32]}],"head":{"environment":{"model":"CR6","os_version":"CR6.Std.12.01","prog_name":"CPU:Updated_CR6_Sample_Template.CR6","serial_no":"11111","station_name":"6239","table_name":"Test"},"fields":[{"name":"batt_volt_Min","process":"Min","settable":false,"type":"xsd:float","units":"Volts"},{"name":"PanelT","process":"Smp","settable":false,"type":"xsd:float","units":"Deg_C"}],"signature":20883,"transaction":0}}'), + ('5b47be95-6ba9-488c-bdda-be7bdbca2909', '2023-02-16 18:53:00.582812+00', NULL); + +INSERT INTO datalogger_equivalency_table (id, datalogger_table_id, datalogger_id, datalogger_deleted, field_name, display_name, instrument_id, timeseries_id) VALUES + ('40ceff10-cdc3-4715-a4ca-c1e570fe25de', '98a77c65-e5c4-49ed-8fb4-b0ffd06add4c', '83a7345c-62d8-4e29-84db-c2e36f8bc40d', false, 'batt_volt_Min', 'Battery Voltage', '9e8f2ca4-4037-45a4-aaca-d9e598877439', '8f4ca3a3-5971-4597-bd6f-332d1cf5af7c'), + ('2f1f7c3d-8b6f-4b11-917e-8f049eb6c62b', '98a77c65-e5c4-49ed-8fb4-b0ffd06add4c', '83a7345c-62d8-4e29-84db-c2e36f8bc40d', false, 'PanelT', 'Panel Temperature', 'a7540f69-c41e-43b3-b655-6e44097edb7e', 'd9697351-3a38-4194-9ac4-41541927e475'); diff --git a/mock/telemetry/Dockerfile b/mock/telemetry/Dockerfile index 99ea7ec6..8c2feb95 100644 --- a/mock/telemetry/Dockerfile +++ b/mock/telemetry/Dockerfile @@ -5,4 +5,4 @@ ENV PYTHONUNBUFFERED=1 COPY ./telemetry_post_tester.py /app/telemetry_post_tester.py WORKDIR /app -ENTRYPOINT [ "./telemetry_post_tester.py", "--use-mock-api-key" ] +ENTRYPOINT [ "./telemetry_post_tester.py", "--use-mock-api-key", "--multi-table" ] diff --git a/mock/telemetry/telemetry_post_tester.py b/mock/telemetry/telemetry_post_tester.py index b0ead33b..b8fbd0ba 100755 --- a/mock/telemetry/telemetry_post_tester.py +++ b/mock/telemetry/telemetry_post_tester.py @@ -20,6 +20,7 @@ DEFAULT_INTERVAL_SECONDS = 10 DEFAULT_MODEL = "CR6" DEFAULT_SN = "12345" +DEFAULT_TABLE_NAME = "Demo Datalogger Table" DEFAULT_VERBOSE = False @@ -48,7 +49,7 @@ def post_data(url: str, data: dict, api_key: str) -> Any | None: return None # exception was thrown -def create_test_data(interval: int, idx: int, model: str, sn: str) -> dict: +def create_test_data(interval: int, idx: int, model: str, sn: str, table_name: str) -> dict: measurement_time_1 = datetime.now().isoformat(timespec="seconds") measurement_time_2 = (datetime.now() - timedelta(seconds=(interval / 2))).isoformat( timespec="seconds" @@ -60,7 +61,7 @@ def create_test_data(interval: int, idx: int, model: str, sn: str) -> dict: "signature": 20883, "environment": { "station_name": "6239", - "table_name": "Test", + "table_name": table_name, "model": model, "serial_no": sn, "os_version": f"{model}.Std.12.01", @@ -139,6 +140,12 @@ def main() -> None: default=DEFAULT_SN, type=str, ) + parser.add_argument( + "--table", + help="table name for payload", + default=DEFAULT_TABLE_NAME, + type=str, + ) parser.add_argument( "--use-mock-api-key", help="[optional] use default mock api key for local testing", @@ -146,6 +153,13 @@ def main() -> None: type=bool, action=argparse.BooleanOptionalAction, ) + parser.add_argument( + "--multi-table", + help="[optional] mock an additional payload sent to another specified table", + default=DEFAULT_USE_MOCK_API_KEY, + type=bool, + action=argparse.BooleanOptionalAction, + ) parser.add_argument( "--verbose", help="[optional] show outgoing the request's mocked payloads", @@ -164,15 +178,29 @@ def main() -> None: i = 0 while True: - data = create_test_data(args.interval, i, args.model, args.sn) - i += 1 + data = create_test_data(args.interval, i, args.model, args.sn, args.table) print(f"POST: {url}") + if args.verbose: print("request payload:", json.dumps(data, indent=2), sep="\n") + post_data(url, data, api_key) + print(f"waiting {args.interval} seconds...\n") time.sleep(args.interval) + if args.multi_table: + data = create_test_data(args.interval, i, args.model, args.sn, "Demo Multi-Table") + print(f"POST: {url}") + + if args.verbose: + print("request payload:", json.dumps(data, indent=2), sep="\n") + + post_data(url, data, api_key) + print(f"waiting {args.interval} seconds...\n") + time.sleep(args.interval) + + i += 1 if __name__ == "__main__": main()