diff --git a/.sqlx/query-08463f596e5d3deac8d5e8a29847fd5690ca76d47a2fccd9df3bd79aa4e9e1ab.json b/.sqlx/query-08463f596e5d3deac8d5e8a29847fd5690ca76d47a2fccd9df3bd79aa4e9e1ab.json new file mode 100644 index 0000000..0b17bb4 --- /dev/null +++ b/.sqlx/query-08463f596e5d3deac8d5e8a29847fd5690ca76d47a2fccd9df3bd79aa4e9e1ab.json @@ -0,0 +1,54 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO ven (\n id,\n created_date_time,\n modification_date_time,\n ven_name,\n attributes,\n targets\n )\n VALUES (gen_random_uuid(), now(), now(), $1, $2, $3)\n RETURNING\n id,\n created_date_time,\n modification_date_time,\n ven_name,\n attributes,\n targets\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "created_date_time", + "type_info": "Timestamptz" + }, + { + "ordinal": 2, + "name": "modification_date_time", + "type_info": "Timestamptz" + }, + { + "ordinal": 3, + "name": "ven_name", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "attributes", + "type_info": "Jsonb" + }, + { + "ordinal": 5, + "name": "targets", + "type_info": "Jsonb" + } + ], + "parameters": { + "Left": [ + "Text", + "Jsonb", + "Jsonb" + ] + }, + "nullable": [ + false, + false, + false, + false, + true, + true + ] + }, + "hash": "08463f596e5d3deac8d5e8a29847fd5690ca76d47a2fccd9df3bd79aa4e9e1ab" +} diff --git a/.sqlx/query-17e4c5c2d187ec2be52b6d30c2ce617eba367c9380148582350d4a488c779f09.json b/.sqlx/query-17e4c5c2d187ec2be52b6d30c2ce617eba367c9380148582350d4a488c779f09.json new file mode 100644 index 0000000..dec6693 --- /dev/null +++ b/.sqlx/query-17e4c5c2d187ec2be52b6d30c2ce617eba367c9380148582350d4a488c779f09.json @@ -0,0 +1,61 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n r.id AS \"id!\", \n r.created_date_time AS \"created_date_time!\", \n r.modification_date_time AS \"modification_date_time!\",\n r.resource_name AS \"resource_name!\",\n r.ven_id AS \"ven_id!\",\n r.attributes,\n r.targets\n FROM resource r\n WHERE ($1::text[] IS NULL OR r.resource_name = ANY($1))\n AND ($2::jsonb = '[]'::jsonb OR $2::jsonb <@ r.targets)\n OFFSET $3 LIMIT $4\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id!", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "created_date_time!", + "type_info": "Timestamptz" + }, + { + "ordinal": 2, + "name": "modification_date_time!", + "type_info": "Timestamptz" + }, + { + "ordinal": 3, + "name": "resource_name!", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "ven_id!", + "type_info": "Text" + }, + { + "ordinal": 5, + "name": "attributes", + "type_info": "Jsonb" + }, + { + "ordinal": 6, + "name": "targets", + "type_info": "Jsonb" + } + ], + "parameters": { + "Left": [ + "TextArray", + "Jsonb", + "Int8", + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + true, + true + ] + }, + "hash": "17e4c5c2d187ec2be52b6d30c2ce617eba367c9380148582350d4a488c779f09" +} diff --git a/.sqlx/query-4692bd4fe201d23dd6c58c5d9fad0be1299c5fd7fea66db0fc027699a281c4be.json b/.sqlx/query-4692bd4fe201d23dd6c58c5d9fad0be1299c5fd7fea66db0fc027699a281c4be.json new file mode 100644 index 0000000..7a60412 --- /dev/null +++ b/.sqlx/query-4692bd4fe201d23dd6c58c5d9fad0be1299c5fd7fea66db0fc027699a281c4be.json @@ -0,0 +1,58 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n id,\n created_date_time,\n modification_date_time,\n resource_name,\n ven_id,\n attributes,\n targets\n FROM resource\n WHERE id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "created_date_time", + "type_info": "Timestamptz" + }, + { + "ordinal": 2, + "name": "modification_date_time", + "type_info": "Timestamptz" + }, + { + "ordinal": 3, + "name": "resource_name", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "ven_id", + "type_info": "Text" + }, + { + "ordinal": 5, + "name": "attributes", + "type_info": "Jsonb" + }, + { + "ordinal": 6, + "name": "targets", + "type_info": "Jsonb" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + true, + true + ] + }, + "hash": "4692bd4fe201d23dd6c58c5d9fad0be1299c5fd7fea66db0fc027699a281c4be" +} diff --git a/.sqlx/query-4feeb8a9353ec05413b798b7ba9ece137afea8c0f867618e9faeabdb0c5ddb55.json b/.sqlx/query-4feeb8a9353ec05413b798b7ba9ece137afea8c0f867618e9faeabdb0c5ddb55.json new file mode 100644 index 0000000..72be4a9 --- /dev/null +++ b/.sqlx/query-4feeb8a9353ec05413b798b7ba9ece137afea8c0f867618e9faeabdb0c5ddb55.json @@ -0,0 +1,62 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE resource\n SET modification_date_time = now(),\n resource_name = $2,\n ven_id = $3,\n attributes = $4,\n targets = $5\n WHERE id = $1\n RETURNING *\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "created_date_time", + "type_info": "Timestamptz" + }, + { + "ordinal": 2, + "name": "modification_date_time", + "type_info": "Timestamptz" + }, + { + "ordinal": 3, + "name": "resource_name", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "ven_id", + "type_info": "Text" + }, + { + "ordinal": 5, + "name": "attributes", + "type_info": "Jsonb" + }, + { + "ordinal": 6, + "name": "targets", + "type_info": "Jsonb" + } + ], + "parameters": { + "Left": [ + "Text", + "Text", + "Text", + "Jsonb", + "Jsonb" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + true, + true + ] + }, + "hash": "4feeb8a9353ec05413b798b7ba9ece137afea8c0f867618e9faeabdb0c5ddb55" +} diff --git a/.sqlx/query-5e7605ed200eb221be758f0b5d82d6dbfe984f88295d64a207d4a1be83730d2f.json b/.sqlx/query-5e7605ed200eb221be758f0b5d82d6dbfe984f88295d64a207d4a1be83730d2f.json new file mode 100644 index 0000000..0e4b500 --- /dev/null +++ b/.sqlx/query-5e7605ed200eb221be758f0b5d82d6dbfe984f88295d64a207d4a1be83730d2f.json @@ -0,0 +1,52 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n id,\n created_date_time,\n modification_date_time,\n ven_name,\n attributes,\n targets\n FROM ven\n WHERE id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "created_date_time", + "type_info": "Timestamptz" + }, + { + "ordinal": 2, + "name": "modification_date_time", + "type_info": "Timestamptz" + }, + { + "ordinal": 3, + "name": "ven_name", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "attributes", + "type_info": "Jsonb" + }, + { + "ordinal": 5, + "name": "targets", + "type_info": "Jsonb" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + false, + false, + false, + true, + true + ] + }, + "hash": "5e7605ed200eb221be758f0b5d82d6dbfe984f88295d64a207d4a1be83730d2f" +} diff --git a/.sqlx/query-8de2bc6635a3ca105865baa079ad1ec02d2651cc44f1538d874434f51b32ac33.json b/.sqlx/query-8de2bc6635a3ca105865baa079ad1ec02d2651cc44f1538d874434f51b32ac33.json new file mode 100644 index 0000000..4b4ab67 --- /dev/null +++ b/.sqlx/query-8de2bc6635a3ca105865baa079ad1ec02d2651cc44f1538d874434f51b32ac33.json @@ -0,0 +1,58 @@ +{ + "db_name": "PostgreSQL", + "query": "\n DELETE FROM resource r\n WHERE r.id = $1\n RETURNING r.*\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "created_date_time", + "type_info": "Timestamptz" + }, + { + "ordinal": 2, + "name": "modification_date_time", + "type_info": "Timestamptz" + }, + { + "ordinal": 3, + "name": "resource_name", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "ven_id", + "type_info": "Text" + }, + { + "ordinal": 5, + "name": "attributes", + "type_info": "Jsonb" + }, + { + "ordinal": 6, + "name": "targets", + "type_info": "Jsonb" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + true, + true + ] + }, + "hash": "8de2bc6635a3ca105865baa079ad1ec02d2651cc44f1538d874434f51b32ac33" +} diff --git a/.sqlx/query-8e35cf8b3bfe12fe70a98180b86867b8854cfafc1176c411733d65f0bb98c8f9.json b/.sqlx/query-8e35cf8b3bfe12fe70a98180b86867b8854cfafc1176c411733d65f0bb98c8f9.json new file mode 100644 index 0000000..11d4062 --- /dev/null +++ b/.sqlx/query-8e35cf8b3bfe12fe70a98180b86867b8854cfafc1176c411733d65f0bb98c8f9.json @@ -0,0 +1,56 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n v.id AS \"id!\", \n v.created_date_time AS \"created_date_time!\", \n v.modification_date_time AS \"modification_date_time!\",\n v.ven_name AS \"ven_name!\",\n v.attributes,\n v.targets\n FROM ven v\n LEFT JOIN resource r ON r.ven_id = v.id\n WHERE ($1::text[] IS NULL OR v.ven_name = ANY($1))\n AND ($2::text[] IS NULL OR r.resource_name = ANY($2))\n AND ($3::jsonb = '[]'::jsonb OR $3::jsonb <@ v.targets)\n OFFSET $4 LIMIT $5\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id!", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "created_date_time!", + "type_info": "Timestamptz" + }, + { + "ordinal": 2, + "name": "modification_date_time!", + "type_info": "Timestamptz" + }, + { + "ordinal": 3, + "name": "ven_name!", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "attributes", + "type_info": "Jsonb" + }, + { + "ordinal": 5, + "name": "targets", + "type_info": "Jsonb" + } + ], + "parameters": { + "Left": [ + "TextArray", + "TextArray", + "Jsonb", + "Int8", + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false, + true, + true + ] + }, + "hash": "8e35cf8b3bfe12fe70a98180b86867b8854cfafc1176c411733d65f0bb98c8f9" +} diff --git a/.sqlx/query-917bf27ebb6ce4a9ff7358b6ad32f334c58cfabc978a763753ad72fa57a4d888.json b/.sqlx/query-917bf27ebb6ce4a9ff7358b6ad32f334c58cfabc978a763753ad72fa57a4d888.json new file mode 100644 index 0000000..8665c5d --- /dev/null +++ b/.sqlx/query-917bf27ebb6ce4a9ff7358b6ad32f334c58cfabc978a763753ad72fa57a4d888.json @@ -0,0 +1,52 @@ +{ + "db_name": "PostgreSQL", + "query": "\n DELETE FROM ven v\n WHERE v.id = $1\n RETURNING\n v.id,\n v.created_date_time,\n v.modification_date_time,\n v.ven_name,\n v.attributes,\n v.targets\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "created_date_time", + "type_info": "Timestamptz" + }, + { + "ordinal": 2, + "name": "modification_date_time", + "type_info": "Timestamptz" + }, + { + "ordinal": 3, + "name": "ven_name", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "attributes", + "type_info": "Jsonb" + }, + { + "ordinal": 5, + "name": "targets", + "type_info": "Jsonb" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + false, + false, + false, + true, + true + ] + }, + "hash": "917bf27ebb6ce4a9ff7358b6ad32f334c58cfabc978a763753ad72fa57a4d888" +} diff --git a/.sqlx/query-b79f09fdb4ddf9909a8bf2851c9f68ddeeb7844bb4b639adede34779435d5e97.json b/.sqlx/query-b79f09fdb4ddf9909a8bf2851c9f68ddeeb7844bb4b639adede34779435d5e97.json new file mode 100644 index 0000000..265766b --- /dev/null +++ b/.sqlx/query-b79f09fdb4ddf9909a8bf2851c9f68ddeeb7844bb4b639adede34779435d5e97.json @@ -0,0 +1,55 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE ven\n SET modification_date_time = now(),\n ven_name = $2,\n attributes = $3,\n targets = $4\n WHERE id = $1\n RETURNING\n id,\n created_date_time,\n modification_date_time,\n ven_name,\n attributes,\n targets\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "created_date_time", + "type_info": "Timestamptz" + }, + { + "ordinal": 2, + "name": "modification_date_time", + "type_info": "Timestamptz" + }, + { + "ordinal": 3, + "name": "ven_name", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "attributes", + "type_info": "Jsonb" + }, + { + "ordinal": 5, + "name": "targets", + "type_info": "Jsonb" + } + ], + "parameters": { + "Left": [ + "Text", + "Text", + "Jsonb", + "Jsonb" + ] + }, + "nullable": [ + false, + false, + false, + false, + true, + true + ] + }, + "hash": "b79f09fdb4ddf9909a8bf2851c9f68ddeeb7844bb4b639adede34779435d5e97" +} diff --git a/.sqlx/query-e0bcb3d6f5cb5820a4c62cff51742671691cbf4d06c6cfec22e6d71cca0f9432.json b/.sqlx/query-e0bcb3d6f5cb5820a4c62cff51742671691cbf4d06c6cfec22e6d71cca0f9432.json new file mode 100644 index 0000000..0087dbd --- /dev/null +++ b/.sqlx/query-e0bcb3d6f5cb5820a4c62cff51742671691cbf4d06c6cfec22e6d71cca0f9432.json @@ -0,0 +1,61 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO resource (\n id,\n created_date_time,\n modification_date_time,\n resource_name,\n ven_id,\n attributes,\n targets\n )\n VALUES (gen_random_uuid(), now(), now(), $1, $2, $3, $4)\n RETURNING *\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "created_date_time", + "type_info": "Timestamptz" + }, + { + "ordinal": 2, + "name": "modification_date_time", + "type_info": "Timestamptz" + }, + { + "ordinal": 3, + "name": "resource_name", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "ven_id", + "type_info": "Text" + }, + { + "ordinal": 5, + "name": "attributes", + "type_info": "Jsonb" + }, + { + "ordinal": 6, + "name": "targets", + "type_info": "Jsonb" + } + ], + "parameters": { + "Left": [ + "Text", + "Text", + "Jsonb", + "Jsonb" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + true, + true + ] + }, + "hash": "e0bcb3d6f5cb5820a4c62cff51742671691cbf4d06c6cfec22e6d71cca0f9432" +} diff --git a/fixtures/resources.sql b/fixtures/resources.sql new file mode 100644 index 0000000..e160016 --- /dev/null +++ b/fixtures/resources.sql @@ -0,0 +1,42 @@ +INSERT INTO resources (id, + created_date_time, + modification_date_time, + resource_name + ven_name, + attributes, + targets) +VALUES ('resource-1', + '2024-07-25 08:31:10.776000 +00:00', + '2024-07-25 08:31:10.776000 +00:00', + 'resource-1-name', + 'ven-1', + NULL, + NULL), + ('resource-2', + '2024-07-25 08:31:10.776000 +00:00', + '2024-07-25 08:31:10.776000 +00:00', + 'resource-2-name', + 'ven-2', + NULL, + NULL), + ('resource-3', + '2024-07-25 08:31:10.776000 +00:00', + '2024-07-25 08:31:10.776000 +00:00', + 'resource-3-name', + 'ven-1', + NULL, + NULL), + ('resource-4', + '2024-07-25 08:31:10.776000 +00:00', + '2024-07-25 08:31:10.776000 +00:00', + 'resource-4-name', + 'ven-2', + NULL, + NULL), + ('resource-5', + '2024-07-25 08:31:10.776000 +00:00', + '2024-07-25 08:31:10.776000 +00:00', + 'resource-5-name', + 'ven-2', + NULL, + NULL); diff --git a/openadr-vtn/src/api/mod.rs b/openadr-vtn/src/api/mod.rs index 335a4d5..2f44142 100644 --- a/openadr-vtn/src/api/mod.rs +++ b/openadr-vtn/src/api/mod.rs @@ -10,6 +10,7 @@ pub mod auth; pub mod event; pub mod program; pub mod report; +pub mod resource; pub mod ven; pub type AppResponse = Result, AppError>; diff --git a/openadr-vtn/src/api/resource.rs b/openadr-vtn/src/api/resource.rs new file mode 100644 index 0000000..b3377f2 --- /dev/null +++ b/openadr-vtn/src/api/resource.rs @@ -0,0 +1,97 @@ +use std::sync::Arc; + +use axum::extract::{Path, State}; +use axum::Json; +use reqwest::StatusCode; +use serde::Deserialize; +use tracing::{info, trace}; +use validator::{Validate, ValidationError}; + +use openadr_wire::resource::{Resource, ResourceContent, ResourceId}; +use openadr_wire::target::TargetLabel; + +use crate::api::{AppResponse, ValidatedJson, ValidatedQuery}; +use crate::data_source::ResourceCrud; +use crate::error::AppError; +use crate::jwt::{User, VenManagerUser}; + +pub async fn get_all( + State(resource_source): State>, + ValidatedQuery(query_params): ValidatedQuery, + VenManagerUser(user): VenManagerUser, +) -> AppResponse> { + trace!(?query_params); + + let resources = resource_source.retrieve_all(&query_params, &user).await?; + + Ok(Json(resources)) +} + +pub async fn get( + State(resource_source): State>, + Path(id): Path, + User(user): User, +) -> AppResponse { + let ven = resource_source.retrieve(&id, &user).await?; + + Ok(Json(ven)) +} + +pub async fn add( + State(resource_source): State>, + VenManagerUser(user): VenManagerUser, + ValidatedJson(new_resource): ValidatedJson, +) -> Result<(StatusCode, Json), AppError> { + let ven = resource_source.create(new_resource, &user).await?; + + Ok((StatusCode::CREATED, Json(ven))) +} + +pub async fn edit( + State(resource_source): State>, + Path(id): Path, + VenManagerUser(user): VenManagerUser, + ValidatedJson(content): ValidatedJson, +) -> AppResponse { + let resource = resource_source.update(&id, content, &user).await?; + + info!(%resource.id, resource.resource_name=resource.content.resource_name, "resource updated"); + + Ok(Json(resource)) +} + +pub async fn delete( + State(resource_source): State>, + Path(id): Path, + VenManagerUser(user): VenManagerUser, +) -> AppResponse { + let resource = resource_source.delete(&id, &user).await?; + info!(%id, "deleted resource"); + Ok(Json(resource)) +} + +#[derive(Deserialize, Validate, Debug)] +#[validate(schema(function = "validate_target_type_value_pair"))] +#[serde(rename_all = "camelCase")] +pub struct QueryParams { + pub(crate) target_type: Option, + pub(crate) target_values: Option>, + #[serde(default)] + #[validate(range(min = 0))] + pub(crate) skip: i64, + #[validate(range(min = 1, max = 50))] + #[serde(default = "get_50")] + pub(crate) limit: i64, +} + +fn validate_target_type_value_pair(query: &QueryParams) -> Result<(), ValidationError> { + if query.target_type.is_some() == query.target_values.is_some() { + Ok(()) + } else { + Err(ValidationError::new("targetType and targetValues query parameter must either both be set or not set at the same time.")) + } +} + +fn get_50() -> i64 { + 50 +} diff --git a/openadr-vtn/src/data_source/mod.rs b/openadr-vtn/src/data_source/mod.rs index 8fc3103..c1fac24 100644 --- a/openadr-vtn/src/data_source/mod.rs +++ b/openadr-vtn/src/data_source/mod.rs @@ -6,6 +6,7 @@ use openadr_wire::{ event::{EventContent, EventId}, program::{ProgramContent, ProgramId}, report::{ReportContent, ReportId}, + resource::{Resource, ResourceContent, ResourceId}, ven::{Ven, VenContent, VenId}, Event, Program, Report, }; @@ -100,6 +101,18 @@ pub trait VenCrud: { } +pub trait ResourceCrud: + Crud< + Type = Resource, + Id = ResourceId, + NewType = ResourceContent, + Error = AppError, + Filter = crate::api::resource::QueryParams, + PermissionFilter = Claims, +> +{ +} + #[async_trait] pub trait AuthSource: Send + Sync + 'static { async fn get_user(&self, client_id: &str, client_secret: &str) -> Option; @@ -110,6 +123,7 @@ pub trait DataSource: Send + Sync + 'static { fn reports(&self) -> Arc; fn events(&self) -> Arc; fn vens(&self) -> Arc; + fn resources(&self) -> Arc; fn auth(&self) -> Arc; } diff --git a/openadr-vtn/src/data_source/postgres/mod.rs b/openadr-vtn/src/data_source/postgres/mod.rs index 67a9a12..951c33a 100644 --- a/openadr-vtn/src/data_source/postgres/mod.rs +++ b/openadr-vtn/src/data_source/postgres/mod.rs @@ -3,11 +3,14 @@ use crate::data_source::postgres::program::PgProgramStorage; use crate::data_source::postgres::report::PgReportStorage; use crate::data_source::postgres::user::PgAuthSource; use crate::data_source::postgres::ven::PgVenStorage; -use crate::data_source::{AuthSource, DataSource, EventCrud, ProgramCrud, ReportCrud, VenCrud}; +use crate::data_source::{ + AuthSource, DataSource, EventCrud, ProgramCrud, ReportCrud, ResourceCrud, VenCrud, +}; use crate::error::AppError; use crate::jwt::{BusinessIds, Claims}; use dotenvy::dotenv; use openadr_wire::target::{TargetLabel, TargetMap}; +use resource::PgResourceStorage; use serde::Serialize; use sqlx::PgPool; use std::sync::Arc; @@ -16,6 +19,7 @@ use tracing::{error, info, trace}; mod event; mod program; mod report; +mod resource; mod user; mod ven; @@ -41,6 +45,10 @@ impl DataSource for PostgresStorage { Arc::::new(self.db.clone().into()) } + fn resources(&self) -> Arc { + Arc::::new(self.db.clone().into()) + } + fn auth(&self) -> Arc { Arc::::new(self.db.clone().into()) } diff --git a/openadr-vtn/src/data_source/postgres/resource.rs b/openadr-vtn/src/data_source/postgres/resource.rs new file mode 100644 index 0000000..f885679 --- /dev/null +++ b/openadr-vtn/src/data_source/postgres/resource.rs @@ -0,0 +1,270 @@ +use crate::api::resource::QueryParams; +use crate::data_source::postgres::{to_json_value, PgTargetsFilter}; +use crate::data_source::{Crud, ResourceCrud}; +use crate::error::AppError; +use crate::jwt::Claims; +use axum::async_trait; +use chrono::{DateTime, Utc}; +use openadr_wire::resource::{Resource, ResourceContent, ResourceId}; +use openadr_wire::target::TargetLabel; +use sqlx::PgPool; +use tracing::{error, trace}; + +#[async_trait] +impl ResourceCrud for PgResourceStorage {} + +pub(crate) struct PgResourceStorage { + db: PgPool, +} + +impl From for PgResourceStorage { + fn from(db: PgPool) -> Self { + Self { db } + } +} + +#[derive(Debug)] +struct PostgresResource { + id: String, + created_date_time: DateTime, + modification_date_time: DateTime, + resource_name: String, + ven_id: String, + attributes: Option, + targets: Option, +} + +impl TryFrom for Resource { + type Error = AppError; + + #[tracing::instrument(name = "TryFrom for Resource")] + fn try_from(value: PostgresResource) -> Result { + let attributes = match value.attributes { + None => None, + Some(t) => serde_json::from_value(t) + .inspect_err(|err| { + error!( + ?err, + "Failed to deserialize JSON from DB to `Vec`" + ) + }) + .map_err(AppError::SerdeJsonInternalServerError)?, + }; + let targets = match value.targets { + None => None, + Some(t) => serde_json::from_value(t) + .inspect_err(|err| { + error!(?err, "Failed to deserialize JSON from DB to `TargetMap`") + }) + .map_err(AppError::SerdeJsonInternalServerError)?, + }; + + Ok(Self { + id: value.id.parse()?, + created_date_time: value.created_date_time, + modification_date_time: value.modification_date_time, + content: ResourceContent { + object_type: Default::default(), + resource_name: value.resource_name, + ven_id: Some(value.ven_id), + targets, + attributes, + }, + }) + } +} + +#[derive(Debug, Default)] +struct PostgresFilter<'a> { + resource_names: Option<&'a [String]>, + targets: Vec>, + skip: i64, + limit: i64, +} + +impl<'a> From<&'a QueryParams> for PostgresFilter<'a> { + fn from(query: &'a QueryParams) -> Self { + let mut filter = Self { + skip: query.skip, + limit: query.limit, + ..Default::default() + }; + match query.target_type { + Some(TargetLabel::VENName) => filter.resource_names = query.target_values.as_deref(), + Some(TargetLabel::ResourceName) => { + filter.resource_names = query.target_values.as_deref() + } + Some(ref label) => { + if let Some(values) = query.target_values.as_ref() { + filter.targets = values + .iter() + .map(|value| PgTargetsFilter { + label: label.as_str(), + value: [value.clone()], + }) + .collect() + } + } + None => {} + }; + + filter + } +} + +#[async_trait] +impl Crud for PgResourceStorage { + type Type = Resource; + type Id = ResourceId; + type NewType = ResourceContent; + type Error = AppError; + type Filter = QueryParams; + type PermissionFilter = Claims; + + async fn create( + &self, + new: Self::NewType, + _user: &Self::PermissionFilter, + ) -> Result { + let resource: Resource = sqlx::query_as!( + PostgresResource, + r#" + INSERT INTO resource ( + id, + created_date_time, + modification_date_time, + resource_name, + ven_id, + attributes, + targets + ) + VALUES (gen_random_uuid(), now(), now(), $1, $2, $3, $4) + RETURNING * + "#, + new.resource_name, + new.ven_id, + to_json_value(new.attributes)?, + to_json_value(new.targets)?, + ) + .fetch_one(&self.db) + .await? + .try_into()?; + + Ok(resource) + } + + async fn retrieve( + &self, + id: &Self::Id, + _user: &Self::PermissionFilter, + ) -> Result { + let resource = sqlx::query_as!( + PostgresResource, + r#" + SELECT + id, + created_date_time, + modification_date_time, + resource_name, + ven_id, + attributes, + targets + FROM resource + WHERE id = $1 + "#, + id.as_str(), + ) + .fetch_one(&self.db) + .await? + .try_into()?; + + Ok(resource) + } + + async fn retrieve_all( + &self, + filter: &Self::Filter, + _user: &Self::PermissionFilter, + ) -> Result, Self::Error> { + let pg_filter: PostgresFilter = filter.into(); + trace!(?pg_filter); + + Ok(sqlx::query_as!( + PostgresResource, + r#" + SELECT + r.id AS "id!", + r.created_date_time AS "created_date_time!", + r.modification_date_time AS "modification_date_time!", + r.resource_name AS "resource_name!", + r.ven_id AS "ven_id!", + r.attributes, + r.targets + FROM resource r + WHERE ($1::text[] IS NULL OR r.resource_name = ANY($1)) + AND ($2::jsonb = '[]'::jsonb OR $2::jsonb <@ r.targets) + OFFSET $3 LIMIT $4 + "#, + pg_filter.resource_names, + serde_json::to_value(pg_filter.targets) + .map_err(AppError::SerdeJsonInternalServerError)?, + pg_filter.skip, + pg_filter.limit, + ) + .fetch_all(&self.db) + .await? + .into_iter() + .map(TryInto::try_into) + .collect::>()?) + } + + async fn update( + &self, + id: &Self::Id, + new: Self::NewType, + _user: &Self::PermissionFilter, + ) -> Result { + let resource: Resource = sqlx::query_as!( + PostgresResource, + r#" + UPDATE resource + SET modification_date_time = now(), + resource_name = $2, + ven_id = $3, + attributes = $4, + targets = $5 + WHERE id = $1 + RETURNING * + "#, + id.as_str(), + new.resource_name, + new.ven_id, + to_json_value(new.attributes)?, + to_json_value(new.targets)? + ) + .fetch_one(&self.db) + .await? + .try_into()?; + + Ok(resource) + } + + async fn delete( + &self, + id: &Self::Id, + _user: &Self::PermissionFilter, + ) -> Result { + Ok(sqlx::query_as!( + PostgresResource, + r#" + DELETE FROM resource r + WHERE r.id = $1 + RETURNING r.* + "#, + id.as_str(), + ) + .fetch_one(&self.db) + .await? + .try_into()?) + } +} diff --git a/openadr-vtn/src/state.rs b/openadr-vtn/src/state.rs index a1cf18f..f35cac5 100644 --- a/openadr-vtn/src/state.rs +++ b/openadr-vtn/src/state.rs @@ -1,5 +1,6 @@ -use crate::api::ven; -use crate::data_source::{AuthSource, DataSource, EventCrud, ProgramCrud, ReportCrud, VenCrud}; +use crate::data_source::{ + AuthSource, DataSource, EventCrud, ProgramCrud, ReportCrud, ResourceCrud, VenCrud, +}; use crate::jwt::JwtManager; use axum::extract::FromRef; use std::sync::Arc; @@ -22,9 +23,12 @@ impl AppState { use axum::routing::{get, post}; use tower_http::trace::TraceLayer; + use crate::api::auth; + use crate::api::event; use crate::api::program; use crate::api::report; - use crate::api::{auth, event}; + use crate::api::resource; + use crate::api::ven; axum::Router::new() .route("/programs", get(program::get_all).post(program::add)) @@ -47,6 +51,14 @@ impl AppState { "/vens/:id", get(ven::get).put(ven::edit).delete(ven::delete), ) + .route( + "/vens:ven_id/resources", + get(resource::get_all).post(ven::add), + ) + .route( + "/vens/:id", + get(ven::get).put(ven::edit).delete(ven::delete), + ) .route("/auth/register", post(auth::register)) .route("/auth/token", post(auth::token)) .layer(TraceLayer::new_for_http()) @@ -86,3 +98,9 @@ impl FromRef for Arc { state.storage.vens() } } + +impl FromRef for Arc { + fn from_ref(state: &AppState) -> Arc { + state.storage.resources() + } +}