开发环境准备
安装文档在项目README.md中,请前往查看最新版
知识准备
Postgres基础学习
Android开发者眼里的Docker技术
Docker是一个后端开发必备的概念了,容器化技术专业名词很多,笔者了解的也不多,单尝试从Android概念去模糊映射这些概念。
| Docker | Android |
|---|---|
| 镜像 (Image) | Apk |
| 容器 (Container) | 安装到设备里的目录 |
| 仓库 (Repository) | GooglePlay |
| Dockerfile | Gradle DSL + AndroidManifest |
| Docker Engine | ART(Android Runtime) |
| Docker Compose | Gradle |
概念映射之后,我们可能需要一些小的联系,熟悉一下这个概念。
Step By Step
- 安装Docker (Macos Docker需要启动状态)
- 安装Rust
- cp dev.env .env
- 修改配置(可选)
- ./script/run_local_server.sh
- 如果遇到postgres的问题,可以尝试删除本地postgres数据库
- Email等配置信息建议填写
- 运行成功后,会有八个Container(App)在Appflowy-Cloud组下
入口
我们运行起Docker和Appflowy-Cloud服务之后,就不会出现error communicating with database: Connection refused (os error 61)这种问题了。
我们从toml文件中得知入口是 src/main.rs.
入口函数很简单
/// 环境变量
// Load environment variables from .env file
dotenvy::dotenv().ok();
let conf =
get_configuration().map_err(|e| anyhow::anyhow!("Failed to read configuration: {}", e))?;
//设置日志
init_subscriber(&conf.app_env, filters);
// 设置 Channel 1000为缓冲区
let (tx, rx) = tokio::sync::mpsc::channel(1000);
// 初始化App共享状态, 这里传入了tx
let state = init_state(&conf, tx)
.await
.map_err(|e| anyhow::anyhow!("Failed to initialize application state: {}", e))?;
// 构造App, 这里传入了rx
let application = Application::build(conf, state, rx).await?;
// 运行App
application.run_until_stopped().await?;
其中Application代表了我们整个App的根结点, build方法构造了这个实例,如果你感到不熟悉,你可能需要补充一点web知识。
- AppState结构太复杂,我们暂时略过
-> Application:build
/// 这三行是固定操作,绑定TcpListener
let address = format!("{}:{}", config.application.host, config.application.port);
let listener = TcpListener::bind(&address)?;
let port = listener.local_addr().unwrap().port();
tracing::info!("Server started at {}", listener.local_addr().unwrap());
let actix_server = run_actix_server(listener, state, config, rt_cmd_recv).await?;
Ok(Self { port, actix_server })
-> Application:run_actix_server
/// 删减了重复的内容
pub async fn run_actix_server(
listener: TcpListener,
state: AppState,
config: Config,
rt_cmd_recv: CLCommandReceiver,
) -> Result<Server, anyhow::Error> {
// redis服务
let redis_store = RedisSessionStore::new(config.redis_uri.expose_secret());
// 钥匙串
let pair = get_certificate_and_server_key(&config);
let key = xxx
// 存储
let storage = state.collab_access_control_storage.clone();
// 访问控制中间件
let access_control = MiddlewareAccessControlTransform::new();
// 实时服务
// Initialize metrics that which are registered in the registry.
let realtime_server = CollaborationServer::<_, _>::new();
// Supervisor 是Actor中管理其他其他Actors的作用,检测健康并自动失败重启
let realtime_server_actor = Supervisor::start(|_| RealtimeServerActor(realtime_server));
let mut server = HttpServer::new(move || {
App::new()
.wrap(NormalizePath::trim())
// Middleware is registered for each App, scope, or Resource and executed in opposite order as registration
.wrap(MetricsMiddleware) // 监控
.wrap(IdentityMiddleware::default()) // 身份认证
.wrap(SessionMiddleware::builder(redis_store.clone(), key.clone()).build(),) // 会话信息
// .wrap(DecryptPayloadMiddleware)
.wrap(access_control.clone()) // 访问控制
.wrap(RequestIdMiddleware) // RequestId检查
.app_data(web::JsonConfig::default().limit(5 * 1024 * 1024)) // Json限制
.service(user_scope()) // 路由服务信息
.app_data(Data::new(state.metrics.registry.clone())) // 监控信息
.app_data(Data::new(state.clone())) // 注入 AppState
.app_data(Data::new(storage.clone())) // 注入 CollabAccessControlStorage
});
server = match pair {
None => server.listen(listener)?, // 不带证书
Some((certificate, _)) => {
server.listen_openssl(listener, make_ssl_acceptor_builder(certificate))? // 带证书的Https
},
};
Ok(server.run()) // 启动Server返回Future
}
其中.service(user_scope())就是我们需要处理的 各种 Api的入口,我们以user_scope举例。
pub fn user_scope() -> Scope {
web::scope("/api/user")
.service(web::resource("/verify/{access_token}").route(web::get().to(verify_user_handler)))
.service(web::resource("/update").route(web::post().to(update_user_handler)))
.service(web::resource("/profile").route(web::get().to(get_user_profile_handler)))
.service(web::resource("/workspace").route(web::get().to(get_user_workspace_info_handler)))
}
在Web开发领域,一切都可以被抽象为资源(resource),类似于Linux中,一切皆文件(File), Route 是指声明对资源的操作,比如Get, Post, Delete等操作。Url和操作是1..n. 到了route就到了我们正常业务逻辑的地方了,这个层面与我们Flutter或者移动端开发是极其相似的。我们先进入较为简单的/profile。然后在浏览其他复杂的。
profile
#[tracing::instrument(skip(state, path), err)]
async fn verify_user_handler(
path: web::Path<String>,
state: Data<AppState>,
) -> Result<JsonAppResponse<SignInTokenResponse>> {
let access_token = path.into_inner(); // 得到token
let is_new = verify_token(&access_token, state.as_ref()) // 验证Token
.await
.map_err(AppResponseError::from)?;
let resp = SignInTokenResponse { is_new }; // 构造返回Struct
Ok(AppResponse::Ok().with_data(resp).into()) // AppResponse是我我们自己的Struct,into会转换为JsonAppResponse
}
/// 转换被放在shared-entity
pub type JsonAppResponse<T> = Json<AppResponse<T>>;
impl<T> From<AppResponse<T>> for JsonAppResponse<T> {
fn from(data: AppResponse<T>) -> Self {
actix_web::web::Json(data)
}
}
/verify/{access_token}路由的写法和Flutter写法类似,Flutter中AutoRoute也会有/products/:id, 原理上都是类似的,path: web::Path<String>和state都会被注入到这个方法,这很酷,依赖注入应用于方法,虽然我确定用了什么技巧,但是应该和FromRequest有关系。以后再说。我们重点集中在verify_token, 我们要通过他了解一下关于数据库查询的方式。
verify_token太长了,我们做一下删减,保留主流程
#[instrument(skip_all, err)]
pub async fn verify_token(access_token: &str, state: &AppState) -> Result<bool, AppError> {
let user = state.gotrue_client.user_info(access_token).await?; // gotrue_client根据token换User
let user_uuid = uuid::Uuid::parse_str(&user.id)?; // String转换为UUID
let name = name_from_user_metadata(&user.user_metadata); // meta中的name
let is_new = !is_user_exist(&state.pg_pool, &user_uuid).await?; // 查本地库,用户是否存在
if !is_new { return Ok(false); } // 返回,标识为旧用户
/// 得到 数据库执行的Transation
let mut txn = state
.pg_pool
.begin()
.await
.context("acquire transaction to verify token")?;
// 得到等价的i64的uuid
let lock_key = user_uuid.as_u128() as i64;
sqlx::query!("SELECT pg_advisory_xact_lock($1)", lock_key) // advisory lock(咨询锁),
.execute(txn.deref_mut()) // 给一个可变的Transation
.await?;
let is_new = !is_user_exist(txn.deref_mut(), &user_uuid).await?; // 这个方法有两个值得关注的点,1. query_scalar 2. Uuid 可以被解析为“整型”
if is_new { // 新用户逻辑
let new_uid = state.id_gen.write().await.next_id(); // 生成一个uid
event!(tracing::Level::INFO, "create new user:{}", new_uid); // tracing event
// 创建 user和user相关初始化信息
let workspace_id = create_user(txn.deref_mut(), new_uid, &user_uuid, &user.email, &name).await?;
// AFWorkspaceRow 代表af_workspace table的一行
let workspace_row = select_workspace(txn.deref_mut(), &workspace_id).await?;
// 上述的代码被封状态libs/database/ 可以确定database这个crate封装了数据库相关操作
// It's essential to cache the user's role because subsequent actions will rely on this cached information. 这里将角色信息存储到状态中。
state
.workspace_access_control
.insert_role(&new_uid, &workspace_id, AFRole::Owner) // AFRole被放在Dto中,代表用于交互,但AFWorkspaceRow确是在entity包下,是实体类型。
.await?;
// Create a workspace with the GetStarted template
// 以Getstart 模版给用户初始化一个工作空间
initialize_workspace_for_user(
new_uid,
&workspace_row,
&mut txn,
vec![GetStartedDocumentTemplate],
&state.collab_access_control_storage,
)
.await?;
} else {
trace!("user already exists:{},{}", user.id, user.email);
}
/// 执行事务操作
txn
.commit()
.await
.context("fail to commit transaction to verify token")?; // 用在链式调用中,补充错误上下文,容易定位问题
Ok(is_new) // 返回是否是新用户
}
上述代码在/biz, 我们是我们具体业务逻辑的组合,我们从/api流转到此,我们的操作最终都需要转换为持久化数据,也就是数据库或文件,create_user这个函数就被定义在/libs/database中,我们继续看database在整个流程中的作用。
/// Attempts to create a new user in the database if they do not already exist.
///
/// This function will:
/// - Insert a new user record into the `af_user` table if the email is unique.
/// - If the user is newly created, it will also:
/// - Create a new workspace for the user in the `af_workspace` table.
/// - Assign the user a role in the `af_workspace_member` table.
/// - Add the user to the `af_collab_member` table with the appropriate permissions.
///
/// # Returns
/// A `Result` containing the workspace_id of the user's newly created workspace
#[instrument(skip(executor), err)]
#[inline]
pub async fn create_user<'a, E: Executor<'a, Database = Postgres>>(
executor: E,
uid: i64,
user_uuid: &Uuid,
email: &str,
name: &str,
) -> Result<Uuid, AppError> {
let name = {
if name.is_empty() {
email
} else {
name
}
};
let row = sqlx::query!(
r#"
WITH ins_user AS (
INSERT INTO af_user (uid, uuid, email, name)
VALUES ($1, $2, $3, $4)
ON CONFLICT(email) DO NOTHING
RETURNING uid
),
owner_role AS (
SELECT id FROM af_roles WHERE name = 'Owner'
),
ins_workspace AS (
INSERT INTO af_workspace (owner_uid)
SELECT uid FROM ins_user # 这个值作为 owner_uid
RETURNING workspace_id, owner_uid # 返回 workspace_id 和 owner_uid
),
ins_collab_member AS (
INSERT INTO af_collab_member (uid, oid, permission_id) # 这三个值待填充
SELECT ins_workspace.owner_uid, # 填充 1
ins_workspace.workspace_id::TEXT, # 填充 2
(SELECT permission_id FROM af_role_permissions WHERE role_id = owner_role.id) # 填充3
FROM ins_workspace, owner_role
)
SELECT workspace_id FROM ins_workspace; # 从 ins_workspace 中查询 workspace_id
"#,
uid,
user_uuid,
email,
name
)
.fetch_one(executor)
.await?;
Ok(row.workspace_id)
}
内部逻辑十分清晰,如果用户为空,则将email作为用户名,将用户信息插入af_user表(email唯一),从af_roles中查到角色为Owner的id。 新增 af_workspace 中的一条记录,将 af_collab_member 加入新User并赋予权限。
Sql真的是一门非常厉害的语言,作为移动的开发,对这一块甚是陌生,这也是前端开者,走向全栈开发的一大难处。后续我将补充这一课题相关知识储备。
小结
目前,我们已经算是走过了一个完整的Api 调用过程,虽然还有很多细节我们并不清楚,但,我们对整个项目的api部分有了一个大致的了解,这是一个好的开始。