Create profile_emoji database table

This commit is contained in:
silverpill 2023-01-21 00:23:15 +00:00
parent 2787efc83f
commit 70c2d2aa25
16 changed files with 123 additions and 53 deletions

View file

@ -0,0 +1,5 @@
CREATE TABLE profile_emoji (
profile_id UUID NOT NULL REFERENCES actor_profile (id) ON DELETE CASCADE,
emoji_id UUID NOT NULL REFERENCES emoji (id) ON DELETE CASCADE,
PRIMARY KEY (profile_id, emoji_id)
);

View file

@ -178,6 +178,12 @@ CREATE TABLE post_emoji (
PRIMARY KEY (post_id, emoji_id) PRIMARY KEY (post_id, emoji_id)
); );
CREATE TABLE profile_emoji (
profile_id UUID NOT NULL REFERENCES actor_profile (id) ON DELETE CASCADE,
emoji_id UUID NOT NULL REFERENCES emoji (id) ON DELETE CASCADE,
PRIMARY KEY (profile_id, emoji_id)
);
CREATE TABLE notification ( CREATE TABLE notification (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
sender_id UUID NOT NULL REFERENCES actor_profile (id) ON DELETE CASCADE, sender_id UUID NOT NULL REFERENCES actor_profile (id) ON DELETE CASCADE,

View file

@ -210,7 +210,7 @@ impl RefetchActor {
pub async fn execute( pub async fn execute(
&self, &self,
config: &Config, config: &Config,
db_client: &impl DatabaseClient, db_client: &mut impl DatabaseClient,
) -> Result<(), Error> { ) -> Result<(), Error> {
let profile = get_profile_by_remote_actor_id( let profile = get_profile_by_remote_actor_id(
db_client, db_client,

View file

@ -1,5 +1,7 @@
use std::path::Path; use std::path::Path;
use uuid::Uuid;
use mitra_config::Instance; use mitra_config::Instance;
use crate::activitypub::{ use crate::activitypub::{
@ -86,7 +88,7 @@ async fn parse_tags(
instance: &Instance, instance: &Instance,
media_dir: &Path, media_dir: &Path,
actor: &Actor, actor: &Actor,
) -> Result<(), HandlerError> { ) -> Result<Vec<Uuid>, HandlerError> {
let mut emojis = vec![]; let mut emojis = vec![];
for tag_value in actor.tag.clone() { for tag_value in actor.tag.clone() {
let tag_type = tag_value["type"].as_str().unwrap_or(HASHTAG); let tag_type = tag_value["type"].as_str().unwrap_or(HASHTAG);
@ -112,11 +114,11 @@ async fn parse_tags(
log::warn!("skipping actor tag of type {}", tag_type); log::warn!("skipping actor tag of type {}", tag_type);
}; };
}; };
Ok(()) Ok(emojis)
} }
pub async fn create_remote_profile( pub async fn create_remote_profile(
db_client: &impl DatabaseClient, db_client: &mut impl DatabaseClient,
instance: &Instance, instance: &Instance,
media_dir: &Path, media_dir: &Path,
actor: Actor, actor: Actor,
@ -134,7 +136,7 @@ pub async fn create_remote_profile(
).await; ).await;
let (identity_proofs, payment_options, extra_fields) = let (identity_proofs, payment_options, extra_fields) =
actor.parse_attachments(); actor.parse_attachments();
parse_tags( let emojis = parse_tags(
db_client, db_client,
instance, instance,
media_dir, media_dir,
@ -150,6 +152,7 @@ pub async fn create_remote_profile(
identity_proofs, identity_proofs,
payment_options, payment_options,
extra_fields, extra_fields,
emojis,
actor_json: Some(actor), actor_json: Some(actor),
}; };
profile_data.clean()?; profile_data.clean()?;
@ -159,7 +162,7 @@ pub async fn create_remote_profile(
/// Updates remote actor's profile /// Updates remote actor's profile
pub async fn update_remote_profile( pub async fn update_remote_profile(
db_client: &impl DatabaseClient, db_client: &mut impl DatabaseClient,
instance: &Instance, instance: &Instance,
media_dir: &Path, media_dir: &Path,
profile: DbActorProfile, profile: DbActorProfile,
@ -189,7 +192,7 @@ pub async fn update_remote_profile(
).await; ).await;
let (identity_proofs, payment_options, extra_fields) = let (identity_proofs, payment_options, extra_fields) =
actor.parse_attachments(); actor.parse_attachments();
parse_tags( let emojis = parse_tags(
db_client, db_client,
instance, instance,
media_dir, media_dir,
@ -204,6 +207,7 @@ pub async fn update_remote_profile(
identity_proofs, identity_proofs,
payment_options, payment_options,
extra_fields, extra_fields,
emojis,
actor_json: Some(actor), actor_json: Some(actor),
}; };
profile_data.clean()?; profile_data.clean()?;

View file

@ -73,7 +73,7 @@ fn key_id_to_actor_id(key_id: &str) -> Result<String, AuthenticationError> {
async fn get_signer( async fn get_signer(
config: &Config, config: &Config,
db_client: &impl DatabaseClient, db_client: &mut impl DatabaseClient,
signer_id: &str, signer_id: &str,
no_fetch: bool, no_fetch: bool,
) -> Result<DbActorProfile, AuthenticationError> { ) -> Result<DbActorProfile, AuthenticationError> {
@ -100,7 +100,7 @@ async fn get_signer(
/// Verifies HTTP signature and returns signer /// Verifies HTTP signature and returns signer
pub async fn verify_signed_request( pub async fn verify_signed_request(
config: &Config, config: &Config,
db_client: &impl DatabaseClient, db_client: &mut impl DatabaseClient,
request: &HttpRequest, request: &HttpRequest,
no_fetch: bool, no_fetch: bool,
) -> Result<DbActorProfile, AuthenticationError> { ) -> Result<DbActorProfile, AuthenticationError> {
@ -131,7 +131,7 @@ pub async fn verify_signed_request(
/// Verifies JSON signature and returns signer /// Verifies JSON signature and returns signer
pub async fn verify_signed_activity( pub async fn verify_signed_activity(
config: &Config, config: &Config,
db_client: &impl DatabaseClient, db_client: &mut impl DatabaseClient,
activity: &Value, activity: &Value,
no_fetch: bool, no_fetch: bool,
) -> Result<DbActorProfile, AuthenticationError> { ) -> Result<DbActorProfile, AuthenticationError> {

View file

@ -31,7 +31,7 @@ use super::fetchers::{
}; };
pub async fn get_or_import_profile_by_actor_id( pub async fn get_or_import_profile_by_actor_id(
db_client: &impl DatabaseClient, db_client: &mut impl DatabaseClient,
instance: &Instance, instance: &Instance,
media_dir: &Path, media_dir: &Path,
actor_id: &str, actor_id: &str,
@ -107,7 +107,7 @@ pub async fn get_or_import_profile_by_actor_id(
/// Fetches actor profile and saves it into database /// Fetches actor profile and saves it into database
pub async fn import_profile_by_actor_address( pub async fn import_profile_by_actor_address(
db_client: &impl DatabaseClient, db_client: &mut impl DatabaseClient,
instance: &Instance, instance: &Instance,
media_dir: &Path, media_dir: &Path,
actor_address: &ActorAddress, actor_address: &ActorAddress,
@ -138,7 +138,7 @@ pub async fn import_profile_by_actor_address(
// Works with local profiles // Works with local profiles
pub async fn get_or_import_profile_by_actor_address( pub async fn get_or_import_profile_by_actor_address(
db_client: &impl DatabaseClient, db_client: &mut impl DatabaseClient,
instance: &Instance, instance: &Instance,
media_dir: &Path, media_dir: &Path,
actor_address: &ActorAddress, actor_address: &ActorAddress,

View file

@ -318,7 +318,7 @@ pub async fn handle_emoji(
pub async fn get_object_tags( pub async fn get_object_tags(
config: &Config, config: &Config,
db_client: &impl DatabaseClient, db_client: &mut impl DatabaseClient,
object: &Object, object: &Object,
redirects: &HashMap<String, String>, redirects: &HashMap<String, String>,
) -> Result<(Vec<Uuid>, Vec<String>, Vec<Uuid>, Vec<Uuid>), HandlerError> { ) -> Result<(Vec<Uuid>, Vec<String>, Vec<Uuid>, Vec<Uuid>), HandlerError> {

View file

@ -95,7 +95,7 @@ struct UpdatePerson {
async fn handle_update_person( async fn handle_update_person(
config: &Config, config: &Config,
db_client: &impl DatabaseClient, db_client: &mut impl DatabaseClient,
activity: Value, activity: Value,
) -> HandlerResult { ) -> HandlerResult {
let activity: UpdatePerson = serde_json::from_value(activity) let activity: UpdatePerson = serde_json::from_value(activity)

View file

@ -358,6 +358,7 @@ impl AccountUpdateData {
identity_proofs, identity_proofs,
payment_options, payment_options,
extra_fields, extra_fields,
emojis: vec![],
actor_json: None, // always None for local profiles actor_json: None, // always None for local profiles
}; };
Ok(profile_data) Ok(profile_data)

View file

@ -236,7 +236,7 @@ async fn update_credentials(
db_pool: web::Data<DbPool>, db_pool: web::Data<DbPool>,
account_data: web::Json<AccountUpdateData>, account_data: web::Json<AccountUpdateData>,
) -> Result<HttpResponse, MastodonError> { ) -> Result<HttpResponse, MastodonError> {
let db_client = &**get_database_client(&db_pool).await?; let db_client = &mut **get_database_client(&db_pool).await?;
let mut current_user = get_current_user(db_client, auth.token()).await?; let mut current_user = get_current_user(db_client, auth.token()).await?;
let mut profile_data = account_data.into_inner() let mut profile_data = account_data.into_inner()
.into_profile_data( .into_profile_data(
@ -384,7 +384,7 @@ async fn create_identity_proof(
db_pool: web::Data<DbPool>, db_pool: web::Data<DbPool>,
proof_data: web::Json<IdentityProofData>, proof_data: web::Json<IdentityProofData>,
) -> Result<HttpResponse, MastodonError> { ) -> Result<HttpResponse, MastodonError> {
let db_client = &**get_database_client(&db_pool).await?; let db_client = &mut **get_database_client(&db_pool).await?;
let mut current_user = get_current_user(db_client, auth.token()).await?; let mut current_user = get_current_user(db_client, auth.token()).await?;
let did = proof_data.did.parse::<Did>() let did = proof_data.did.parse::<Did>()
.map_err(|_| ValidationError("invalid DID"))?; .map_err(|_| ValidationError("invalid DID"))?;

View file

@ -99,7 +99,7 @@ fn parse_search_query(search_query: &str) -> SearchQuery {
async fn search_profiles_or_import( async fn search_profiles_or_import(
config: &Config, config: &Config,
db_client: &impl DatabaseClient, db_client: &mut impl DatabaseClient,
username: String, username: String,
mut maybe_hostname: Option<String>, mut maybe_hostname: Option<String>,
limit: u16, limit: u16,
@ -183,7 +183,7 @@ async fn find_post_by_url(
async fn find_profile_by_url( async fn find_profile_by_url(
config: &Config, config: &Config,
db_client: &impl DatabaseClient, db_client: &mut impl DatabaseClient,
url: &str, url: &str,
) -> Result<Option<DbActorProfile>, DatabaseError> { ) -> Result<Option<DbActorProfile>, DatabaseError> {
let profile = match parse_local_actor_id( let profile = match parse_local_actor_id(

View file

@ -107,7 +107,7 @@ pub async fn register_subscription_option(
maybe_blockchain: web::Data<Option<ContractSet>>, maybe_blockchain: web::Data<Option<ContractSet>>,
subscription_option: web::Json<SubscriptionOption>, subscription_option: web::Json<SubscriptionOption>,
) -> Result<HttpResponse, MastodonError> { ) -> Result<HttpResponse, MastodonError> {
let db_client = &**get_database_client(&db_pool).await?; let db_client = &mut **get_database_client(&db_pool).await?;
let mut current_user = get_current_user(db_client, auth.token()).await?; let mut current_user = get_current_user(db_client, auth.token()).await?;
if !current_user.role.has_permission(Permission::ManageSubscriptionOptions) { if !current_user.role.has_permission(Permission::ManageSubscriptionOptions) {
return Err(MastodonError::PermissionError); return Err(MastodonError::PermissionError);

View file

@ -106,7 +106,7 @@ mod tests {
#[tokio::test] #[tokio::test]
#[serial] #[serial]
async fn test_create_attachment() { async fn test_create_attachment() {
let db_client = &create_test_database().await; let db_client = &mut create_test_database().await;
let profile_data = ProfileCreateData { let profile_data = ProfileCreateData {
username: "test".to_string(), username: "test".to_string(),
..Default::default() ..Default::default()

View file

@ -19,6 +19,7 @@ use crate::models::{
find_orphaned_ipfs_objects, find_orphaned_ipfs_objects,
DeletionQueue, DeletionQueue,
}, },
emojis::types::DbEmoji,
instances::queries::create_instance, instances::queries::create_instance,
relationships::types::RelationshipType, relationships::types::RelationshipType,
}; };
@ -31,16 +32,42 @@ use super::types::{
ProfileUpdateData, ProfileUpdateData,
}; };
async fn create_profile_emojis(
db_client: &impl DatabaseClient,
profile_id: &Uuid,
emojis: Vec<Uuid>,
) -> Result<Vec<DbEmoji>, DatabaseError> {
let emojis_rows = db_client.query(
"
INSERT INTO profile_emoji (profile_id, emoji_id)
SELECT $1, emoji.id FROM emoji WHERE id = ANY($2)
RETURNING (
SELECT emoji FROM emoji
WHERE emoji.id = emoji_id
)
",
&[&profile_id, &emojis],
).await?;
if emojis_rows.len() != emojis.len() {
return Err(DatabaseError::NotFound("emoji"));
};
let emojis = emojis_rows.iter()
.map(|row| row.try_get("emoji"))
.collect::<Result<_, _>>()?;
Ok(emojis)
}
/// Create new profile using given Client or Transaction. /// Create new profile using given Client or Transaction.
pub async fn create_profile( pub async fn create_profile(
db_client: &impl DatabaseClient, db_client: &mut impl DatabaseClient,
profile_data: ProfileCreateData, profile_data: ProfileCreateData,
) -> Result<DbActorProfile, DatabaseError> { ) -> Result<DbActorProfile, DatabaseError> {
let transaction = db_client.transaction().await?;
let profile_id = generate_ulid(); let profile_id = generate_ulid();
if let Some(ref hostname) = profile_data.hostname { if let Some(ref hostname) = profile_data.hostname {
create_instance(db_client, hostname).await?; create_instance(&transaction, hostname).await?;
}; };
let row = db_client.query_one( let row = transaction.query_one(
" "
INSERT INTO actor_profile ( INSERT INTO actor_profile (
id, username, hostname, display_name, bio, bio_source, id, username, hostname, display_name, bio, bio_source,
@ -67,15 +94,25 @@ pub async fn create_profile(
], ],
).await.map_err(catch_unique_violation("profile"))?; ).await.map_err(catch_unique_violation("profile"))?;
let profile = row.try_get("actor_profile")?; let profile = row.try_get("actor_profile")?;
// Create related objects
create_profile_emojis(
&transaction,
&profile_id,
profile_data.emojis,
).await?;
transaction.commit().await?;
Ok(profile) Ok(profile)
} }
pub async fn update_profile( pub async fn update_profile(
db_client: &impl DatabaseClient, db_client: &mut impl DatabaseClient,
profile_id: &Uuid, profile_id: &Uuid,
data: ProfileUpdateData, profile_data: ProfileUpdateData,
) -> Result<DbActorProfile, DatabaseError> { ) -> Result<DbActorProfile, DatabaseError> {
let maybe_row = db_client.query_opt( let transaction = db_client.transaction().await?;
let maybe_row = transaction.query_opt(
" "
UPDATE actor_profile UPDATE actor_profile
SET SET
@ -93,22 +130,35 @@ pub async fn update_profile(
RETURNING actor_profile RETURNING actor_profile
", ",
&[ &[
&data.display_name, &profile_data.display_name,
&data.bio, &profile_data.bio,
&data.bio_source, &profile_data.bio_source,
&data.avatar, &profile_data.avatar,
&data.banner, &profile_data.banner,
&IdentityProofs(data.identity_proofs), &IdentityProofs(profile_data.identity_proofs),
&PaymentOptions(data.payment_options), &PaymentOptions(profile_data.payment_options),
&ExtraFields(data.extra_fields), &ExtraFields(profile_data.extra_fields),
&data.actor_json, &profile_data.actor_json,
&profile_id, &profile_id,
], ],
).await?; ).await?;
let profile = match maybe_row { let profile: DbActorProfile = match maybe_row {
Some(row) => row.try_get("actor_profile")?, Some(row) => row.try_get("actor_profile")?,
None => return Err(DatabaseError::NotFound("profile")), None => return Err(DatabaseError::NotFound("profile")),
}; };
// Delete and re-create related objects
transaction.execute(
"DELETE FROM profile_emoji WHERE profile_id = $1",
&[&profile.id],
).await?;
create_profile_emojis(
&transaction,
&profile.id,
profile_data.emojis,
).await?;
transaction.commit().await?;
Ok(profile) Ok(profile)
} }
@ -706,8 +756,8 @@ mod tests {
username: "test".to_string(), username: "test".to_string(),
..Default::default() ..Default::default()
}; };
let db_client = create_test_database().await; let db_client = &mut create_test_database().await;
let profile = create_profile(&db_client, profile_data).await.unwrap(); let profile = create_profile(db_client, profile_data).await.unwrap();
assert_eq!(profile.username, "test"); assert_eq!(profile.username, "test");
assert_eq!(profile.hostname, None); assert_eq!(profile.hostname, None);
assert_eq!(profile.acct, "test"); assert_eq!(profile.acct, "test");
@ -725,8 +775,8 @@ mod tests {
actor_json: Some(create_test_actor("https://example.com/users/test")), actor_json: Some(create_test_actor("https://example.com/users/test")),
..Default::default() ..Default::default()
}; };
let db_client = create_test_database().await; let db_client = &mut create_test_database().await;
let profile = create_profile(&db_client, profile_data).await.unwrap(); let profile = create_profile(db_client, profile_data).await.unwrap();
assert_eq!(profile.username, "test"); assert_eq!(profile.username, "test");
assert_eq!(profile.hostname.unwrap(), "example.com"); assert_eq!(profile.hostname.unwrap(), "example.com");
assert_eq!(profile.acct, "test@example.com"); assert_eq!(profile.acct, "test@example.com");
@ -739,7 +789,7 @@ mod tests {
#[tokio::test] #[tokio::test]
#[serial] #[serial]
async fn test_actor_id_unique() { async fn test_actor_id_unique() {
let db_client = create_test_database().await; let db_client = &mut create_test_database().await;
let actor_id = "https://example.com/users/test"; let actor_id = "https://example.com/users/test";
let profile_data_1 = ProfileCreateData { let profile_data_1 = ProfileCreateData {
username: "test-1".to_string(), username: "test-1".to_string(),
@ -747,31 +797,31 @@ mod tests {
actor_json: Some(create_test_actor(actor_id)), actor_json: Some(create_test_actor(actor_id)),
..Default::default() ..Default::default()
}; };
create_profile(&db_client, profile_data_1).await.unwrap(); create_profile(db_client, profile_data_1).await.unwrap();
let profile_data_2 = ProfileCreateData { let profile_data_2 = ProfileCreateData {
username: "test-2".to_string(), username: "test-2".to_string(),
hostname: Some("example.com".to_string()), hostname: Some("example.com".to_string()),
actor_json: Some(create_test_actor(actor_id)), actor_json: Some(create_test_actor(actor_id)),
..Default::default() ..Default::default()
}; };
let error = create_profile(&db_client, profile_data_2).await.err().unwrap(); let error = create_profile(db_client, profile_data_2).await.err().unwrap();
assert_eq!(error.to_string(), "profile already exists"); assert_eq!(error.to_string(), "profile already exists");
} }
#[tokio::test] #[tokio::test]
#[serial] #[serial]
async fn test_update_profile() { async fn test_update_profile() {
let db_client = create_test_database().await; let db_client = &mut create_test_database().await;
let profile_data = ProfileCreateData { let profile_data = ProfileCreateData {
username: "test".to_string(), username: "test".to_string(),
..Default::default() ..Default::default()
}; };
let profile = create_profile(&db_client, profile_data).await.unwrap(); let profile = create_profile(db_client, profile_data).await.unwrap();
let mut profile_data = ProfileUpdateData::from(&profile); let mut profile_data = ProfileUpdateData::from(&profile);
let bio = "test bio"; let bio = "test bio";
profile_data.bio = Some(bio.to_string()); profile_data.bio = Some(bio.to_string());
let profile_updated = update_profile( let profile_updated = update_profile(
&db_client, db_client,
&profile.id, &profile.id,
profile_data, profile_data,
).await.unwrap(); ).await.unwrap();
@ -784,9 +834,9 @@ mod tests {
#[serial] #[serial]
async fn test_delete_profile() { async fn test_delete_profile() {
let profile_data = ProfileCreateData::default(); let profile_data = ProfileCreateData::default();
let mut db_client = create_test_database().await; let db_client = &mut create_test_database().await;
let profile = create_profile(&db_client, profile_data).await.unwrap(); let profile = create_profile(db_client, profile_data).await.unwrap();
let deletion_queue = delete_profile(&mut db_client, &profile.id).await.unwrap(); let deletion_queue = delete_profile(db_client, &profile.id).await.unwrap();
assert_eq!(deletion_queue.files.len(), 0); assert_eq!(deletion_queue.files.len(), 0);
assert_eq!(deletion_queue.ipfs_objects.len(), 0); assert_eq!(deletion_queue.ipfs_objects.len(), 0);
} }
@ -855,7 +905,7 @@ mod tests {
#[tokio::test] #[tokio::test]
#[serial] #[serial]
async fn test_set_reachability_status() { async fn test_set_reachability_status() {
let db_client = &create_test_database().await; let db_client = &mut create_test_database().await;
let actor_id = "https://example.com/users/test"; let actor_id = "https://example.com/users/test";
let profile_data = ProfileCreateData { let profile_data = ProfileCreateData {
username: "test".to_string(), username: "test".to_string(),

View file

@ -439,6 +439,7 @@ pub struct ProfileCreateData {
pub identity_proofs: Vec<IdentityProof>, pub identity_proofs: Vec<IdentityProof>,
pub payment_options: Vec<PaymentOption>, pub payment_options: Vec<PaymentOption>,
pub extra_fields: Vec<ExtraField>, pub extra_fields: Vec<ExtraField>,
pub emojis: Vec<Uuid>,
pub actor_json: Option<Actor>, pub actor_json: Option<Actor>,
} }
@ -470,6 +471,7 @@ pub struct ProfileUpdateData {
pub identity_proofs: Vec<IdentityProof>, pub identity_proofs: Vec<IdentityProof>,
pub payment_options: Vec<PaymentOption>, pub payment_options: Vec<PaymentOption>,
pub extra_fields: Vec<ExtraField>, pub extra_fields: Vec<ExtraField>,
pub emojis: Vec<Uuid>,
pub actor_json: Option<Actor>, pub actor_json: Option<Actor>,
} }
@ -517,6 +519,7 @@ impl From<&DbActorProfile> for ProfileUpdateData {
identity_proofs: profile.identity_proofs.into_inner(), identity_proofs: profile.identity_proofs.into_inner(),
payment_options: profile.payment_options.into_inner(), payment_options: profile.payment_options.into_inner(),
extra_fields: profile.extra_fields.into_inner(), extra_fields: profile.extra_fields.into_inner(),
emojis: vec![],
actor_json: profile.actor_json, actor_json: profile.actor_json,
} }
} }

View file

@ -71,7 +71,7 @@ pub async fn create_user(
db_client: &mut impl DatabaseClient, db_client: &mut impl DatabaseClient,
user_data: UserCreateData, user_data: UserCreateData,
) -> Result<User, DatabaseError> { ) -> Result<User, DatabaseError> {
let transaction = db_client.transaction().await?; let mut transaction = db_client.transaction().await?;
// Prevent changes to actor_profile table // Prevent changes to actor_profile table
transaction.execute( transaction.execute(
"LOCK TABLE actor_profile IN EXCLUSIVE MODE", "LOCK TABLE actor_profile IN EXCLUSIVE MODE",
@ -114,9 +114,10 @@ pub async fn create_user(
identity_proofs: vec![], identity_proofs: vec![],
payment_options: vec![], payment_options: vec![],
extra_fields: vec![], extra_fields: vec![],
emojis: vec![],
actor_json: None, actor_json: None,
}; };
let profile = create_profile(&transaction, profile_data).await?; let profile = create_profile(&mut transaction, profile_data).await?;
// Create user // Create user
let row = transaction.query_one( let row = transaction.query_one(
" "