use super::*; impl PostgresRegistry { pub async fn list_workspaces(&self) -> Result, RegistryError> { let rows = sqlx::query!( "select id, slug, display_name, status, settings_json, created_at as \"created_at!: time::OffsetDateTime\", updated_at as \"updated_at!: time::OffsetDateTime\" from workspaces order by slug asc", ) .fetch_all(&self.pool) .await?; rows.into_iter() .map(|row| { Ok(WorkspaceRecord { workspace: Workspace { id: WorkspaceId::new(row.id), slug: row.slug, display_name: row.display_name, status: deserialize_enum_text(&row.status, "status")?, settings: row.settings_json, created_at: row.created_at, updated_at: row.updated_at, }, }) }) .collect() } pub async fn list_workspaces_for_user( &self, user_id: &UserId, ) -> Result, RegistryError> { let rows = sqlx::query!( "select w.id, w.slug, w.display_name, w.status, w.settings_json, w.created_at as \"created_at!: time::OffsetDateTime\", w.updated_at as \"updated_at!: time::OffsetDateTime\", m.role from memberships m join workspaces w on w.id = m.workspace_id where m.user_id = $1 order by w.slug asc", user_id.as_str(), ) .fetch_all(&self.pool) .await?; rows.into_iter() .map(|row| { Ok(WorkspaceMembershipRecord { workspace: Workspace { id: WorkspaceId::new(row.id), slug: row.slug, display_name: row.display_name, status: deserialize_enum_text(&row.status, "status")?, settings: row.settings_json, created_at: row.created_at, updated_at: row.updated_at, }, role: deserialize_enum_text(&row.role, "role")?, }) }) .collect() } pub async fn list_memberships( &self, workspace_id: &WorkspaceId, ) -> Result, RegistryError> { let rows = sqlx::query!( "select m.workspace_id, m.user_id, m.role, m.created_at as \"created_at!: time::OffsetDateTime\", u.email, u.display_name, u.status, u.created_at as \"user_created_at!: time::OffsetDateTime\" from memberships m join users u on u.id = m.user_id where m.workspace_id = $1 order by u.email asc", workspace_id.as_str(), ) .fetch_all(&self.pool) .await?; rows.into_iter() .map(|row| { Ok(MembershipRecord { workspace_id: WorkspaceId::new(row.workspace_id), user: User { id: UserId::new(row.user_id), email: row.email, display_name: row.display_name, status: deserialize_enum_text(&row.status, "status")?, created_at: row.user_created_at, }, role: deserialize_enum_text(&row.role, "role")?, created_at: row.created_at, }) }) .collect() } pub async fn update_membership_role( &self, workspace_id: &WorkspaceId, user_id: &UserId, role: MembershipRole, ) -> Result<(), RegistryError> { let result = sqlx::query( "update memberships set role = $3 where workspace_id = $1 and user_id = $2", ) .bind(workspace_id.as_str()) .bind(user_id.as_str()) .bind(serialize_enum_text(&role, "role")?) .execute(&self.pool) .await?; if result.rows_affected() == 0 { return Err(RegistryError::MembershipNotFound { workspace_id: workspace_id.as_str().to_owned(), user_id: user_id.as_str().to_owned(), }); } Ok(()) } pub async fn delete_membership( &self, workspace_id: &WorkspaceId, user_id: &UserId, ) -> Result<(), RegistryError> { let result = sqlx::query( "delete from memberships where workspace_id = $1 and user_id = $2", ) .bind(workspace_id.as_str()) .bind(user_id.as_str()) .execute(&self.pool) .await?; if result.rows_affected() == 0 { return Err(RegistryError::MembershipNotFound { workspace_id: workspace_id.as_str().to_owned(), user_id: user_id.as_str().to_owned(), }); } Ok(()) } pub async fn list_invitations( &self, workspace_id: &WorkspaceId, ) -> Result, RegistryError> { let rows = sqlx::query!( "select id, workspace_id, email, role, status, token_hash, expires_at as \"expires_at!: time::OffsetDateTime\", created_at as \"created_at!: time::OffsetDateTime\" from invitation_tokens where workspace_id = $1 order by created_at desc", workspace_id.as_str(), ) .fetch_all(&self.pool) .await?; rows.into_iter() .map(|row| { Ok(InvitationRecord { invitation: InvitationToken { id: InvitationId::new(row.id), workspace_id: WorkspaceId::new(row.workspace_id), email: row.email, role: deserialize_enum_text(&row.role, "role")?, status: deserialize_enum_text(&row.status, "status")?, token_hash: row.token_hash, expires_at: row.expires_at, created_at: row.created_at, }, }) }) .collect() } pub async fn create_invitation( &self, request: CreateInvitationRequest<'_>, ) -> Result<(), RegistryError> { sqlx::query( "insert into invitation_tokens ( id, workspace_id, email, role, status, token_hash, expires_at, created_at ) values ( $1, $2, $3, $4, $5, $6, $7::timestamptz, $8::timestamptz )", ) .bind(request.invitation.id.as_str()) .bind(request.invitation.workspace_id.as_str()) .bind(&request.invitation.email) .bind(serialize_enum_text(&request.invitation.role, "role")?) .bind(serialize_enum_text(&request.invitation.status, "status")?) .bind(&request.invitation.token_hash) .bind(request.invitation.expires_at) .bind(request.invitation.created_at) .execute(&self.pool) .await?; Ok(()) } pub async fn delete_invitation( &self, workspace_id: &WorkspaceId, invitation_id: &InvitationId, ) -> Result<(), RegistryError> { let result = sqlx::query("delete from invitation_tokens where workspace_id = $1 and id = $2") .bind(workspace_id.as_str()) .bind(invitation_id.as_str()) .execute(&self.pool) .await?; if result.rows_affected() == 0 { return Err(RegistryError::InvitationNotFound { invitation_id: invitation_id.as_str().to_owned(), }); } Ok(()) } pub async fn delete_workspace(&self, workspace_id: &WorkspaceId) -> Result<(), RegistryError> { let result = sqlx::query("delete from workspaces where id = $1") .bind(workspace_id.as_str()) .execute(&self.pool) .await?; if result.rows_affected() == 0 { return Err(RegistryError::WorkspaceNotFound { workspace_id: workspace_id.as_str().to_owned(), }); } Ok(()) } pub async fn get_workspace( &self, workspace_id: &WorkspaceId, ) -> Result, RegistryError> { let row = sqlx::query!( "select id, slug, display_name, status, settings_json, created_at as \"created_at!: time::OffsetDateTime\", updated_at as \"updated_at!: time::OffsetDateTime\" from workspaces where id = $1", workspace_id.as_str(), ) .fetch_optional(&self.pool) .await?; row.map(|row| { Ok(WorkspaceRecord { workspace: Workspace { id: WorkspaceId::new(row.id), slug: row.slug, display_name: row.display_name, status: deserialize_enum_text(&row.status, "status")?, settings: row.settings_json, created_at: row.created_at, updated_at: row.updated_at, }, }) }) .transpose() } pub async fn create_workspace( &self, request: CreateWorkspaceRequest<'_>, ) -> Result<(), RegistryError> { let result = sqlx::query( "insert into workspaces ( id, slug, display_name, status, settings_json, created_at, updated_at ) values ( $1, $2, $3, $4, $5, $6::timestamptz, $7::timestamptz )", ) .bind(request.workspace.id.as_str()) .bind(&request.workspace.slug) .bind(&request.workspace.display_name) .bind(serialize_enum_text(&request.workspace.status, "status")?) .bind(Json(request.workspace.settings.clone())) .bind(request.workspace.created_at) .bind(request.workspace.updated_at) .execute(&self.pool) .await; match result { Ok(_) => Ok(()), Err(sqlx::Error::Database(error)) if error.constraint() == Some("workspaces_slug_key") => { Err(RegistryError::WorkspaceSlugAlreadyExists { slug: request.workspace.slug.clone(), }) } Err(error) => Err(RegistryError::Storage(error)), } } pub async fn update_workspace( &self, request: UpdateWorkspaceRequest<'_>, ) -> Result<(), RegistryError> { let result = sqlx::query( "update workspaces set slug = $1, display_name = $2, status = $3, settings_json = $4, updated_at = $5::timestamptz where id = $6", ) .bind(&request.workspace.slug) .bind(&request.workspace.display_name) .bind(serialize_enum_text(&request.workspace.status, "status")?) .bind(Json(request.workspace.settings.clone())) .bind(request.workspace.updated_at) .bind(request.workspace.id.as_str()) .execute(&self.pool) .await; match result { Ok(result) if result.rows_affected() == 0 => Err(RegistryError::WorkspaceNotFound { workspace_id: request.workspace.id.as_str().to_owned(), }), Ok(_) => Ok(()), Err(sqlx::Error::Database(error)) if error.constraint() == Some("workspaces_slug_key") => { Err(RegistryError::WorkspaceSlugAlreadyExists { slug: request.workspace.slug.clone(), }) } Err(error) => Err(RegistryError::Storage(error)), } } }