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:
parent
c2a839ba8b
commit
47f87cee1c
|
|
@ -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 }}
|
||||||
|
|
@ -8,18 +8,18 @@ edition = "2018"
|
||||||
crate-type = ["cdylib", "rlib"]
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
wasm-bindgen = "0.2.84"
|
wasm-bindgen = "0.2.90"
|
||||||
|
|
||||||
# The `console_error_panic_hook` crate provides better debugging of panics by
|
# The `console_error_panic_hook` crate provides better debugging of panics by
|
||||||
# logging them with `console.error`. This is great for development, but requires
|
# 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
|
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
|
||||||
# code size when deploying.
|
# code size when deploying.
|
||||||
console_error_panic_hook = { version = "0.1.7", optional = true }
|
console_error_panic_hook = { version = "0.1.7" }
|
||||||
serde = { version = "1.0.197", features = ["derive"] }
|
serde.workspace = true
|
||||||
serde_json = "1.0.64"
|
serde_json.workspace = true
|
||||||
client-api = { path = "../client-api" }
|
client-api = { path = "../client-api" }
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
wasm-bindgen-futures = "0.4.20"
|
wasm-bindgen-futures = "0.4.40"
|
||||||
tsify = "0.4.5"
|
tsify = "0.4.5"
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
bytes.workspace = true
|
bytes.workspace = true
|
||||||
|
|
@ -29,12 +29,11 @@ uuid.workspace = true
|
||||||
database-entity.workspace = true
|
database-entity.workspace = true
|
||||||
collab-rt-entity.workspace = true
|
collab-rt-entity.workspace = true
|
||||||
collab-entity.workspace = true
|
collab-entity.workspace = true
|
||||||
serde_repr = "0.1.18"
|
serde_repr = "0.1.19"
|
||||||
wee_alloc = { version = "0.4.5", optional = true }
|
wee_alloc = { version = "0.4.5" }
|
||||||
serde-wasm-bindgen = "0.6.5"
|
serde-wasm-bindgen = "0.6.5"
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
wasm-bindgen-test = "0.3.34"
|
wasm-bindgen-test = "0.3.34"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
enable_wee_alloc = ["wee_alloc"]
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use client_api::entity::AFUserProfile;
|
use client_api::entity::{AFUserProfile, AuthProvider};
|
||||||
use client_api::error::{AppResponseError, ErrorCode};
|
use client_api::error::{AppResponseError, ErrorCode};
|
||||||
use collab_entity::{CollabType, EncodedCollab};
|
use collab_entity::{CollabType, EncodedCollab};
|
||||||
use database_entity::dto::{
|
use database_entity::dto::{
|
||||||
|
|
@ -237,3 +237,43 @@ impl From<BatchQueryCollabResult> for BatchClientEncodeCollab {
|
||||||
BatchClientEncodeCollab(hash_map)
|
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);
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,13 @@ use crate::entities::*;
|
||||||
use client_api::notify::TokenState;
|
use client_api::notify::TokenState;
|
||||||
use client_api::{Client, ClientConfiguration};
|
use client_api::{Client, ClientConfiguration};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use client_api::error::ErrorCode;
|
||||||
|
use console_error_panic_hook;
|
||||||
use database_entity::dto::QueryCollab;
|
use database_entity::dto::QueryCollab;
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
#[cfg(feature = "enable_wee_alloc")]
|
|
||||||
#[global_allocator]
|
#[global_allocator]
|
||||||
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
|
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
|
||||||
|
|
||||||
|
|
@ -49,6 +51,7 @@ pub struct ClientAPI {
|
||||||
impl ClientAPI {
|
impl ClientAPI {
|
||||||
pub fn new(config: ClientAPIConfig) -> ClientAPI {
|
pub fn new(config: ClientAPIConfig) -> ClientAPI {
|
||||||
tracing_wasm::set_as_global_default();
|
tracing_wasm::set_as_global_default();
|
||||||
|
console_error_panic_hook::set_once();
|
||||||
let configuration = ClientConfiguration::default();
|
let configuration = ClientConfiguration::default();
|
||||||
|
|
||||||
if let Some(compression) = &config.configuration {
|
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> {
|
pub async fn get_user(&self) -> Result<User, ClientResponse> {
|
||||||
match self.client.get_profile().await {
|
match self.client.get_profile().await {
|
||||||
Ok(profile) => Ok(User::from(profile)),
|
Ok(profile) => Ok(User::from(profile)),
|
||||||
|
|
@ -160,4 +209,57 @@ impl ClientAPI {
|
||||||
Err(err) => Err(ClientResponse::from(err)),
|
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)),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -293,6 +293,17 @@ impl Client {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns an OAuth URL by constructing the authorization URL for the specified provider.
|
/// 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
|
/// 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,
|
/// 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`.
|
/// 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 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
|
/// # Parameters
|
||||||
/// - `provider`: A reference to an `OAuthProvider` indicating which OAuth provider to use for login.
|
/// - `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
|
/// # Returns
|
||||||
/// - `Ok(String)`: A `String` containing the constructed authorization URL if the specified provider is available.
|
/// - `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.
|
/// - `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_url_with_provider_and_redirect_to(
|
||||||
pub async fn generate_oauth_url_with_provider(
|
|
||||||
&self,
|
&self,
|
||||||
provider: &AuthProvider,
|
provider: &AuthProvider,
|
||||||
|
redirect_to: Option<String>,
|
||||||
) -> Result<String, AppResponseError> {
|
) -> Result<String, AppResponseError> {
|
||||||
let settings = self.gotrue_client.settings().await?;
|
let settings = self.gotrue_client.settings().await?;
|
||||||
if !settings.external.has_provider(provider) {
|
if !settings.external.has_provider(provider) {
|
||||||
|
|
@ -328,7 +339,12 @@ impl Client {
|
||||||
url
|
url
|
||||||
.query_pairs_mut()
|
.query_pairs_mut()
|
||||||
.append_pair("provider", provider.as_str())
|
.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 {
|
if let AuthProvider::Google = provider {
|
||||||
url
|
url
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ use bytes::Bytes;
|
||||||
use client_api_entity::{PublishInfo, UpdatePublishNamespace};
|
use client_api_entity::{PublishInfo, UpdatePublishNamespace};
|
||||||
use reqwest::Method;
|
use reqwest::Method;
|
||||||
use shared_entity::response::{AppResponse, AppResponseError};
|
use shared_entity::response::{AppResponse, AppResponseError};
|
||||||
|
use tracing::instrument;
|
||||||
|
|
||||||
use crate::Client;
|
use crate::Client;
|
||||||
|
|
||||||
|
|
@ -65,6 +66,7 @@ impl Client {
|
||||||
|
|
||||||
// Guest API (no login required)
|
// Guest API (no login required)
|
||||||
impl Client {
|
impl Client {
|
||||||
|
#[instrument(level = "debug", skip_all)]
|
||||||
pub async fn get_published_collab_info(
|
pub async fn get_published_collab_info(
|
||||||
&self,
|
&self,
|
||||||
view_id: &uuid::Uuid,
|
view_id: &uuid::Uuid,
|
||||||
|
|
@ -77,6 +79,7 @@ impl Client {
|
||||||
.into_data()
|
.into_data()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "debug", skip_all)]
|
||||||
pub async fn get_published_collab<T>(
|
pub async fn get_published_collab<T>(
|
||||||
&self,
|
&self,
|
||||||
publish_namespace: &str,
|
publish_namespace: &str,
|
||||||
|
|
@ -85,6 +88,11 @@ impl Client {
|
||||||
where
|
where
|
||||||
T: serde::de::DeserializeOwned,
|
T: serde::de::DeserializeOwned,
|
||||||
{
|
{
|
||||||
|
tracing::debug!(
|
||||||
|
"get_published_collab: {} {}",
|
||||||
|
publish_namespace,
|
||||||
|
publish_name
|
||||||
|
);
|
||||||
let url = format!(
|
let url = format!(
|
||||||
"{}/api/workspace/published/{}/{}",
|
"{}/api/workspace/published/{}/{}",
|
||||||
self.base_url, publish_namespace, publish_name
|
self.base_url, publish_namespace, publish_name
|
||||||
|
|
@ -104,14 +112,21 @@ impl Client {
|
||||||
}
|
}
|
||||||
|
|
||||||
let meta = serde_json::from_str::<T>(&txt)?;
|
let meta = serde_json::from_str::<T>(&txt)?;
|
||||||
|
|
||||||
Ok(meta)
|
Ok(meta)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "debug", skip_all)]
|
||||||
pub async fn get_published_collab_blob(
|
pub async fn get_published_collab_blob(
|
||||||
&self,
|
&self,
|
||||||
publish_namespace: &str,
|
publish_namespace: &str,
|
||||||
publish_name: &str,
|
publish_name: &str,
|
||||||
) -> Result<Bytes, AppResponseError> {
|
) -> Result<Bytes, AppResponseError> {
|
||||||
|
tracing::debug!(
|
||||||
|
"get_published_collab_blob: {} {}",
|
||||||
|
publish_namespace,
|
||||||
|
publish_name
|
||||||
|
);
|
||||||
let url = format!(
|
let url = format!(
|
||||||
"{}/api/workspace/published/{}/{}/blob",
|
"{}/api/workspace/published/{}/{}/blob",
|
||||||
self.base_url, publish_namespace, publish_name
|
self.base_url, publish_namespace, publish_name
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue