feat: support publish interfaces for wasm (#654)

* feat: support publish interfaces for wasm

* fix: fmt

* feat: support sign in with url
This commit is contained in:
Kilu.He 2024-07-23 17:47:21 +08:00 committed by GitHub
parent c2a839ba8b
commit 47f87cee1c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 255 additions and 14 deletions

69
.github/workflows/wasm_publish.yml vendored Normal file
View File

@ -0,0 +1,69 @@
name: Manual NPM Package Publish
on:
workflow_dispatch:
inputs:
working_directory:
description: 'Working directory (e.g., libs/client-api-wasm)'
required: true
default: 'libs/client-api-wasm'
package_name:
description: 'Which package to publish'
required: true
default: '@appflowyinc/client-api-wasm'
type: choice
options:
- '@appflowyinc/client-api-wasm'
package_version:
description: 'Package version'
required: true
env:
NODE_VERSION: '20.12.0'
RUST_TOOLCHAIN: "1.77.1"
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
- name: Setup Node.js
uses: actions/setup-node@v1
with:
node-version: ${{ env.NODE_VERSION }}
- uses: Swatinem/rust-cache@v2
with:
workspaces: |
AppFlowy-Cloud
- name: Install wasm-pack
run: cargo install wasm-pack
- name: Build with wasm-pack
run: wasm-pack build --release
working-directory: ${{ github.event.inputs.working_directory }}
- name: Update name
working-directory: ${{ github.event.inputs.working_directory }}/pkg
run: |
jq '.name = "${{ github.event.inputs.package_name }}"' package.json > package.json.tmp
mv package.json.tmp package.json
- name: Update version
working-directory: ${{ github.event.inputs.working_directory }}/pkg
run: |
npm version ${{ github.event.inputs.package_version }}
- name: Configure npm for wasm-pack
run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ${{ github.event.inputs.working_directory }}/pkg/.npmrc
- name: Publish package
run: |
npm config set access public
wasm-pack publish
working-directory: ${{ github.event.inputs.working_directory }}/pkg
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@ -8,18 +8,18 @@ edition = "2018"
crate-type = ["cdylib", "rlib"]
[dependencies]
wasm-bindgen = "0.2.84"
wasm-bindgen = "0.2.90"
# The `console_error_panic_hook` crate provides better debugging of panics by
# logging them with `console.error`. This is great for development, but requires
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
# code size when deploying.
console_error_panic_hook = { version = "0.1.7", optional = true }
serde = { version = "1.0.197", features = ["derive"] }
serde_json = "1.0.64"
console_error_panic_hook = { version = "0.1.7" }
serde.workspace = true
serde_json.workspace = true
client-api = { path = "../client-api" }
lazy_static = "1.4.0"
wasm-bindgen-futures = "0.4.20"
wasm-bindgen-futures = "0.4.40"
tsify = "0.4.5"
tracing.workspace = true
bytes.workspace = true
@ -29,12 +29,11 @@ uuid.workspace = true
database-entity.workspace = true
collab-rt-entity.workspace = true
collab-entity.workspace = true
serde_repr = "0.1.18"
wee_alloc = { version = "0.4.5", optional = true }
serde_repr = "0.1.19"
wee_alloc = { version = "0.4.5" }
serde-wasm-bindgen = "0.6.5"
[dev-dependencies]
wasm-bindgen-test = "0.3.34"
[features]
default = []
enable_wee_alloc = ["wee_alloc"]

View File

@ -1,4 +1,4 @@
use client_api::entity::AFUserProfile;
use client_api::entity::{AFUserProfile, AuthProvider};
use client_api::error::{AppResponseError, ErrorCode};
use collab_entity::{CollabType, EncodedCollab};
use database_entity::dto::{
@ -237,3 +237,43 @@ impl From<BatchQueryCollabResult> for BatchClientEncodeCollab {
BatchClientEncodeCollab(hash_map)
}
}
#[derive(Tsify, Serialize, Deserialize, Default, Debug)]
#[tsify(into_wasm_abi, from_wasm_abi)]
pub struct PublishViewMeta {
pub data: String,
}
#[derive(Tsify, Serialize, Deserialize, Default, Debug)]
#[tsify(into_wasm_abi, from_wasm_abi)]
pub struct PublishViewPayload {
pub meta: PublishViewMeta,
/// The doc_state of the encoded collab.
pub data: Vec<u8>,
}
#[derive(Tsify, Serialize, Deserialize, Default, Debug)]
#[tsify(into_wasm_abi, from_wasm_abi)]
pub struct PublishInfo {
pub namespace: Option<String>,
pub publish_name: String,
}
from_struct_for_jsvalue!(PublishViewMeta);
from_struct_for_jsvalue!(PublishViewPayload);
from_struct_for_jsvalue!(PublishInfo);
pub fn parse_provider(provider: &str) -> AuthProvider {
match provider {
"google" => AuthProvider::Google,
"github" => AuthProvider::Github,
"discord" => AuthProvider::Discord,
_ => AuthProvider::Google,
}
}
#[derive(Tsify, Serialize, Deserialize, Default, Debug)]
#[tsify(into_wasm_abi, from_wasm_abi)]
pub struct OAuthURLResponse {
pub url: String,
}
from_struct_for_jsvalue!(OAuthURLResponse);

View File

@ -5,11 +5,13 @@ use crate::entities::*;
use client_api::notify::TokenState;
use client_api::{Client, ClientConfiguration};
use std::sync::Arc;
use uuid::Uuid;
use client_api::error::ErrorCode;
use console_error_panic_hook;
use database_entity::dto::QueryCollab;
use wasm_bindgen::prelude::*;
#[cfg(feature = "enable_wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
@ -49,6 +51,7 @@ pub struct ClientAPI {
impl ClientAPI {
pub fn new(config: ClientAPIConfig) -> ClientAPI {
tracing_wasm::set_as_global_default();
console_error_panic_hook::set_once();
let configuration = ClientConfiguration::default();
if let Some(compression) = &config.configuration {
@ -115,6 +118,52 @@ impl ClientAPI {
}
}
pub async fn sign_in_with_magic_link(
&self,
email: &str,
redirect_to: &str,
) -> Result<(), ClientResponse> {
match self
.client
.sign_in_with_magic_link(email, Some(redirect_to.to_string()))
.await
{
Ok(_) => Ok(()),
Err(err) => Err(ClientResponse::from(err)),
}
}
pub async fn generate_oauth_url_with_provider(
&self,
provider: &str,
redirect_to: &str,
) -> Result<OAuthURLResponse, ClientResponse> {
if provider.is_empty() {
return Err(ClientResponse {
code: ErrorCode::OAuthError,
message: "Provider is empty".to_string(),
});
}
let provider = parse_provider(provider);
match self
.client
.generate_url_with_provider_and_redirect_to(&provider, Some(redirect_to.to_string()))
.await
{
Ok(url) => Ok(OAuthURLResponse { url }),
Err(err) => Err(ClientResponse::from(err)),
}
}
pub async fn sign_in_with_url(&self, url: &str) -> Result<(), ClientResponse> {
match self.client.sign_in_with_url(url).await {
Ok(_) => Ok(()),
Err(err) => Err(ClientResponse::from(err)),
}
}
pub async fn get_user(&self) -> Result<User, ClientResponse> {
match self.client.get_profile().await {
Ok(profile) => Ok(User::from(profile)),
@ -160,4 +209,57 @@ impl ClientAPI {
Err(err) => Err(ClientResponse::from(err)),
}
}
pub async fn get_publish_view_meta(
&self,
publish_namespace: String,
publish_name: String,
) -> Result<PublishViewMeta, ClientResponse> {
match self
.client
.get_published_collab::<serde_json::Value>(publish_namespace.as_str(), publish_name.as_str())
.await
{
Ok(data) => Ok(PublishViewMeta {
data: data.to_string(),
}),
Err(err) => Err(ClientResponse::from(err)),
}
}
pub async fn get_publish_view(
&self,
publish_namespace: String,
publish_name: String,
) -> Result<PublishViewPayload, ClientResponse> {
let meta = self
.get_publish_view_meta(publish_namespace.clone(), publish_name.clone())
.await?;
let data = self
.client
.get_published_collab_blob(publish_namespace.as_str(), publish_name.as_str())
.await
.map_err(ClientResponse::from)?;
Ok(PublishViewPayload {
meta,
data: data.to_vec(),
})
}
pub async fn get_publish_info(&self, view_id: String) -> Result<PublishInfo, ClientResponse> {
let view_id = Uuid::parse_str(view_id.as_str()).map_err(|err| ClientResponse {
code: ErrorCode::UuidError,
message: format!("Failed to parse view_id: {}", err),
})?;
match self.client.get_published_collab_info(&view_id).await {
Ok(info) => Ok(PublishInfo {
namespace: info.namespace,
publish_name: info.publish_name,
}),
Err(err) => Err(ClientResponse::from(err)),
}
}
}

View File

@ -293,6 +293,17 @@ impl Client {
}
/// Returns an OAuth URL by constructing the authorization URL for the specified provider.
#[instrument(level = "debug", skip_all, err)]
pub async fn generate_oauth_url_with_provider(
&self,
provider: &AuthProvider,
) -> Result<String, AppResponseError> {
self
.generate_url_with_provider_and_redirect_to(provider, None)
.await
}
/// Returns an OAuth URL by constructing the authorization URL for the specified provider and redirecting to the specified URL.
///
/// This asynchronous function communicates with the GoTrue client to retrieve settings and
/// validate the availability of the specified OAuth provider. If the provider is available,
@ -303,19 +314,19 @@ impl Client {
/// For example, the OAuth URL on Google looks like `https://appflowy.io/authorize?provider=google`.
/// The deep link looks like `appflowy-flutter://#access_token=...&expires_in=3600&provider_token=...&refresh_token=...&token_type=bearer`.
///
/// The appflowy-flutter:// is a hardcoded schema in the frontend application
///
/// # Parameters
/// - `provider`: A reference to an `OAuthProvider` indicating which OAuth provider to use for login.
/// - `redirect_to`: An optional `String` containing the URL to redirect to after the user is authenticated.
///
/// # Returns
/// - `Ok(String)`: A `String` containing the constructed authorization URL if the specified provider is available.
/// - `Err(AppResponseError)`: An `AppResponseError` indicating either the OAuth provider is invalid or other issues occurred while fetching settings.
///
#[instrument(level = "debug", skip_all, err)]
pub async fn generate_oauth_url_with_provider(
pub async fn generate_url_with_provider_and_redirect_to(
&self,
provider: &AuthProvider,
redirect_to: Option<String>,
) -> Result<String, AppResponseError> {
let settings = self.gotrue_client.settings().await?;
if !settings.external.has_provider(provider) {
@ -328,7 +339,12 @@ impl Client {
url
.query_pairs_mut()
.append_pair("provider", provider.as_str())
.append_pair("redirect_to", DESKTOP_CALLBACK_URL);
.append_pair(
"redirect_to",
redirect_to
.unwrap_or_else(|| DESKTOP_CALLBACK_URL.to_string())
.as_str(),
);
if let AuthProvider::Google = provider {
url

View File

@ -2,6 +2,7 @@ use bytes::Bytes;
use client_api_entity::{PublishInfo, UpdatePublishNamespace};
use reqwest::Method;
use shared_entity::response::{AppResponse, AppResponseError};
use tracing::instrument;
use crate::Client;
@ -65,6 +66,7 @@ impl Client {
// Guest API (no login required)
impl Client {
#[instrument(level = "debug", skip_all)]
pub async fn get_published_collab_info(
&self,
view_id: &uuid::Uuid,
@ -77,6 +79,7 @@ impl Client {
.into_data()
}
#[instrument(level = "debug", skip_all)]
pub async fn get_published_collab<T>(
&self,
publish_namespace: &str,
@ -85,6 +88,11 @@ impl Client {
where
T: serde::de::DeserializeOwned,
{
tracing::debug!(
"get_published_collab: {} {}",
publish_namespace,
publish_name
);
let url = format!(
"{}/api/workspace/published/{}/{}",
self.base_url, publish_namespace, publish_name
@ -104,14 +112,21 @@ impl Client {
}
let meta = serde_json::from_str::<T>(&txt)?;
Ok(meta)
}
#[instrument(level = "debug", skip_all)]
pub async fn get_published_collab_blob(
&self,
publish_namespace: &str,
publish_name: &str,
) -> Result<Bytes, AppResponseError> {
tracing::debug!(
"get_published_collab_blob: {} {}",
publish_namespace,
publish_name
);
let url = format!(
"{}/api/workspace/published/{}/{}/blob",
self.base_url, publish_namespace, publish_name