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: 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: i32,
94 github_email: Option<&str>,
95 tx: &DatabaseTransaction,
96 ) -> Result<User> {
97 if let Some(user_by_github_user_id) = user::Entity::find()
98 .filter(user::Column::GithubUserId.eq(github_user_id))
99 .one(tx)
100 .await?
101 {
102 let mut user_by_github_user_id = user_by_github_user_id.into_active_model();
103 user_by_github_user_id.github_login = ActiveValue::set(github_login.into());
104 Ok(user_by_github_user_id.update(tx).await?)
105 } else if let Some(user_by_github_login) = user::Entity::find()
106 .filter(user::Column::GithubLogin.eq(github_login))
107 .one(tx)
108 .await?
109 {
110 let mut user_by_github_login = user_by_github_login.into_active_model();
111 user_by_github_login.github_user_id = ActiveValue::set(Some(github_user_id));
112 Ok(user_by_github_login.update(tx).await?)
113 } else {
114 let user = user::Entity::insert(user::ActiveModel {
115 email_address: ActiveValue::set(github_email.map(|email| email.into())),
116 github_login: ActiveValue::set(github_login.into()),
117 github_user_id: ActiveValue::set(Some(github_user_id)),
118 admin: ActiveValue::set(false),
119 invite_count: ActiveValue::set(0),
120 invite_code: ActiveValue::set(None),
121 metrics_id: ActiveValue::set(Uuid::new_v4()),
122 ..Default::default()
123 })
124 .exec_with_returning(&*tx)
125 .await?;
126 Ok(user)
127 }
128 }
129
130 /// get_all_users returns the next page of users. To get more call again with
131 /// the same limit and the page incremented by 1.
132 pub async fn get_all_users(&self, page: u32, limit: u32) -> Result<Vec<User>> {
133 self.transaction(|tx| async move {
134 Ok(user::Entity::find()
135 .order_by_asc(user::Column::GithubLogin)
136 .limit(limit as u64)
137 .offset(page as u64 * limit as u64)
138 .all(&*tx)
139 .await?)
140 })
141 .await
142 }
143
144 /// Returns the metrics id for the user.
145 pub async fn get_user_metrics_id(&self, id: UserId) -> Result<String> {
146 #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
147 enum QueryAs {
148 MetricsId,
149 }
150
151 self.transaction(|tx| async move {
152 let metrics_id: Uuid = user::Entity::find_by_id(id)
153 .select_only()
154 .column(user::Column::MetricsId)
155 .into_values::<_, QueryAs>()
156 .one(&*tx)
157 .await?
158 .ok_or_else(|| anyhow!("could not find user"))?;
159 Ok(metrics_id.to_string())
160 })
161 .await
162 }
163
164 /// Sets "connected_once" on the user for analytics.
165 pub async fn set_user_connected_once(&self, id: UserId, connected_once: bool) -> Result<()> {
166 self.transaction(|tx| async move {
167 user::Entity::update_many()
168 .filter(user::Column::Id.eq(id))
169 .set(user::ActiveModel {
170 connected_once: ActiveValue::set(connected_once),
171 ..Default::default()
172 })
173 .exec(&*tx)
174 .await?;
175 Ok(())
176 })
177 .await
178 }
179
180 /// hard delete the user.
181 pub async fn destroy_user(&self, id: UserId) -> Result<()> {
182 self.transaction(|tx| async move {
183 access_token::Entity::delete_many()
184 .filter(access_token::Column::UserId.eq(id))
185 .exec(&*tx)
186 .await?;
187 user::Entity::delete_by_id(id).exec(&*tx).await?;
188 Ok(())
189 })
190 .await
191 }
192
193 /// Find users where github_login ILIKE name_query.
194 pub async fn fuzzy_search_users(&self, name_query: &str, limit: u32) -> Result<Vec<User>> {
195 self.transaction(|tx| async {
196 let tx = tx;
197 let like_string = Self::fuzzy_like_string(name_query);
198 let query = "
199 SELECT users.*
200 FROM users
201 WHERE github_login ILIKE $1
202 ORDER BY github_login <-> $2
203 LIMIT $3
204 ";
205
206 Ok(user::Entity::find()
207 .from_raw_sql(Statement::from_sql_and_values(
208 self.pool.get_database_backend(),
209 query,
210 vec![like_string.into(), name_query.into(), limit.into()],
211 ))
212 .all(&*tx)
213 .await?)
214 })
215 .await
216 }
217
218 /// fuzzy_like_string creates a string for matching in-order using fuzzy_search_users.
219 /// e.g. "cir" would become "%c%i%r%"
220 pub fn fuzzy_like_string(string: &str) -> String {
221 let mut result = String::with_capacity(string.len() * 2 + 1);
222 for c in string.chars() {
223 if c.is_alphanumeric() {
224 result.push('%');
225 result.push(c);
226 }
227 }
228 result.push('%');
229 result
230 }
231
232 /// Creates a new feature flag.
233 pub async fn create_user_flag(&self, flag: &str) -> Result<FlagId> {
234 self.transaction(|tx| async move {
235 let flag = feature_flag::Entity::insert(feature_flag::ActiveModel {
236 flag: ActiveValue::set(flag.to_string()),
237 ..Default::default()
238 })
239 .exec(&*tx)
240 .await?
241 .last_insert_id;
242
243 Ok(flag)
244 })
245 .await
246 }
247
248 /// Add the given user to the feature flag
249 pub async fn add_user_flag(&self, user: UserId, flag: FlagId) -> Result<()> {
250 self.transaction(|tx| async move {
251 user_feature::Entity::insert(user_feature::ActiveModel {
252 user_id: ActiveValue::set(user),
253 feature_id: ActiveValue::set(flag),
254 })
255 .exec(&*tx)
256 .await?;
257
258 Ok(())
259 })
260 .await
261 }
262
263 /// Returns the active flags for the user.
264 pub async fn get_user_flags(&self, user: UserId) -> Result<Vec<String>> {
265 self.transaction(|tx| async move {
266 #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
267 enum QueryAs {
268 Flag,
269 }
270
271 let flags = user::Model {
272 id: user,
273 ..Default::default()
274 }
275 .find_linked(user::UserFlags)
276 .select_only()
277 .column(feature_flag::Column::Flag)
278 .into_values::<_, QueryAs>()
279 .all(&*tx)
280 .await?;
281
282 Ok(flags)
283 })
284 .await
285 }
286}