Skip to main content

arminius_lib_db_devices/
lib.rs

1// SPDX-FileCopyrightText: 2025 BWI GmbH
2//
3// SPDX-License-Identifier: Apache-2.0
4
5//! Queries related to the `devices` table.
6
7mod tests;
8
9use arminius_lib_db::{Database, DatabaseError};
10use ruma::{DeviceId, OwnedDeviceId, UserId};
11
12/// Represents a device from the database
13#[derive(Debug)]
14pub struct Device {
15	/// Identifier of this device.
16	pub device_id: OwnedDeviceId,
17}
18
19/// Queries related to the device table.
20// Queries are usually used as extension traits so suggest `import as _`.
21#[rust_analyzer::prefer_underscore_import]
22pub trait DeviceQueries: Database {
23	/// Fetches all devices for a specific user
24	#[tracing::instrument(skip(self))]
25	async fn fetch_devices_for_user(
26		&mut self,
27		user_id: &UserId,
28	) -> Result<Vec<Device>, DatabaseError> {
29		Ok(sqlx::query_as!(
30			Device,
31			"SELECT device_id FROM devices WHERE user_nid = (SELECT nid FROM users WHERE user_id = $1)",
32			user_id.as_str()
33		)
34		.fetch_all(self.connection().await?)
35		.await?)
36	}
37
38	/// Fetches a specific device for a specific user
39	#[tracing::instrument(skip(self))]
40	async fn fetch_device_for_user(
41		&mut self,
42		user_id: &UserId,
43		device_id: &DeviceId,
44	) -> Result<Option<Device>, DatabaseError> {
45		Ok(sqlx::query_as!(
46			Device,
47			"SELECT device_id FROM devices WHERE user_nid = (SELECT nid FROM users WHERE user_id = $1) AND device_id = $2",
48			user_id.as_str(),
49			device_id.as_str(),
50		)
51		.fetch_optional(self.connection().await?)
52		.await?)
53	}
54
55	/// Inserts a new device.
56	///
57	/// NOTE: For now, the `idp_id` is set with the value of the `device_id`.
58	#[tracing::instrument(skip(self))]
59	async fn insert_device_for_user(
60		&mut self,
61		user_id: &UserId,
62		device_id: &DeviceId,
63	) -> Result<(), DatabaseError> {
64		sqlx::query!(
65			"INSERT INTO devices (user_nid, device_id, idp_id)
66				VALUES ((SELECT nid FROM users WHERE user_id = $1), $2, $2)",
67			user_id.to_string(),
68			device_id.to_string()
69		)
70		.execute(self.connection().await?)
71		.await?;
72
73		Ok(())
74	}
75
76	/// Idempotently inserts a device coming from a homeserver import. `idp_id`
77	/// is left NULL — linking to the IdP is a separate manual step. Synapse's
78	/// last_seen/ip/user_agent have no arminius equivalent and are dropped.
79	#[tracing::instrument(skip(self))]
80	async fn import_device(
81		&mut self,
82		user_id: &UserId,
83		device_id: &DeviceId,
84	) -> Result<bool, DatabaseError> {
85		let result = sqlx::query!(
86			r#"
87			INSERT INTO devices (user_nid, device_id, idp_id)
88				VALUES ((
89						SELECT
90							nid
91						FROM
92							users
93						WHERE
94							user_id = $1), $2, NULL)
95			ON CONFLICT (user_nid,
96				device_id)
97				DO NOTHING
98			"#,
99			user_id.as_str(),
100			device_id.as_str(),
101		)
102		.execute(self.connection().await?)
103		.await?;
104		Ok(result.rows_affected() > 0)
105	}
106
107	/// Insert a test user with a given id (if it does not exist) with a device
108	/// into the database
109	#[cfg(feature = "test_utils")]
110	#[tracing::instrument(skip(self))]
111	async fn insert_test_user_with_device(
112		&mut self,
113		user_id: &UserId,
114		device_id: &DeviceId,
115	) -> Result<(), DatabaseError> {
116		// This does cause unnecessary updates to the database, but is
117		// significantly easier to read than properly returning the nid in DO
118		// NOTHING
119		sqlx::query!(
120		"WITH new_user AS
121                  (INSERT INTO users(user_id, idp_id) VALUES ($1, $1) ON CONFLICT (user_id) DO UPDATE SET user_id = EXCLUDED.user_id RETURNING nid)
122		INSERT INTO devices(user_nid, device_id, idp_id) VALUES ((SELECT nid FROM new_user), $2, $2) ON CONFLICT (user_nid, device_id) DO NOTHING",
123		user_id.as_str(),
124		device_id.as_str(),
125	)
126	.execute(self.connection().await?)
127	.await?;
128		Ok(())
129	}
130}
131
132/// Make the query available on types implementing the
133/// Database trait.
134impl<T> DeviceQueries for T where T: Database {}