1use super::*;
2
3impl Database {
4 /// Creates a new user.
5 pub async fn create_user(
6 &self,
7 email_address: &str,
8 admin: bool,
9 params: NewUserParams,
10 ) -> Result<NewUserResult> {
11 self.transaction(|tx| async {
12 let tx = tx;
13 let user = user::Entity::insert(user::ActiveModel {
14 email_address: ActiveValue::set(Some(email_address.into())),
15 github_login: ActiveValue::set(params.github_login.clone()),
16 github_user_id: ActiveValue::set(Some(params.github_user_id)),
17 admin: ActiveValue::set(admin),
18 metrics_id: ActiveValue::set(Uuid::new_v4()),
19 ..Default::default()
20 })
21 .on_conflict(
22 OnConflict::column(user::Column::GithubLogin)
23 .update_column(user::Column::GithubLogin)
24 .to_owned(),
25 )
26 .exec_with_returning(&*tx)
27 .await?;
28
29 Ok(NewUserResult {
30 user_id: user.id,
31 metrics_id: user.metrics_id.to_string(),
32 signup_device_id: None,
33 inviting_user_id: None,
34 })
35 })
36 .await
37 }
38
39 /// Returns a user by ID. There are no access checks here, so this should only be used internally.
40 pub async fn get_user_by_id(&self, id: UserId) -> Result<Option<user::Model>> {
41 self.transaction(|tx| async move { Ok(user::Entity::find_by_id(id).one(&*tx).await?) })
42 .await
43 }
44
45 /// Returns all users by ID. There are no access checks here, so this should only be used internally.
46 pub async fn get_users_by_ids(&self, ids: Vec<UserId>) -> Result<Vec<user::Model>> {
47 self.transaction(|tx| async {
48 let tx = tx;
49 Ok(user::Entity::find()
50 .filter(user::Column::Id.is_in(ids.iter().copied()))
51 .all(&*tx)
52 .await?)
53 })
54 .await
55 }
56
57 /// Returns a user by GitHub login. There are no access checks here, so this should only be used internally.
58 pub async fn get_user_by_github_login(&self, github_login: &str) -> Result<Option<User>> {
59 self.transaction(|tx| async move {
60 Ok(user::Entity::find()
61 .filter(user::Column::GithubLogin.eq(github_login))
62 .one(&*tx)
63 .await?)
64 })
65 .await
66 }
67
68 pub async fn get_or_create_user_by_github_account(
69 &self,
70 github_login: &str,
71 github_user_id: Option<i32>,
72 github_email: Option<&str>,
73 ) -> Result<Option<User>> {
74 self.transaction(|tx| async move {
75 let tx = &*tx;
76 if let Some(github_user_id) = github_user_id {
77 if let Some(user_by_github_user_id) = user::Entity::find()
78 .filter(user::Column::GithubUserId.eq(github_user_id))
79 .one(tx)
80 .await?
81 {
82 let mut user_by_github_user_id = user_by_github_user_id.into_active_model();
83 user_by_github_user_id.github_login = ActiveValue::set(github_login.into());
84 Ok(Some(user_by_github_user_id.update(tx).await?))
85 } else if let Some(user_by_github_login) = user::Entity::find()
86 .filter(user::Column::GithubLogin.eq(github_login))
87 .one(tx)
88 .await?
89 {
90 let mut user_by_github_login = user_by_github_login.into_active_model();
91 user_by_github_login.github_user_id = ActiveValue::set(Some(github_user_id));
92 Ok(Some(user_by_github_login.update(tx).await?))
93 } else {
94 let user = user::Entity::insert(user::ActiveModel {
95 email_address: ActiveValue::set(github_email.map(|email| email.into())),
96 github_login: ActiveValue::set(github_login.into()),
97 github_user_id: ActiveValue::set(Some(github_user_id)),
98 admin: ActiveValue::set(false),
99 invite_count: ActiveValue::set(0),
100 invite_code: ActiveValue::set(None),
101 metrics_id: ActiveValue::set(Uuid::new_v4()),
102 ..Default::default()
103 })
104 .exec_with_returning(&*tx)
105 .await?;
106 Ok(Some(user))
107 }
108 } else {
109 Ok(user::Entity::find()
110 .filter(user::Column::GithubLogin.eq(github_login))
111 .one(tx)
112 .await?)
113 }
114 })
115 .await
116 }
117
118 /// get_all_users returns the next page of users. To get more call again with
119 /// the same limit and the page incremented by 1.
120 pub async fn get_all_users(&self, page: u32, limit: u32) -> Result<Vec<User>> {
121 self.transaction(|tx| async move {
122 Ok(user::Entity::find()
123 .order_by_asc(user::Column::GithubLogin)
124 .limit(limit as u64)
125 .offset(page as u64 * limit as u64)
126 .all(&*tx)
127 .await?)
128 })
129 .await
130 }
131
132 /// Returns the metrics id for the user.
133 pub async fn get_user_metrics_id(&self, id: UserId) -> Result<String> {
134 #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
135 enum QueryAs {
136 MetricsId,
137 }
138
139 self.transaction(|tx| async move {
140 let metrics_id: Uuid = user::Entity::find_by_id(id)
141 .select_only()
142 .column(user::Column::MetricsId)
143 .into_values::<_, QueryAs>()
144 .one(&*tx)
145 .await?
146 .ok_or_else(|| anyhow!("could not find user"))?;
147 Ok(metrics_id.to_string())
148 })
149 .await
150 }
151
152 /// Set "connected_once" on the user for analytics.
153 pub async fn set_user_connected_once(&self, id: UserId, connected_once: bool) -> Result<()> {
154 self.transaction(|tx| async move {
155 user::Entity::update_many()
156 .filter(user::Column::Id.eq(id))
157 .set(user::ActiveModel {
158 connected_once: ActiveValue::set(connected_once),
159 ..Default::default()
160 })
161 .exec(&*tx)
162 .await?;
163 Ok(())
164 })
165 .await
166 }
167
168 /// hard delete the user.
169 pub async fn destroy_user(&self, id: UserId) -> Result<()> {
170 self.transaction(|tx| async move {
171 access_token::Entity::delete_many()
172 .filter(access_token::Column::UserId.eq(id))
173 .exec(&*tx)
174 .await?;
175 user::Entity::delete_by_id(id).exec(&*tx).await?;
176 Ok(())
177 })
178 .await
179 }
180
181 /// Find users where github_login ILIKE name_query.
182 pub async fn fuzzy_search_users(&self, name_query: &str, limit: u32) -> Result<Vec<User>> {
183 self.transaction(|tx| async {
184 let tx = tx;
185 let like_string = Self::fuzzy_like_string(name_query);
186 let query = "
187 SELECT users.*
188 FROM users
189 WHERE github_login ILIKE $1
190 ORDER BY github_login <-> $2
191 LIMIT $3
192 ";
193
194 Ok(user::Entity::find()
195 .from_raw_sql(Statement::from_sql_and_values(
196 self.pool.get_database_backend(),
197 query,
198 vec![like_string.into(), name_query.into(), limit.into()],
199 ))
200 .all(&*tx)
201 .await?)
202 })
203 .await
204 }
205
206 /// fuzzy_like_string creates a string for matching in-order using fuzzy_search_users.
207 /// e.g. "cir" would become "%c%i%r%"
208 pub fn fuzzy_like_string(string: &str) -> String {
209 let mut result = String::with_capacity(string.len() * 2 + 1);
210 for c in string.chars() {
211 if c.is_alphanumeric() {
212 result.push('%');
213 result.push(c);
214 }
215 }
216 result.push('%');
217 result
218 }
219
220 /// Creates a new feature flag.
221 pub async fn create_user_flag(&self, flag: &str) -> Result<FlagId> {
222 self.transaction(|tx| async move {
223 let flag = feature_flag::Entity::insert(feature_flag::ActiveModel {
224 flag: ActiveValue::set(flag.to_string()),
225 ..Default::default()
226 })
227 .exec(&*tx)
228 .await?
229 .last_insert_id;
230
231 Ok(flag)
232 })
233 .await
234 }
235
236 /// Add the given user to the feature flag
237 pub async fn add_user_flag(&self, user: UserId, flag: FlagId) -> Result<()> {
238 self.transaction(|tx| async move {
239 user_feature::Entity::insert(user_feature::ActiveModel {
240 user_id: ActiveValue::set(user),
241 feature_id: ActiveValue::set(flag),
242 })
243 .exec(&*tx)
244 .await?;
245
246 Ok(())
247 })
248 .await
249 }
250
251 /// Return the active flags for the user.
252 pub async fn get_user_flags(&self, user: UserId) -> Result<Vec<String>> {
253 self.transaction(|tx| async move {
254 #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
255 enum QueryAs {
256 Flag,
257 }
258
259 let flags = user::Model {
260 id: user,
261 ..Default::default()
262 }
263 .find_linked(user::UserFlags)
264 .select_only()
265 .column(feature_flag::Column::Flag)
266 .into_values::<_, QueryAs>()
267 .all(&*tx)
268 .await?;
269
270 Ok(flags)
271 })
272 .await
273 }
274}