//! Collaboration: areas get members with roles, people can be invited into
//! areas (existing users accept a pending invitation; sign-up invites carry
//! area grants applied on registration), and users can friend each other.
//!
//! Areas become members-only, so an OSM place is now unique per area rather
//! than globally — two private areas can both list the same Dunkin' — and
//! area names stop being globally unique: someone else's private "Boston"
//! mustn't block (or reveal itself to) yours.
//!
//! Existing data: each area's creator becomes its admin and every existing
//! user an editor of every existing area (admin, where the area has no
//! recorded creator), so nobody loses access to anything on upgrade.
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
fn created_at<T: IntoIden>(col: T) -> ColumnDef {
ColumnDef::new(col).timestamp_with_time_zone().not_null().default(Expr::current_timestamp()).to_owned()
}
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(AreaMembers::Table)
.if_not_exists()
.col(ColumnDef::new(AreaMembers::AreaId).uuid().not_null())
.col(ColumnDef::new(AreaMembers::UserId).uuid().not_null())
// voter | editor | admin
.col(ColumnDef::new(AreaMembers::Role).string_len(16).not_null())
.col(created_at(AreaMembers::CreatedAt))
.primary_key(Index::create().col(AreaMembers::AreaId).col(AreaMembers::UserId))
.foreign_key(
ForeignKey::create()
.name("fk_area_members_area_id")
.from(AreaMembers::Table, AreaMembers::AreaId)
.to(Areas::Table, Areas::Id)
.on_delete(ForeignKeyAction::Cascade),
)
.foreign_key(
ForeignKey::create()
.name("fk_area_members_user_id")
.from(AreaMembers::Table, AreaMembers::UserId)
.to(Users::Table, Users::Id)
.on_delete(ForeignKeyAction::Cascade),
)
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.name("idx_area_members_user_id")
.table(AreaMembers::Table)
.col(AreaMembers::UserId)
.to_owned(),
)
.await?;
// An existing user invited into an area, until they accept or decline.
manager
.create_table(
Table::create()
.table(AreaInvitations::Table)
.if_not_exists()
.col(ColumnDef::new(AreaInvitations::AreaId).uuid().not_null())
.col(ColumnDef::new(AreaInvitations::UserId).uuid().not_null())
.col(ColumnDef::new(AreaInvitations::Role).string_len(16).not_null())
.col(ColumnDef::new(AreaInvitations::InvitedBy).uuid().null())
.col(created_at(AreaInvitations::CreatedAt))
.primary_key(Index::create().col(AreaInvitations::AreaId).col(AreaInvitations::UserId))
.foreign_key(
ForeignKey::create()
.name("fk_area_invitations_area_id")
.from(AreaInvitations::Table, AreaInvitations::AreaId)
.to(Areas::Table, Areas::Id)
.on_delete(ForeignKeyAction::Cascade),
)
.foreign_key(
ForeignKey::create()
.name("fk_area_invitations_user_id")
.from(AreaInvitations::Table, AreaInvitations::UserId)
.to(Users::Table, Users::Id)
.on_delete(ForeignKeyAction::Cascade),
)
.foreign_key(
ForeignKey::create()
.name("fk_area_invitations_invited_by")
.from(AreaInvitations::Table, AreaInvitations::InvitedBy)
.to(Users::Table, Users::Id)
.on_delete(ForeignKeyAction::SetNull),
)
.to_owned(),
)
.await?;
// Areas a sign-up invite grants, applied when it's used.
manager
.create_table(
Table::create()
.table(RegistrationTokenAreas::Table)
.if_not_exists()
.col(ColumnDef::new(RegistrationTokenAreas::TokenId).uuid().not_null())
.col(ColumnDef::new(RegistrationTokenAreas::AreaId).uuid().not_null())
.col(ColumnDef::new(RegistrationTokenAreas::Role).string_len(16).not_null())
.primary_key(
Index::create()
.col(RegistrationTokenAreas::TokenId)
.col(RegistrationTokenAreas::AreaId),
)
.foreign_key(
ForeignKey::create()
.name("fk_registration_token_areas_token_id")
.from(RegistrationTokenAreas::Table, RegistrationTokenAreas::TokenId)
.to(RegistrationTokens::Table, RegistrationTokens::Id)
.on_delete(ForeignKeyAction::Cascade),
)
.foreign_key(
ForeignKey::create()
.name("fk_registration_token_areas_area_id")
.from(RegistrationTokenAreas::Table, RegistrationTokenAreas::AreaId)
.to(Areas::Table, Areas::Id)
.on_delete(ForeignKeyAction::Cascade),
)
.to_owned(),
)
.await?;
// One row per pair, in the direction it was asked. Friends are the
// accepted rows with you on either side.
manager
.create_table(
Table::create()
.table(Friendships::Table)
.if_not_exists()
.col(ColumnDef::new(Friendships::RequesterId).uuid().not_null())
.col(ColumnDef::new(Friendships::AddresseeId).uuid().not_null())
// pending | accepted
.col(ColumnDef::new(Friendships::Status).string_len(16).not_null())
.col(created_at(Friendships::CreatedAt))
.col(ColumnDef::new(Friendships::AcceptedAt).timestamp_with_time_zone().null())
.primary_key(Index::create().col(Friendships::RequesterId).col(Friendships::AddresseeId))
.foreign_key(
ForeignKey::create()
.name("fk_friendships_requester_id")
.from(Friendships::Table, Friendships::RequesterId)
.to(Users::Table, Users::Id)
.on_delete(ForeignKeyAction::Cascade),
)
.foreign_key(
ForeignKey::create()
.name("fk_friendships_addressee_id")
.from(Friendships::Table, Friendships::AddresseeId)
.to(Users::Table, Users::Id)
.on_delete(ForeignKeyAction::Cascade),
)
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.name("idx_friendships_addressee_id")
.table(Friendships::Table)
.col(Friendships::AddresseeId)
.to_owned(),
)
.await?;
// OSM places: unique per area, not globally.
manager
.drop_index(Index::drop().name("idx_restaurants_osm_ref").table(Restaurants::Table).to_owned())
.await?;
manager
.create_index(
Index::create()
.name("idx_restaurants_area_osm_ref")
.table(Restaurants::Table)
.col(Restaurants::AreaId)
.col(Restaurants::OsmRef)
.unique()
.to_owned(),
)
.await?;
let db = manager.get_connection();
db.execute_unprepared("ALTER TABLE areas DROP CONSTRAINT IF EXISTS areas_name_key").await?;
db.execute_unprepared(
"INSERT INTO area_members (area_id, user_id, role)
SELECT id, created_by, 'admin' FROM areas WHERE created_by IS NOT NULL
ON CONFLICT DO NOTHING",
)
.await?;
db.execute_unprepared(
"INSERT INTO area_members (area_id, user_id, role)
SELECT a.id, u.id, CASE WHEN a.created_by IS NULL THEN 'admin' ELSE 'editor' END
FROM areas a CROSS JOIN users u
ON CONFLICT DO NOTHING",
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.get_connection()
.execute_unprepared("ALTER TABLE areas ADD CONSTRAINT areas_name_key UNIQUE (name)")
.await?;
manager
.drop_index(Index::drop().name("idx_restaurants_area_osm_ref").table(Restaurants::Table).to_owned())
.await?;
// Fails if two areas now share a place; that's the honest outcome.
manager
.create_index(
Index::create()
.name("idx_restaurants_osm_ref")
.table(Restaurants::Table)
.col(Restaurants::OsmRef)
.unique()
.to_owned(),
)
.await?;
for table in [
Friendships::Table.into_iden(),
RegistrationTokenAreas::Table.into_iden(),
AreaInvitations::Table.into_iden(),
AreaMembers::Table.into_iden(),
] {
manager.drop_table(Table::drop().table(table).to_owned()).await?;
}
Ok(())
}
}
#[derive(DeriveIden)]
enum Users {
Table,
Id,
}
#[derive(DeriveIden)]
enum Areas {
Table,
Id,
}
#[derive(DeriveIden)]
enum Restaurants {
Table,
AreaId,
OsmRef,
}
#[derive(DeriveIden)]
enum RegistrationTokens {
Table,
Id,
}
#[derive(DeriveIden)]
enum AreaMembers {
Table,
AreaId,
UserId,
Role,
CreatedAt,
}
#[derive(DeriveIden)]
enum AreaInvitations {
Table,
AreaId,
UserId,
Role,
InvitedBy,
CreatedAt,
}
#[derive(DeriveIden)]
enum RegistrationTokenAreas {
Table,
TokenId,
AreaId,
Role,
}
#[derive(DeriveIden)]
enum Friendships {
Table,
RequesterId,
AddresseeId,
Status,
CreatedAt,
AcceptedAt,
}
neiam
/fieri-forever
the flavortown lunch engine
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
Log in to mark this repository.
11.2 KiB · text
History
7a00c02