Skip to content

Commit 1007cdd

Browse files
committed
[WIP] Data storage
1 parent aa2079a commit 1007cdd

2 files changed

Lines changed: 371 additions & 1 deletion

File tree

openleadr-vtn/src/data_source/postgres/mod.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use crate::{
66
data_source::{
77
postgres::{
88
event::PgEventStorage, program::PgProgramStorage, report::PgReportStorage,
9-
ven::PgVenStorage,
9+
subscription::PgSubscriptionStorage, ven::PgVenStorage,
1010
},
1111
DataSource, EventCrud, ProgramCrud, ReportCrud, ResourceCrud, VenCrud,
1212
},
@@ -25,6 +25,7 @@ mod event;
2525
mod program;
2626
mod report;
2727
mod resource;
28+
mod subscription;
2829
#[cfg(feature = "internal-oauth")]
2930
mod user;
3031
mod ven;
@@ -59,6 +60,10 @@ impl DataSource for PostgresStorage {
5960
Arc::<PgResourceStorage>::new(self.db.clone().into())
6061
}
6162

63+
fn subscriptions(&self) -> Arc<dyn super::SubscriptionCrud> {
64+
Arc::<PgSubscriptionStorage>::new(self.db.clone().into())
65+
}
66+
6267
#[cfg(feature = "internal-oauth")]
6368
fn auth(&self) -> Arc<dyn AuthSource> {
6469
Arc::<PgAuthSource>::new(self.db.clone().into())
Lines changed: 365 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,365 @@
1+
use crate::{
2+
api::subscription::QueryParams,
3+
data_source::{postgres::to_json_value, Crud, SubscriptionCrud},
4+
error::AppError,
5+
};
6+
use async_trait::async_trait;
7+
use chrono::{DateTime, Utc};
8+
use openleadr_wire::{
9+
subscription::{
10+
NotificationMechanism, Operation, Subscription, SubscriptionId, SubscriptionRequest,
11+
},
12+
target::Target,
13+
ClientId, ObjectType,
14+
};
15+
use sqlx::PgPool;
16+
use tracing::{error, trace, warn};
17+
18+
impl SubscriptionCrud for PgSubscriptionStorage {}
19+
20+
pub(crate) struct PgSubscriptionStorage {
21+
db: PgPool,
22+
}
23+
24+
impl From<PgPool> for PgSubscriptionStorage {
25+
fn from(db: PgPool) -> Self {
26+
Self { db }
27+
}
28+
}
29+
30+
#[derive(Debug)]
31+
pub(crate) struct PostgresSubscription {
32+
id: String,
33+
created_date_time: DateTime<Utc>,
34+
modification_date_time: DateTime<Utc>,
35+
client_id: ClientId,
36+
client_name: String,
37+
program_id: Option<String>,
38+
object_operations: Vec<SubscriptionObjectOperation>,
39+
targets: Vec<Target>,
40+
}
41+
42+
impl TryFrom<PostgresSubscription> for Subscription {
43+
type Error = AppError;
44+
45+
#[tracing::instrument(name = "TryFrom<PostgresResource> for Resource")]
46+
fn try_from(value: PostgresSubscription) -> Result<Self, Self::Error> {
47+
let attributes = match value.attributes {
48+
None => None,
49+
Some(t) => serde_json::from_value(t)
50+
.inspect_err(|err| {
51+
error!(
52+
?err,
53+
"Failed to deserialize JSON from DB to `Vec<ValuesMap>`"
54+
)
55+
})
56+
.map_err(AppError::SerdeJsonInternalServerError)?,
57+
};
58+
59+
Ok(Self {
60+
id: value.id.parse()?,
61+
created_date_time: value.created_date_time,
62+
modification_date_time: value.modification_date_time,
63+
content: SubscriptionRequest {
64+
client_name: value.client_name,
65+
program_id: value
66+
.program_id
67+
.map(|program_id| program_id.parse())
68+
.transpose()?,
69+
object_operations: (),
70+
targets: value.targets,
71+
},
72+
})
73+
}
74+
}
75+
76+
#[async_trait]
77+
impl Crud for PgSubscriptionStorage {
78+
type Type = Subscription;
79+
type Id = SubscriptionId;
80+
type NewType = SubscriptionRequest;
81+
type Error = AppError;
82+
type Filter = QueryParams;
83+
type PermissionFilter = Option<ClientId>;
84+
85+
async fn create(
86+
&self,
87+
new: Self::NewType,
88+
_client_id: &Self::PermissionFilter,
89+
) -> Result<Self::Type, Self::Error> {
90+
let resource: Subscription = sqlx::query_as!(
91+
PostgresSubscription,
92+
r#"
93+
INSERT INTO resource (
94+
id,
95+
created_date_time,
96+
modification_date_time,
97+
resource_name,
98+
ven_id,
99+
attributes,
100+
targets
101+
)
102+
VALUES (gen_random_uuid(), now(), now(), $1, $2, $3, $4)
103+
RETURNING
104+
id,
105+
created_date_time,
106+
modification_date_time,
107+
resource_name,
108+
ven_id,
109+
attributes,
110+
targets as "targets:Vec<Target>"
111+
"#,
112+
new.resource_name,
113+
new.ven_id.as_str(),
114+
to_json_value(new.attributes)?,
115+
new.targets as _,
116+
)
117+
.fetch_one(&self.db)
118+
.await?
119+
.try_into()?;
120+
121+
Ok(resource)
122+
}
123+
124+
async fn retrieve(
125+
&self,
126+
id: &Self::Id,
127+
client_id: &Self::PermissionFilter,
128+
) -> Result<Self::Type, Self::Error> {
129+
let resource = sqlx::query_as!(
130+
PostgresSubscription,
131+
r#"
132+
SELECT
133+
r.id,
134+
r.created_date_time,
135+
r.modification_date_time,
136+
r.resource_name,
137+
r.ven_id,
138+
r.attributes,
139+
r.targets as "targets:Vec<Target>"
140+
FROM resource r
141+
JOIN ven v on r.ven_id = v.id
142+
WHERE r.id = $1
143+
AND ($2::text IS NULL OR v.client_id = $2)
144+
"#,
145+
id.as_str(),
146+
client_id as _
147+
)
148+
.fetch_one(&self.db)
149+
.await?
150+
.try_into()?;
151+
152+
Ok(resource)
153+
}
154+
155+
async fn retrieve_all(
156+
&self,
157+
filter: &Self::Filter,
158+
client_id: &Self::PermissionFilter,
159+
) -> Result<Vec<Self::Type>, Self::Error> {
160+
let res = sqlx::query_as!(
161+
PostgresSubscription,
162+
r#"
163+
SELECT
164+
r.id,
165+
r.created_date_time,
166+
r.modification_date_time,
167+
r.resource_name,
168+
r.ven_id,
169+
r.attributes,
170+
r.targets as "targets:Vec<Target>"
171+
FROM resource r
172+
JOIN ven v on r.ven_id = v.id
173+
WHERE ($1::text IS NULL OR r.ven_id = $1)
174+
AND ($2::text IS NULL OR r.resource_name = $2)
175+
AND ($3::text[] IS NULL OR r.targets @> $3)
176+
AND ($4::text IS NULL OR v.client_id = $4)
177+
ORDER BY r.created_date_time
178+
OFFSET $5 LIMIT $6
179+
"#,
180+
filter.ven_id as _,
181+
filter.resource_name,
182+
filter.targets.as_deref() as _,
183+
client_id as _,
184+
filter.skip,
185+
filter.limit,
186+
)
187+
.fetch_all(&self.db)
188+
.await?
189+
.into_iter()
190+
.map(TryInto::try_into)
191+
.collect::<Result<Vec<_>, _>>()?;
192+
193+
trace!("retrieved {} resources", res.len());
194+
195+
Ok(res)
196+
}
197+
198+
async fn update(
199+
&self,
200+
id: &Self::Id,
201+
new: Self::NewType,
202+
client_id: &Self::PermissionFilter,
203+
) -> Result<Self::Type, Self::Error> {
204+
let mut tx = self.db.begin().await?;
205+
206+
let old_ven_id = sqlx::query_scalar!(
207+
r#"
208+
SELECT ven_id FROM resource WHERE id = $1
209+
"#,
210+
id.as_str()
211+
)
212+
.fetch_one(&mut *tx)
213+
.await?;
214+
215+
if old_ven_id != new.ven_id.as_str() {
216+
let error = "Tried to update `ven_id` of resource. \
217+
This is not allowed in the current version of openLEADR as the specification is not quite \
218+
clear about if that should be allowed. If you disagree with that interpretation, please open \
219+
an issue on GitHub.";
220+
error!(resource_id = id.as_str(), "{}", error);
221+
return Err(Self::Error::BadRequest(error));
222+
}
223+
224+
let resource: Subscription = sqlx::query_as!(
225+
PostgresSubscription,
226+
r#"
227+
UPDATE resource r
228+
SET modification_date_time = now(),
229+
resource_name = $2,
230+
attributes = $3,
231+
targets = $4
232+
FROM ven v
233+
WHERE r.ven_id = v.id
234+
AND r.id = $1
235+
AND ($5::text IS NULL OR v.client_id = $5)
236+
RETURNING
237+
r.id,
238+
r.created_date_time,
239+
r.modification_date_time,
240+
r.resource_name,
241+
r.ven_id,
242+
r.attributes,
243+
r.targets as "targets:Vec<Target>"
244+
"#,
245+
id.as_str(),
246+
new.resource_name,
247+
to_json_value(new.attributes)?,
248+
new.targets as _,
249+
client_id as _
250+
)
251+
.fetch_one(&mut *tx)
252+
.await?
253+
.try_into()?;
254+
255+
tx.commit().await?;
256+
257+
Ok(resource)
258+
}
259+
260+
async fn delete(
261+
&self,
262+
id: &Self::Id,
263+
client_id: &Self::PermissionFilter,
264+
) -> Result<Self::Type, Self::Error> {
265+
Ok(sqlx::query_as!(
266+
PostgresSubscription,
267+
r#"
268+
DELETE FROM resource r
269+
USING ven v
270+
WHERE r.ven_id = v.id
271+
AND r.id = $1
272+
AND ($2::text IS NULL OR v.client_id = $2)
273+
RETURNING
274+
r.id,
275+
r.created_date_time,
276+
r.modification_date_time,
277+
r.resource_name,
278+
r.ven_id,
279+
r.attributes,
280+
r.targets as "targets:Vec<Target>"
281+
"#,
282+
id.as_str(),
283+
client_id as _
284+
)
285+
.fetch_one(&self.db)
286+
.await?
287+
.try_into()?)
288+
}
289+
}
290+
291+
#[cfg(test)]
292+
#[cfg(feature = "live-db-test")]
293+
mod test {
294+
use crate::{
295+
api::subscription::QueryParams,
296+
data_source::{postgres::subscription::PgSubscriptionStorage, Crud},
297+
};
298+
use sqlx::PgPool;
299+
300+
impl Default for QueryParams {
301+
fn default() -> Self {
302+
Self {
303+
program_id: None,
304+
objects: vec![],
305+
skip: 0,
306+
limit: 50,
307+
}
308+
}
309+
}
310+
311+
impl QueryParams {
312+
fn program_id(program_id: &str) -> QueryParams {
313+
Self {
314+
program_id: Some(program_id.parse().unwrap()),
315+
..Self::default()
316+
}
317+
}
318+
}
319+
320+
#[sqlx::test(fixtures("users", "vens", "resources", "subscriptions"))] // FIXME remove unnecessary fixtures
321+
async fn retrieve_all(db: PgPool) {
322+
let repo = PgSubscriptionStorage::from(db.clone());
323+
324+
let subscription = repo
325+
.retrieve_all(
326+
&QueryParams::program_id("program-1"),
327+
&Some("ven-1-client-id".parse().unwrap()),
328+
)
329+
.await
330+
.unwrap();
331+
assert_eq!(subscription.len(), 2);
332+
333+
let subscription = repo
334+
.retrieve_all(
335+
&QueryParams::program("program-2"),
336+
&Some("ven-2-client-id".parse().unwrap()),
337+
)
338+
.await
339+
.unwrap();
340+
assert_eq!(subscription.len(), 3);
341+
342+
let filters = QueryParams {
343+
resource_name: Some("resource-1-name".to_string()),
344+
ven_id: Some("ven-1".parse().unwrap()),
345+
..Default::default()
346+
};
347+
348+
let resources = repo
349+
.retrieve_all(&filters, &Some("ven-1-client-id".parse().unwrap()))
350+
.await
351+
.unwrap();
352+
assert_eq!(resources.len(), 1);
353+
assert_eq!(resources[0].content.resource_name, "resource-1-name");
354+
355+
// Ensure a client cannot see resources of another client
356+
let resources = repo
357+
.retrieve_all(
358+
&QueryParams::program_id("program-2"),
359+
&Some("ven-1-client-id".parse().unwrap()),
360+
)
361+
.await
362+
.unwrap();
363+
assert_eq!(resources.len(), 0);
364+
}
365+
}

0 commit comments

Comments
 (0)