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