diff --git a/migrations/000002_user_data.sql b/migrations/000002_user_data.sql index 419de3e..9ff7827 100644 --- a/migrations/000002_user_data.sql +++ b/migrations/000002_user_data.sql @@ -1,6 +1,8 @@ create table user_data( owner uuid not null references users(id) on delete cascade, "user" uuid not null references users(id) on delete cascade, + "key" text not null, data jsonb not null, - primary key (owner, "user") + unique (owner, "user"), + primary key (owner, "user", "key") ); diff --git a/src/api/user.rs b/src/api/user.rs index f430be4..5681dce 100644 --- a/src/api/user.rs +++ b/src/api/user.rs @@ -21,7 +21,8 @@ pub(super) fn router() -> Router> { Router::new() .route("/{target}", get(user_info)) .route("/@me/balance", get(user_balance)) - .route("/@me/data", get(get_user_data).post(set_user_data)) + .route("/@me/data", get(list_user_data_keys)) + .route("/@me/data/{key}", get(get_user_data).post(set_user_data)) .route("/@me/accounts", get(user_accounts)) .route("/@me/transactions", get(me_transaction_history)) .route("/", get(list_users)) @@ -101,14 +102,26 @@ pub async fn user_accounts(EState(state): State, auth: Auth) -> Result Result>, Error> { + let user = auth.user_id(); + let conn = state.conn().await?; + let data = Users::list_data(&conn, user).await?; + Ok(Json(data)) +} + #[instrument(skip(state))] pub async fn get_user_data( EState(state): State, auth: Auth, + Path(key): Path, ) -> Result>, Error> { let user = auth.user_id(); let conn = state.conn().await?; - let data = Users::get_data(&conn, user).await?; + let data = Users::get_data(&conn, user, &key).await?; Ok(Json(data)) } @@ -116,13 +129,14 @@ pub async fn get_user_data( pub async fn set_user_data( EState(state): State, auth: Auth, + Path(key): Path, Json(body): Json>, ) -> Result<(), Error> { let user = auth.user_id(); let mut conn = state.conn().await?; match body { - Some(data) => Users::set_data(&mut conn, user, data).await?, - None => Users::delete_data(&mut conn, user).await?, + Some(data) => Users::set_data(&mut conn, user, &key, data).await?, + None => Users::delete_data(&mut conn, user, &key).await?, } Ok(()) } diff --git a/src/model/user.rs b/src/model/user.rs index aff9831..fa99a08 100644 --- a/src/model/user.rs +++ b/src/model/user.rs @@ -121,37 +121,64 @@ impl Users { Ok(info) } + #[instrument(skip(client))] + pub async fn list_data( + client: &impl GenericClient, + user: Uuid, + ) -> Result, tokio_postgres::Error> { + let stmt = client + .prepare_cached("select \"key\" from user_data where owner = $1 and \"user\" = $1") + .await?; + let res = client + .query(&stmt, &[&user]) + .await? + .into_iter() + .map(|row| row.get(0)) + .collect(); + Ok(res) + } + + #[instrument(skip(client))] pub async fn get_data( client: &impl GenericClient, user: Uuid, + key: &str, ) -> Result, tokio_postgres::Error> { let stmt = client - .prepare_cached("select data from user_data where owner = $1 and \"user\" = $1") + .prepare_cached( + "select data from user_data where owner = $1 and \"user\" = $1 and \"key\" = $2 ", + ) .await?; - let res = client.query_opt(&stmt, &[&user]).await?; + let res = client.query_opt(&stmt, &[&user, &key]).await?; Ok(res.map(|row| row.get(0))) } + #[instrument(skip(client, data))] pub async fn set_data( client: &mut impl GenericClient, user: Uuid, + key: &str, data: serde_json::Value, ) -> Result<(), tokio_postgres::Error> { let stmt = client - .prepare_cached("insert into user_data(owner, \"user\", data) values ($1, $1, $2) on conflict (owner, \"user\") do update set data = $2") + .prepare_cached("insert into user_data(owner, \"user\", \"key\", data) values ($1, $1, $2, $3) on conflict (owner, \"user\") do update set data = $3") .await?; - client.execute(&stmt, &[&user, &data]).await?; + client.execute(&stmt, &[&user, &key, &data]).await?; Ok(()) } + #[instrument(skip(client))] pub async fn delete_data( client: &mut impl GenericClient, user: Uuid, + key: &str, ) -> Result<(), tokio_postgres::Error> { let stmt = client - .prepare_cached("delete from user_data where \"owner\" = $1 and user = $2") + .prepare_cached( + "delete from user_data where \"owner\" = $1 and \"user\" = $1 and \"key\" = $2", + ) .await?; - client.execute(&stmt, &[&user]).await?; + client.execute(&stmt, &[&user, &key]).await?; Ok(()) } } diff --git a/tests/integration/user_data.hurl b/tests/integration/user_data.hurl index 58d7ecd..83d58ff 100644 --- a/tests/integration/user_data.hurl +++ b/tests/integration/user_data.hurl @@ -8,16 +8,60 @@ HTTP 200 [Captures] token: jsonpath "$.token" -POST {{host}}/api/users/@me/data +GET {{host}}/api/users/@me/data +Authorization: Bearer {{token}} +HTTP 200 + +[Asserts] +jsonpath "$" isCollection +jsonpath "$" isEmpty + +GET {{host}}/api/users/@me/data/test +Authorization: Bearer {{token}} +HTTP 200 + +[Asserts] +jsonpath "$" == null + +POST {{host}}/api/users/@me/data/test Authorization: Bearer {{token}} { "hello": "world" } HTTP 200 -GET {{host}}/api/users/@me/data +GET {{host}}/api/users/@me/data/test Authorization: Bearer {{token}} HTTP 200 [Asserts] jsonpath "$.hello" == "world" + +GET {{host}}/api/users/@me/data +Authorization: Bearer {{token}} +HTTP 200 + +[Asserts] +jsonpath "$" isCollection +jsonpath "$" count == 1 +jsonpath "$[0]" == "test" + +POST {{host}}/api/users/@me/data/test +Authorization: Bearer {{token}} +null +HTTP 200 + +GET {{host}}/api/users/@me/data/test +Authorization: Bearer {{token}} +HTTP 200 + +[Asserts] +jsonpath "$" == null + +GET {{host}}/api/users/@me/data +Authorization: Bearer {{token}} +HTTP 200 + +[Asserts] +jsonpath "$" isCollection +jsonpath "$" isEmpty