Merge remote-tracking branch 'origin/master' into feat/pricing-plan
Project CI / AI game creator shell Rust crates (pull_request) Successful in 6m0s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 6m43s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 7m42s
Project CI / Frontend tests (pull_request) Failing after 1m16s
Project CI / Backend tests (pull_request) Successful in 8m51s
Project CI / AI game creator shell web tests (pull_request) Successful in 3m34s
Project CI / Repository checks (pull_request) Successful in 5m34s
Project CI / Native shell tests (pull_request) Successful in 7m18s

# Conflicts:
#	docs/project-memory/shared-memory/decision-log.md
This commit is contained in:
2026-10-06 10:14:15 +08:00
131 changed files with 15731 additions and 5835 deletions
+3
View File
@@ -208,6 +208,7 @@ dependencies = [
"bytes",
"cbc",
"dotenvy",
"flate2",
"futures-util",
"hex",
"hmac",
@@ -2879,6 +2880,7 @@ dependencies = [
name = "module-auth"
version = "0.1.0"
dependencies = [
"base64",
"platform-auth",
"serde",
"serde_json",
@@ -5471,6 +5473,7 @@ dependencies = [
"log",
"module-ai",
"module-assets",
"module-auth",
"module-editor-agent",
"module-game-distribution",
"module-runtime",
+2 -1
View File
@@ -12,6 +12,7 @@ base64 = { workspace = true }
cbc = { workspace = true }
bytes = { workspace = true }
dotenvy = { workspace = true }
flate2 = { workspace = true }
hex = { workspace = true }
image = { workspace = true, features = ["jpeg", "png", "webp"] }
http-body-util = { workspace = true }
@@ -21,7 +22,7 @@ rmcp = { workspace = true, features = ["server", "transport-streamable-http-serv
webp = { workspace = true }
module-ai = { workspace = true }
module-assets = { workspace = true, features = ["server-service"] }
module-auth = { workspace = true }
module-auth = { workspace = true, features = ["services"] }
module-editor-agent = { workspace = true }
module-game-distribution = { workspace = true }
module-runtime = { workspace = true }
+1
View File
@@ -50,6 +50,7 @@ pub fn build_router(state: AppState) -> Router {
.merge(modules::external_api::router(state.clone()))
.merge(modules::frontend_runtime_config::router(state.clone()))
.merge(modules::game_distribution::router(state.clone()))
.merge(modules::creator::router(state.clone()))
.merge(modules::assets::router(state.clone()))
.merge(modules::editor_project::router(state.clone()))
.merge(modules::platform::router(state.clone()))
@@ -2,6 +2,7 @@ pub mod admin;
pub mod assets;
pub mod auth;
pub mod client_downloads;
pub mod creator;
pub mod editor_project;
pub mod external_api;
pub mod external_generation;
@@ -0,0 +1,343 @@
use axum::{
Router,
extract::{Extension, Path, Query, Request, State, rejection::QueryRejection},
http::{HeaderValue, StatusCode, header},
middleware::{self, Next},
response::{IntoResponse, Response},
routing::{delete, get, put},
};
use module_auth::creator::{
ConnectionCursor, ConnectionKind, CreatorAction, normalize_user_id, parse_list_limit,
};
use serde::Deserialize;
use spacetime_client::{SpacetimeClientError, creator::CreatorListInput};
use crate::{
api_response::json_success_body, auth::optional_access_token_from_headers,
http_error::AppError, request_context::RequestContext, state::AppState,
};
#[derive(Clone)]
struct CreatorViewer(Option<String>);
#[derive(Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
struct ConnectionsQuery {
limit: Option<String>,
cursor: Option<String>,
}
pub fn router(state: AppState) -> Router<AppState> {
Router::new()
.route("/api/creators/{user_id}", get(profile))
.route("/api/creators/{user_id}/following", get(following))
.route("/api/creators/{user_id}/followers", get(followers))
.route("/api/creators/{user_id}/relationship", get(relationship))
.route(
"/api/creators/{user_id}/follow",
put(follow).delete(unfollow),
)
.route(
"/api/creators/me/followers/{user_id}",
delete(remove_follower),
)
.route_layer(middleware::from_fn_with_state(state, creator_auth))
.route_layer(middleware::from_fn(no_store))
}
async fn no_store(request: Request, next: Next) -> Response {
let mut response = next.run(request).await;
response.headers_mut().insert(
header::CACHE_CONTROL,
HeaderValue::from_static("private, no-store"),
);
response
}
async fn creator_auth(
State(state): State<AppState>,
mut request: Request,
next: Next,
) -> Result<Response, AppError> {
let ctx = request
.extensions()
.get::<RequestContext>()
.ok_or_else(|| AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR))?;
let authenticated = optional_access_token_from_headers(
&state,
request.uri().path().into(),
request.headers().clone(),
ctx.request_id().into(),
)
.await?;
request.extensions_mut().insert(CreatorViewer(
authenticated.map(|auth| auth.claims().user_id().to_owned()),
));
Ok(next.run(request).await)
}
fn input_error(error: impl std::fmt::Display) -> AppError {
AppError::from_status(StatusCode::BAD_REQUEST).with_message(error.to_string())
}
fn actor(viewer: CreatorViewer) -> Result<String, AppError> {
viewer
.0
.ok_or_else(|| AppError::from_status(StatusCode::UNAUTHORIZED))
}
fn database_error(error: SpacetimeClientError) -> AppError {
let status = match &error {
SpacetimeClientError::Procedure(code) => match code.as_str() {
"invalid_input" => StatusCode::BAD_REQUEST,
"user_not_found" => StatusCode::NOT_FOUND,
"forbidden" => StatusCode::FORBIDDEN,
_ => StatusCode::INTERNAL_SERVER_ERROR,
},
_ => StatusCode::INTERNAL_SERVER_ERROR,
};
// 不把底层连接信息或内部身份送到公开响应。
AppError::from_status(status)
}
async fn profile(
State(state): State<AppState>,
Extension(ctx): Extension<RequestContext>,
Path(id): Path<String>,
) -> Result<Response, AppError> {
let id = normalize_user_id(&id).map_err(input_error)?;
let profile = state
.spacetime_client()
.creator_profile(id)
.await
.map_err(database_error)?;
Ok(json_success_body(Some(&ctx), profile).into_response())
}
async fn relationship(
State(state): State<AppState>,
Extension(ctx): Extension<RequestContext>,
Extension(viewer): Extension<CreatorViewer>,
Path(id): Path<String>,
) -> Result<Response, AppError> {
let actor_id = actor(viewer)?;
let id = normalize_user_id(&id).map_err(input_error)?;
let relation = state
.spacetime_client()
.creator_relationship(actor_id, id)
.await
.map_err(database_error)?;
Ok(json_success_body(Some(&ctx), relation).into_response())
}
fn list_input(
id: &str,
viewer: CreatorViewer,
query: Result<Query<ConnectionsQuery>, QueryRejection>,
kind: ConnectionKind,
) -> Result<CreatorListInput, AppError> {
let id = normalize_user_id(id).map_err(input_error)?;
let Query(query) = query.map_err(|_| input_error("列表查询参数不合法"))?;
let limit = parse_list_limit(query.limit.as_deref()).map_err(input_error)? as u32;
if let Some(cursor) = &query.cursor {
ConnectionCursor::decode(cursor, &id, kind).map_err(input_error)?;
}
Ok(CreatorListInput {
owner_id: id,
viewer_id: viewer.0,
kind,
limit,
cursor: query.cursor,
})
}
async fn following(
State(state): State<AppState>,
Extension(ctx): Extension<RequestContext>,
Extension(viewer): Extension<CreatorViewer>,
Path(id): Path<String>,
query: Result<Query<ConnectionsQuery>, QueryRejection>,
) -> Result<Response, AppError> {
connections(
state,
ctx,
list_input(&id, viewer, query, ConnectionKind::Following)?,
)
.await
}
async fn followers(
State(state): State<AppState>,
Extension(ctx): Extension<RequestContext>,
Extension(viewer): Extension<CreatorViewer>,
Path(id): Path<String>,
query: Result<Query<ConnectionsQuery>, QueryRejection>,
) -> Result<Response, AppError> {
connections(
state,
ctx,
list_input(&id, viewer, query, ConnectionKind::Followers)?,
)
.await
}
async fn connections(
state: AppState,
ctx: RequestContext,
input: CreatorListInput,
) -> Result<Response, AppError> {
let list = state
.spacetime_client()
.creator_connections(input)
.await
.map_err(database_error)?;
Ok(json_success_body(Some(&ctx), list).into_response())
}
async fn mutate(
state: AppState,
ctx: RequestContext,
viewer: CreatorViewer,
id: String,
action: CreatorAction,
) -> Result<Response, AppError> {
let actor_id = actor(viewer)?;
let command =
module_auth::creator::prepare_mutation(&actor_id, &id, action).map_err(input_error)?;
let response = state
.spacetime_client()
.mutate_creator_relationship(command.actor_id, command.target_id, action)
.await
.map_err(database_error)?;
Ok(json_success_body(Some(&ctx), response).into_response())
}
async fn follow(
State(state): State<AppState>,
Extension(ctx): Extension<RequestContext>,
Extension(viewer): Extension<CreatorViewer>,
Path(id): Path<String>,
) -> Result<Response, AppError> {
mutate(state, ctx, viewer, id, CreatorAction::Follow).await
}
async fn unfollow(
State(state): State<AppState>,
Extension(ctx): Extension<RequestContext>,
Extension(viewer): Extension<CreatorViewer>,
Path(id): Path<String>,
) -> Result<Response, AppError> {
mutate(state, ctx, viewer, id, CreatorAction::Unfollow).await
}
async fn remove_follower(
State(state): State<AppState>,
Extension(ctx): Extension<RequestContext>,
Extension(viewer): Extension<CreatorViewer>,
Path(id): Path<String>,
) -> Result<Response, AppError> {
mutate(state, ctx, viewer, id, CreatorAction::RemoveFollower).await
}
#[cfg(test)]
mod tests {
use super::*;
use axum::{body::Body, http::Request};
use tower::ServiceExt;
#[tokio::test]
async fn creator_private_routes_reject_anonymous_and_forged_actor_headers() {
let app =
crate::app::build_router(AppState::new(crate::config::AppConfig::default()).unwrap());
for (method, path) in [
("GET", "/api/creators/b/relationship"),
("PUT", "/api/creators/b/follow"),
("DELETE", "/api/creators/b/follow"),
("DELETE", "/api/creators/me/followers/b"),
] {
let response = app
.clone()
.oneshot(
Request::builder()
.method(method)
.uri(path)
.header("x-genarrative-authenticated-user-id", "a")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(
response.status(),
StatusCode::UNAUTHORIZED,
"{method} {path}"
);
assert_eq!(
response.headers()[header::CACHE_CONTROL],
"private, no-store"
);
}
}
#[tokio::test]
async fn creator_public_routes_reject_invalid_bearer_instead_of_downgrading() {
let app =
crate::app::build_router(AppState::new(crate::config::AppConfig::default()).unwrap());
for path in [
"/api/creators/b",
"/api/creators/b/following",
"/api/creators/b/followers",
] {
let response = app
.clone()
.oneshot(
Request::builder()
.uri(path)
.header(header::AUTHORIZATION, "Bearer invalid")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
}
#[tokio::test]
async fn creator_lists_reject_invalid_limit_cursor_and_viewer_override_before_database() {
let app =
crate::app::build_router(AppState::new(crate::config::AppConfig::default()).unwrap());
for query in [
"limit=0",
"limit=51",
"limit=1.5",
"cursor=invalid",
"viewerId=b",
"limit=1&limit=2",
] {
for kind in ["following", "followers"] {
let response = app
.clone()
.oneshot(
Request::builder()
.uri(format!("/api/creators/b/{kind}?{query}"))
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::BAD_REQUEST, "{kind} {query}");
}
}
}
#[test]
fn creator_error_mapping_is_explicit_and_hides_internal_failures() {
for (code, status) in [
("user_not_found", StatusCode::NOT_FOUND),
("forbidden", StatusCode::FORBIDDEN),
("invalid_input", StatusCode::BAD_REQUEST),
(
"secret connection details",
StatusCode::INTERNAL_SERVER_ERROR,
),
] {
let error = database_error(SpacetimeClientError::Procedure(code.into()));
assert_eq!(error.status_code(), status);
assert!(!error.message().contains("secret"));
}
}
}
File diff suppressed because it is too large Load Diff
+10 -5
View File
@@ -4,14 +4,19 @@ edition.workspace = true
version.workspace = true
license.workspace = true
[features]
default = ["services"]
services = ["dep:platform-auth", "dep:shared-kernel", "dep:sha2", "dep:time", "dep:tracing"]
[dependencies]
platform-auth = { workspace = true }
shared-kernel = { workspace = true }
base64 = { workspace = true }
platform-auth = { workspace = true, optional = true }
shared-kernel = { workspace = true, optional = true }
serde = { workspace = true }
serde_json = { workspace = true }
sha2 = { workspace = true }
time = { workspace = true, features = ["formatting", "parsing"] }
tracing = { workspace = true }
sha2 = { workspace = true, optional = true }
time = { workspace = true, features = ["formatting", "parsing"], optional = true }
tracing = { workspace = true, optional = true }
[dev-dependencies]
tokio = { workspace = true, features = ["macros", "rt"] }
+284
View File
@@ -0,0 +1,284 @@
//! 创作者关系纯规则;不依赖账号服务、数据库或 HTTP。
use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
use serde::{Deserialize, Serialize};
pub const MAX_USER_ID_BYTES: usize = 256;
pub const MAX_CURSOR_BYTES: usize = 4096;
pub const DEFAULT_LIST_LIMIT: usize = 20;
pub const MAX_LIST_LIMIT: usize = 50;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CreatorInputError {
InvalidUserId,
SelfRelationship,
InvalidLimit,
InvalidCursor,
}
impl std::fmt::Display for CreatorInputError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::InvalidUserId => "用户 ID 不合法",
Self::SelfRelationship => "不能对自己操作关注关系",
Self::InvalidLimit => "列表数量必须是 1 到 50 的整数",
Self::InvalidCursor => "分页游标不合法或不属于当前列表",
})
}
}
impl std::error::Error for CreatorInputError {}
pub fn normalize_user_id(raw: &str) -> Result<String, CreatorInputError> {
let id = raw.trim();
if id.is_empty() || id.len() > MAX_USER_ID_BYTES || id.chars().any(char::is_control) {
return Err(CreatorInputError::InvalidUserId);
}
Ok(id.to_owned())
}
/// 调用方先规范化 ID;字节长度前缀避免分隔符或多字节 ID 碰撞。
pub fn relationship_id(follower: &str, followee: &str) -> String {
format!("{}:{follower}{followee}", follower.len())
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CreatorAction {
Follow,
Unfollow,
RemoveFollower,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CreatorMutation {
pub actor_id: String,
pub target_id: String,
pub follower_id: String,
pub followee_id: String,
pub relationship_id: String,
pub insert: bool,
}
pub fn prepare_mutation(
actor: &str,
target: &str,
action: CreatorAction,
) -> Result<CreatorMutation, CreatorInputError> {
let actor_id = normalize_user_id(actor)?;
let target_id = normalize_user_id(target)?;
if actor_id == target_id {
return Err(CreatorInputError::SelfRelationship);
}
let (follower_id, followee_id) = match action {
CreatorAction::RemoveFollower => (target_id.clone(), actor_id.clone()),
_ => (actor_id.clone(), target_id.clone()),
};
Ok(CreatorMutation {
relationship_id: relationship_id(&follower_id, &followee_id),
actor_id,
target_id,
follower_id,
followee_id,
insert: action == CreatorAction::Follow,
})
}
pub fn parse_list_limit(raw: Option<&str>) -> Result<usize, CreatorInputError> {
let Some(raw) = raw else {
return Ok(DEFAULT_LIST_LIMIT);
};
if raw.is_empty() || raw.len() > 2 || !raw.bytes().all(|b| b.is_ascii_digit()) {
return Err(CreatorInputError::InvalidLimit);
}
let limit: usize = raw.parse().map_err(|_| CreatorInputError::InvalidLimit)?;
if !(1..=MAX_LIST_LIMIT).contains(&limit) {
return Err(CreatorInputError::InvalidLimit);
}
Ok(limit)
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum ConnectionKind {
Following,
Followers,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ConnectionCursor {
version: u8,
owner_id: String,
kind: ConnectionKind,
created_at_micros: String,
relationship_id: String,
}
impl ConnectionCursor {
pub fn new(
owner_id: String,
kind: ConnectionKind,
micros: i64,
relationship_id: String,
) -> Self {
Self {
version: 1,
owner_id,
kind,
created_at_micros: micros.to_string(),
relationship_id,
}
}
pub fn encode(&self) -> String {
URL_SAFE_NO_PAD.encode(serde_json::to_vec(self).expect("固定结构的游标可序列化"))
}
pub fn decode(raw: &str, owner: &str, kind: ConnectionKind) -> Result<Self, CreatorInputError> {
let invalid = CreatorInputError::InvalidCursor;
if raw.len() > MAX_CURSOR_BYTES {
return Err(invalid);
}
let bytes = URL_SAFE_NO_PAD.decode(raw).map_err(|_| invalid)?;
let cursor: Self = serde_json::from_slice(&bytes).map_err(|_| invalid)?;
let micros: i64 = cursor.created_at_micros.parse().map_err(|_| invalid)?;
if cursor.version != 1
|| cursor.owner_id != owner
|| cursor.kind != kind
|| micros.to_string() != cursor.created_at_micros
|| cursor.relationship_id.is_empty()
|| cursor.relationship_id.len() > 2 * MAX_USER_ID_BYTES + 4
{
return Err(invalid);
}
let (length, pair) = cursor.relationship_id.split_once(':').ok_or(invalid)?;
let length: usize = length.parse().map_err(|_| invalid)?;
let follower = pair.get(..length).ok_or(invalid)?;
let followee = pair.get(length..).ok_or(invalid)?;
if normalize_user_id(follower).map_err(|_| invalid)? != follower
|| normalize_user_id(followee).map_err(|_| invalid)? != followee
|| follower == followee
|| relationship_id(follower, followee) != cursor.relationship_id
|| match kind {
ConnectionKind::Following => follower != owner,
ConnectionKind::Followers => followee != owner,
}
{
return Err(invalid);
}
Ok(cursor)
}
pub fn is_after(&self, micros: i64, key: &str) -> bool {
// 私有字段只可由构造函数或严格 decode 生成。
let at: i64 = self.created_at_micros.parse().expect("已校验的游标时间");
(micros, key) < (at, self.relationship_id.as_str())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn creator_keys_do_not_collide_and_preserve_direction() {
assert_ne!(relationship_id("a", "bc"), relationship_id("ab", "c"));
assert_ne!(relationship_id("a:", "b"), relationship_id("a", ":b"));
assert_ne!(relationship_id("甲", "乙"), relationship_id("乙", "甲"));
assert_eq!(relationship_id("甲", "乙"), "3:甲乙");
}
#[test]
fn creator_removing_follower_only_targets_incoming_edge() {
let follow = prepare_mutation("a", "b", CreatorAction::Follow).unwrap();
let unfollow = prepare_mutation("a", "b", CreatorAction::Unfollow).unwrap();
let remove = prepare_mutation("a", "b", CreatorAction::RemoveFollower).unwrap();
assert_eq!(follow.relationship_id, unfollow.relationship_id);
assert_eq!(remove.relationship_id, relationship_id("b", "a"));
assert_eq!(remove.target_id, "b");
assert!(follow.insert);
assert!(!unfollow.insert);
assert!(!remove.insert);
for action in [
CreatorAction::Follow,
CreatorAction::Unfollow,
CreatorAction::RemoveFollower,
] {
assert_eq!(
prepare_mutation(" a ", "a", action),
Err(CreatorInputError::SelfRelationship)
);
}
}
#[test]
fn creator_input_limits_reject_invalid_and_unbounded_values() {
for id in ["", " ", "a\0b", &"甲".repeat(86)] {
assert!(normalize_user_id(id).is_err());
}
assert_eq!(normalize_user_id(" 甲 ").unwrap(), "甲");
assert_eq!(parse_list_limit(None).unwrap(), 20);
assert_eq!(parse_list_limit(Some("50")).unwrap(), 50);
for limit in ["", "0", "51", "-1", "+1", "1.5", " 2", "1000000000000"] {
assert!(parse_list_limit(Some(limit)).is_err());
}
}
#[test]
fn creator_cursor_binds_owner_kind_and_orders_equal_timestamps() {
let cursor = ConnectionCursor::new(
"a".into(),
ConnectionKind::Following,
9007199254740993,
"1:ac".into(),
);
let encoded = cursor.encode();
let restored = ConnectionCursor::decode(&encoded, "a", ConnectionKind::Following).unwrap();
assert_eq!(restored, cursor);
assert!(restored.is_after(9007199254740993, "1:ab"));
assert!(!restored.is_after(9007199254740993, "1:ac"));
assert!(!restored.is_after(9007199254740994, "1:ab"));
assert!(ConnectionCursor::decode(&encoded, "b", ConnectionKind::Following).is_err());
assert!(ConnectionCursor::decode(&encoded, "a", ConnectionKind::Followers).is_err());
}
#[test]
fn creator_cursor_rejects_malformed_unknown_and_oversized_payloads() {
let cursor = ConnectionCursor::new("a".into(), ConnectionKind::Followers, 3, "1:ba".into());
let mut json = serde_json::to_value(cursor).unwrap();
for bad in [
serde_json::json!(3),
serde_json::json!("+3"),
serde_json::json!("03"),
serde_json::json!("9223372036854775808"),
] {
json["createdAtMicros"] = bad;
assert!(
ConnectionCursor::decode(
&URL_SAFE_NO_PAD.encode(serde_json::to_vec(&json).unwrap()),
"a",
ConnectionKind::Followers
)
.is_err()
);
}
json["createdAtMicros"] = serde_json::json!("3");
json["extra"] = serde_json::json!(true);
assert!(
ConnectionCursor::decode(
&URL_SAFE_NO_PAD.encode(serde_json::to_vec(&json).unwrap()),
"a",
ConnectionKind::Followers
)
.is_err()
);
for raw in ["!", "", &"a".repeat(MAX_CURSOR_BYTES + 1)] {
assert!(ConnectionCursor::decode(raw, "a", ConnectionKind::Followers).is_err());
}
for key in ["broken", "999:ba", "01:ba", "1:aa", "1:bc", "1:甲a"] {
let cursor =
ConnectionCursor::new("a".into(), ConnectionKind::Followers, 3, key.into());
assert!(
ConnectionCursor::decode(&cursor.encode(), "a", ConnectionKind::Followers).is_err()
);
}
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -72,6 +72,8 @@ const MAIN_SPA_PATHS: &[&str] = &[
"/games/mine",
"/games/play",
"/games/publish",
"/creators",
"/creators/connections",
"/pay",
"/profile",
"/profile/payment",
@@ -0,0 +1,95 @@
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct CreatorUser {
pub id: String,
pub public_user_code: String,
pub display_name: String,
pub avatar_url: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct CreatorRelationship {
pub is_self: bool,
pub is_following: bool,
pub is_followed_by: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct CreatorProfile {
pub user: CreatorUser,
pub following_count: u64,
pub follower_count: u64,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct CreatorConnection {
pub user: CreatorUser,
pub followed_at: String,
pub relationship: Option<CreatorRelationship>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct CreatorConnections {
pub items: Vec<CreatorConnection>,
pub next_cursor: Option<String>,
pub total: u64,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct CreatorRelationshipResponse {
pub user_id: String,
pub relationship: CreatorRelationship,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn creator_public_list_contract_has_explicit_nulls_and_only_public_fields() {
let list = CreatorConnections {
items: vec![CreatorConnection {
user: CreatorUser {
id: "b".into(),
public_user_code: "SY-2".into(),
display_name: "作者乙".into(),
avatar_url: None,
},
followed_at: "2026-10-05T00:00:00Z".into(),
relationship: None,
}],
next_cursor: None,
total: 1,
};
let expected = json!({"items":[{"user":{"id":"b","publicUserCode":"SY-2","displayName":"作者乙","avatarUrl":null},"followedAt":"2026-10-05T00:00:00Z","relationship":null}],"nextCursor":null,"total":1});
assert_eq!(serde_json::to_value(&list).unwrap(), expected);
assert_eq!(
serde_json::from_value::<CreatorConnections>(expected).unwrap(),
list
);
}
#[test]
fn creator_remove_follower_receipt_keeps_outgoing_direction() {
let response = CreatorRelationshipResponse {
user_id: "b".into(),
relationship: CreatorRelationship {
is_self: false,
is_following: true,
is_followed_by: false,
},
};
assert_eq!(
serde_json::to_value(response).unwrap(),
json!({"userId":"b","relationship":{"isSelf":false,"isFollowing":true,"isFollowedBy":false}})
);
}
}
@@ -8,6 +8,7 @@ pub mod assets;
pub mod auth;
pub mod client_downloads;
pub mod creation_audio;
pub mod creator;
pub mod editor_agent;
pub mod editor_generation;
pub mod error_reports;
+1 -1
View File
@@ -10,7 +10,7 @@ path = "src/active.rs"
[dependencies]
module-ai = { workspace = true }
module-assets = { workspace = true }
module-auth = { workspace = true }
module-auth = { workspace = true, features = ["services"] }
module-runtime = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
@@ -15,6 +15,7 @@ pub mod agc_models;
pub mod ai;
pub mod assets;
pub mod auth;
pub mod creator;
pub mod editor_agent;
pub mod editor_project;
#[path = "active/error_reports.rs"]
@@ -10,6 +10,8 @@ mod ai;
mod assets;
#[path = "mapper/auth.rs"]
mod auth;
#[path = "mapper/creator.rs"]
pub(crate) mod creator;
#[path = "mapper/editor_agent.rs"]
mod editor_agent;
#[path = "mapper/editor_project.rs"]
@@ -0,0 +1,120 @@
use crate::{SpacetimeClientError, module_bindings as b};
use shared_contracts::creator as c;
fn result<T>(data: Option<T>, error: Option<String>) -> Result<T, SpacetimeClientError> {
if error.is_some() {
return Err(SpacetimeClientError::procedure_failed(error));
}
data.ok_or_else(|| SpacetimeClientError::procedure_failed(Some("创作者查询缺少结果".into())))
}
fn user(value: b::CreatorUserView) -> c::CreatorUser {
c::CreatorUser {
id: value.id,
public_user_code: value.public_user_code,
display_name: value.display_name,
avatar_url: value.avatar_url,
}
}
fn relationship(value: b::CreatorRelationshipView) -> c::CreatorRelationship {
c::CreatorRelationship {
is_self: value.is_self,
is_following: value.is_following,
is_followed_by: value.is_followed_by,
}
}
pub(crate) fn profile(
value: b::CreatorProfileResult,
) -> Result<c::CreatorProfile, SpacetimeClientError> {
let value = result(value.profile, value.error_code)?;
Ok(c::CreatorProfile {
user: user(value.user),
following_count: value.following_count,
follower_count: value.follower_count,
})
}
pub(crate) fn connections(
value: b::CreatorConnectionsResult,
) -> Result<c::CreatorConnections, SpacetimeClientError> {
let value = result(value.connections, value.error_code)?;
Ok(c::CreatorConnections {
items: value
.items
.into_iter()
.map(|row| c::CreatorConnection {
user: user(row.user),
followed_at: row.followed_at,
relationship: row.relationship.map(relationship),
})
.collect(),
next_cursor: value.next_cursor,
total: value.total,
})
}
pub(crate) fn relationship_response(
value: b::CreatorRelationshipResult,
) -> Result<c::CreatorRelationshipResponse, SpacetimeClientError> {
let value = result(value.record, value.error_code)?;
Ok(c::CreatorRelationshipResponse {
user_id: value.user_id,
relationship: relationship(value.relationship),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn creator_mapper_does_not_convert_errors_to_empty_success() {
assert!(
profile(b::CreatorProfileResult {
profile: None,
error_code: None
})
.is_err()
);
assert!(
connections(b::CreatorConnectionsResult {
connections: None,
error_code: Some("forbidden".into())
})
.is_err()
);
}
#[test]
fn creator_mapper_preserves_readonly_rows_and_incoming_removal() {
let mapped = connections(b::CreatorConnectionsResult {
connections: Some(b::CreatorConnectionsView {
items: vec![b::CreatorConnectionView {
user: b::CreatorUserView {
id: "b".into(),
public_user_code: "SY-2".into(),
display_name: "作者".into(),
avatar_url: None,
},
followed_at: "2026-10-05T00:00:00Z".into(),
relationship: None,
}],
total: 1,
next_cursor: None,
}),
error_code: None,
})
.unwrap();
assert_eq!(mapped.items[0].relationship, None);
let mapped = relationship_response(b::CreatorRelationshipResult {
record: Some(b::CreatorRelationshipRecord {
user_id: "b".into(),
relationship: b::CreatorRelationshipView {
is_self: false,
is_following: true,
is_followed_by: false,
},
}),
error_code: None,
})
.unwrap();
assert!(mapped.relationship.is_following);
assert!(!mapped.relationship.is_followed_by);
}
}
@@ -0,0 +1,110 @@
use super::*;
use crate::mapper::creator as map;
use shared_contracts::creator as c;
#[derive(Clone, Debug)]
pub struct CreatorListInput {
pub owner_id: String,
pub viewer_id: Option<String>,
pub kind: module_auth::creator::ConnectionKind,
pub limit: u32,
pub cursor: Option<String>,
}
impl SpacetimeClient {
pub async fn creator_profile(
&self,
user_id: String,
) -> Result<c::CreatorProfile, SpacetimeClientError> {
self.call_after_connect("creator_profile", move |connection, sender| {
connection.procedures().get_creator_profile_and_return_then(
user_id,
move |_, result| {
send_once(
&sender,
result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map::profile),
);
},
);
})
.await
}
pub async fn creator_connections(
&self,
input: CreatorListInput,
) -> Result<c::CreatorConnections, SpacetimeClientError> {
let input = module_bindings::CreatorConnectionsInput {
owner_id: input.owner_id,
viewer_id: input.viewer_id,
kind: match input.kind {
module_auth::creator::ConnectionKind::Following => "following",
module_auth::creator::ConnectionKind::Followers => "followers",
}
.into(),
limit: input.limit,
cursor: input.cursor,
};
self.call_after_connect("creator_connections", move |connection, sender| {
connection
.procedures()
.list_creator_connections_and_return_then(input, move |_, result| {
send_once(
&sender,
result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map::connections),
);
});
})
.await
}
pub async fn creator_relationship(
&self,
actor_id: String,
target_id: String,
) -> Result<c::CreatorRelationshipResponse, SpacetimeClientError> {
let input = module_bindings::CreatorRelationshipInput {
actor_id,
target_id,
};
self.call_after_connect("creator_relationship", move |connection, sender| {
connection
.procedures()
.get_creator_relationship_and_return_then(input, move |_, result| {
send_once(
&sender,
result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map::relationship_response),
);
});
})
.await
}
pub async fn mutate_creator_relationship(
&self,
actor_id: String,
target_id: String,
action: module_auth::creator::CreatorAction,
) -> Result<c::CreatorRelationshipResponse, SpacetimeClientError> {
let input = module_bindings::CreatorRelationshipInput {
actor_id,
target_id,
};
self.call_after_connect("mutate_creator_relationship", move |connection, sender| {
let callback = move |_: &module_bindings::ProcedureEventContext, result: Result<module_bindings::CreatorRelationshipResult, _>| {
send_once(&sender, result.map_err(SpacetimeClientError::from_sdk_error).and_then(map::relationship_response));
};
match action {
module_auth::creator::CreatorAction::Follow => connection.procedures().follow_creator_and_return_then(input, callback),
module_auth::creator::CreatorAction::Unfollow => connection.procedures().unfollow_creator_and_return_then(input, callback),
module_auth::creator::CreatorAction::RemoveFollower => connection.procedures().remove_creator_follower_and_return_then(input, callback),
}
}).await
}
}
@@ -32,6 +32,7 @@ pub struct GameDistributionPublicGameListRecordInput {
pub search: Option<String>,
pub category: Option<String>,
pub limit: u32,
pub author_id: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
@@ -531,6 +532,7 @@ impl SpacetimeClient {
search: input.search,
category: input.category,
limit: input.limit,
author_id: input.author_id,
};
self.call_after_connect("list_game_distribution_games", move |connection, sender| {
connection
@@ -182,6 +182,17 @@ pub mod creation_entry_config_table;
pub mod creation_entry_config_type;
pub mod creation_entry_type_config_table;
pub mod creation_entry_type_config_type;
pub mod creator_connection_view_type;
pub mod creator_connections_input_type;
pub mod creator_connections_result_type;
pub mod creator_connections_view_type;
pub mod creator_profile_result_type;
pub mod creator_profile_view_type;
pub mod creator_relationship_input_type;
pub mod creator_relationship_record_type;
pub mod creator_relationship_result_type;
pub mod creator_relationship_view_type;
pub mod creator_user_view_type;
pub mod database_migration_authorize_operator_input_type;
pub mod database_migration_export_input_type;
pub mod database_migration_import_chunk_input_type;
@@ -409,6 +420,7 @@ pub mod feature_gate_config_snapshot_type;
pub mod feature_gate_config_table;
pub mod feature_gate_config_type;
pub mod find_editor_asset_group_source_and_return_procedure;
pub mod follow_creator_and_return_procedure;
pub mod game_distribution_admin_game_list_input_type;
pub mod game_distribution_admin_game_list_result_type;
pub mod game_distribution_admin_game_snapshot_type;
@@ -474,6 +486,8 @@ pub mod get_admin_game_distribution_user_review_and_return_procedure;
pub mod get_asset_object_by_id_and_return_procedure;
pub mod get_asset_object_by_location_and_return_procedure;
pub mod get_asset_read_access_by_location_and_return_procedure;
pub mod get_creator_profile_and_return_procedure;
pub mod get_creator_relationship_and_return_procedure;
pub mod get_editor_agent_conversation_and_return_procedure;
pub mod get_editor_asset_folder_and_return_procedure;
pub mod get_editor_asset_library_and_return_procedure;
@@ -518,6 +532,7 @@ pub mod list_admin_game_distribution_games_and_return_procedure;
pub mod list_admin_game_distribution_user_reviews_and_return_procedure;
pub mod list_agc_tracking_events_procedure;
pub mod list_asset_history_and_return_procedure;
pub mod list_creator_connections_and_return_procedure;
pub mod list_editor_agent_conversations_and_return_procedure;
pub mod list_editor_projects_and_return_procedure;
pub mod list_error_reports_and_return_procedure;
@@ -676,6 +691,7 @@ pub mod refresh_session_type;
pub mod refund_profile_wallet_points_and_return_procedure;
pub mod reject_game_distribution_version_and_return_procedure;
pub mod release_profile_recharge_refund_hold_and_return_procedure;
pub mod remove_creator_follower_and_return_procedure;
pub mod rename_editor_project_and_return_procedure;
pub mod renew_external_generation_job_lease_and_return_procedure;
pub mod repair_editor_asset_media_and_return_procedure;
@@ -872,6 +888,7 @@ pub mod tracking_daily_stat_table;
pub mod tracking_daily_stat_type;
pub mod tracking_event_table;
pub mod tracking_event_type;
pub mod unfollow_creator_and_return_procedure;
pub mod unpublish_game_distribution_game_and_return_procedure;
pub mod update_admin_account_and_return_procedure;
pub mod update_editor_asset_and_return_procedure;
@@ -892,6 +909,8 @@ pub mod user_account_table;
pub mod user_account_type;
pub mod user_browse_history_table;
pub mod user_browse_history_type;
pub mod user_follow_table;
pub mod user_follow_type;
pub mod validate_auth_session_procedure;
pub use acknowledge_external_generation_job_summaries_and_return_procedure::acknowledge_external_generation_job_summaries_and_return;
@@ -1070,6 +1089,17 @@ pub use creation_entry_config_table::*;
pub use creation_entry_config_type::CreationEntryConfig;
pub use creation_entry_type_config_table::*;
pub use creation_entry_type_config_type::CreationEntryTypeConfig;
pub use creator_connection_view_type::CreatorConnectionView;
pub use creator_connections_input_type::CreatorConnectionsInput;
pub use creator_connections_result_type::CreatorConnectionsResult;
pub use creator_connections_view_type::CreatorConnectionsView;
pub use creator_profile_result_type::CreatorProfileResult;
pub use creator_profile_view_type::CreatorProfileView;
pub use creator_relationship_input_type::CreatorRelationshipInput;
pub use creator_relationship_record_type::CreatorRelationshipRecord;
pub use creator_relationship_result_type::CreatorRelationshipResult;
pub use creator_relationship_view_type::CreatorRelationshipView;
pub use creator_user_view_type::CreatorUserView;
pub use database_migration_authorize_operator_input_type::DatabaseMigrationAuthorizeOperatorInput;
pub use database_migration_export_input_type::DatabaseMigrationExportInput;
pub use database_migration_import_chunk_input_type::DatabaseMigrationImportChunkInput;
@@ -1297,6 +1327,7 @@ pub use feature_gate_config_snapshot_type::FeatureGateConfigSnapshot;
pub use feature_gate_config_table::*;
pub use feature_gate_config_type::FeatureGateConfig;
pub use find_editor_asset_group_source_and_return_procedure::find_editor_asset_group_source_and_return;
pub use follow_creator_and_return_procedure::follow_creator_and_return;
pub use game_distribution_admin_game_list_input_type::GameDistributionAdminGameListInput;
pub use game_distribution_admin_game_list_result_type::GameDistributionAdminGameListResult;
pub use game_distribution_admin_game_snapshot_type::GameDistributionAdminGameSnapshot;
@@ -1362,6 +1393,8 @@ pub use get_admin_game_distribution_user_review_and_return_procedure::get_admin_
pub use get_asset_object_by_id_and_return_procedure::get_asset_object_by_id_and_return;
pub use get_asset_object_by_location_and_return_procedure::get_asset_object_by_location_and_return;
pub use get_asset_read_access_by_location_and_return_procedure::get_asset_read_access_by_location_and_return;
pub use get_creator_profile_and_return_procedure::get_creator_profile_and_return;
pub use get_creator_relationship_and_return_procedure::get_creator_relationship_and_return;
pub use get_editor_agent_conversation_and_return_procedure::get_editor_agent_conversation_and_return;
pub use get_editor_asset_folder_and_return_procedure::get_editor_asset_folder_and_return;
pub use get_editor_asset_library_and_return_procedure::get_editor_asset_library_and_return;
@@ -1406,6 +1439,7 @@ pub use list_admin_game_distribution_games_and_return_procedure::list_admin_game
pub use list_admin_game_distribution_user_reviews_and_return_procedure::list_admin_game_distribution_user_reviews_and_return;
pub use list_agc_tracking_events_procedure::list_agc_tracking_events;
pub use list_asset_history_and_return_procedure::list_asset_history_and_return;
pub use list_creator_connections_and_return_procedure::list_creator_connections_and_return;
pub use list_editor_agent_conversations_and_return_procedure::list_editor_agent_conversations_and_return;
pub use list_editor_projects_and_return_procedure::list_editor_projects_and_return;
pub use list_error_reports_and_return_procedure::list_error_reports_and_return;
@@ -1564,6 +1598,7 @@ pub use refresh_session_type::RefreshSession;
pub use refund_profile_wallet_points_and_return_procedure::refund_profile_wallet_points_and_return;
pub use reject_game_distribution_version_and_return_procedure::reject_game_distribution_version_and_return;
pub use release_profile_recharge_refund_hold_and_return_procedure::release_profile_recharge_refund_hold_and_return;
pub use remove_creator_follower_and_return_procedure::remove_creator_follower_and_return;
pub use rename_editor_project_and_return_procedure::rename_editor_project_and_return;
pub use renew_external_generation_job_lease_and_return_procedure::renew_external_generation_job_lease_and_return;
pub use repair_editor_asset_media_and_return_procedure::repair_editor_asset_media_and_return;
@@ -1760,6 +1795,7 @@ pub use tracking_daily_stat_table::*;
pub use tracking_daily_stat_type::TrackingDailyStat;
pub use tracking_event_table::*;
pub use tracking_event_type::TrackingEvent;
pub use unfollow_creator_and_return_procedure::unfollow_creator_and_return;
pub use unpublish_game_distribution_game_and_return_procedure::unpublish_game_distribution_game_and_return;
pub use update_admin_account_and_return_procedure::update_admin_account_and_return;
pub use update_editor_asset_and_return_procedure::update_editor_asset_and_return;
@@ -1780,6 +1816,8 @@ pub use user_account_table::*;
pub use user_account_type::UserAccount;
pub use user_browse_history_table::*;
pub use user_browse_history_type::UserBrowseHistory;
pub use user_follow_table::*;
pub use user_follow_type::UserFollow;
pub use validate_auth_session_procedure::validate_auth_session;
#[derive(Clone, PartialEq, Debug)]
@@ -1988,6 +2026,7 @@ pub struct DbUpdate {
tracking_event: __sdk::TableUpdate<TrackingEvent>,
user_account: __sdk::TableUpdate<UserAccount>,
user_browse_history: __sdk::TableUpdate<UserBrowseHistory>,
user_follow: __sdk::TableUpdate<UserFollow>,
}
impl TryFrom<__ws::v2::TransactionUpdate> for DbUpdate {
@@ -2330,6 +2369,9 @@ impl TryFrom<__ws::v2::TransactionUpdate> for DbUpdate {
"user_browse_history" => db_update
.user_browse_history
.append(user_browse_history_table::parse_table_update(table_update)?),
"user_follow" => db_update
.user_follow
.append(user_follow_table::parse_table_update(table_update)?),
unknown => {
return Err(__sdk::InternalError::unknown_name(
@@ -2829,6 +2871,9 @@ impl __sdk::DbUpdate for DbUpdate {
&self.user_browse_history,
)
.with_updates_by_pk(|row| &row.browse_history_id);
diff.user_follow = cache
.apply_diff_to_table::<UserFollow>("user_follow", &self.user_follow)
.with_updates_by_pk(|row| &row.relationship_id);
diff
}
@@ -3112,6 +3157,9 @@ impl __sdk::DbUpdate for DbUpdate {
"user_browse_history" => db_update
.user_browse_history
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"user_follow" => db_update
.user_follow
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
unknown => {
return Err(
__sdk::InternalError::unknown_name("table", unknown, "QueryRows").into(),
@@ -3401,6 +3449,9 @@ impl __sdk::DbUpdate for DbUpdate {
"user_browse_history" => db_update
.user_browse_history
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"user_follow" => db_update
.user_follow
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
unknown => {
return Err(
__sdk::InternalError::unknown_name("table", unknown, "QueryRows").into(),
@@ -3517,6 +3568,7 @@ pub struct AppliedDiff<'r> {
tracking_event: __sdk::TableAppliedDiff<'r, TrackingEvent>,
user_account: __sdk::TableAppliedDiff<'r, UserAccount>,
user_browse_history: __sdk::TableAppliedDiff<'r, UserBrowseHistory>,
user_follow: __sdk::TableAppliedDiff<'r, UserFollow>,
__unused: std::marker::PhantomData<&'r ()>,
}
@@ -3978,6 +4030,7 @@ impl<'r> __sdk::AppliedDiff<'r> for AppliedDiff<'r> {
&self.user_browse_history,
event,
);
callbacks.invoke_table_row_callbacks::<UserFollow>("user_follow", &self.user_follow, event);
}
}
@@ -4730,6 +4783,7 @@ impl __sdk::SpacetimeModule for RemoteModule {
tracking_event_table::register_table(client_cache);
user_account_table::register_table(client_cache);
user_browse_history_table::register_table(client_cache);
user_follow_table::register_table(client_cache);
}
const ALL_TABLE_NAMES: &'static [&'static str] = &[
"admin_account",
@@ -4824,5 +4878,6 @@ impl __sdk::SpacetimeModule for RemoteModule {
"tracking_event",
"user_account",
"user_browse_history",
"user_follow",
];
}
@@ -0,0 +1,20 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
use super::creator_relationship_view_type::CreatorRelationshipView;
use super::creator_user_view_type::CreatorUserView;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
pub struct CreatorConnectionView {
pub user: CreatorUserView,
pub followed_at: String,
pub relationship: Option<CreatorRelationshipView>,
}
impl __sdk::InModule for CreatorConnectionView {
type Module = super::RemoteModule;
}
@@ -0,0 +1,19 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
pub struct CreatorConnectionsInput {
pub owner_id: String,
pub viewer_id: Option<String>,
pub kind: String,
pub limit: u32,
pub cursor: Option<String>,
}
impl __sdk::InModule for CreatorConnectionsInput {
type Module = super::RemoteModule;
}
@@ -0,0 +1,18 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
use super::creator_connections_view_type::CreatorConnectionsView;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
pub struct CreatorConnectionsResult {
pub connections: Option<CreatorConnectionsView>,
pub error_code: Option<String>,
}
impl __sdk::InModule for CreatorConnectionsResult {
type Module = super::RemoteModule;
}
@@ -0,0 +1,19 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
use super::creator_connection_view_type::CreatorConnectionView;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
pub struct CreatorConnectionsView {
pub items: Vec<CreatorConnectionView>,
pub next_cursor: Option<String>,
pub total: u64,
}
impl __sdk::InModule for CreatorConnectionsView {
type Module = super::RemoteModule;
}
@@ -0,0 +1,18 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
use super::creator_profile_view_type::CreatorProfileView;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
pub struct CreatorProfileResult {
pub profile: Option<CreatorProfileView>,
pub error_code: Option<String>,
}
impl __sdk::InModule for CreatorProfileResult {
type Module = super::RemoteModule;
}

Some files were not shown because too many files have changed in this diff Show More