Appflowy学习系列-开发环境准备

796 阅读5分钟

开发环境准备

安装文档在项目README.md中,请前往查看最新版

知识准备

Postgres基础学习

www.postgres.cn/docs/14/tut…

Android开发者眼里的Docker技术

Docker是一个后端开发必备的概念了,容器化技术专业名词很多,笔者了解的也不多,单尝试从Android概念去模糊映射这些概念。

DockerAndroid
镜像 (Image)Apk
容器 (Container)安装到设备里的目录
仓库 (Repository)GooglePlay
DockerfileGradle DSL + AndroidManifest
Docker EngineART(Android Runtime)
Docker ComposeGradle

概念映射之后,我们可能需要一些小的联系,熟悉一下这个概念。

Step By Step

  1. 安装Docker (Macos Docker需要启动状态)
  2. 安装Rust
  3. cp dev.env .env
  4. 修改配置(可选)
  5. ./script/run_local_server.sh
    • 如果遇到postgres的问题,可以尝试删除本地postgres数据库
    • Email等配置信息建议填写
  6. 运行成功后,会有八个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部分有了一个大致的了解,这是一个好的开始。