Files
Genarrative/server-rs/crates/spacetime-module/src/asset_metadata/bindings.rs

172 lines
6.2 KiB
Rust

use crate::*;
#[spacetimedb::table(
accessor = asset_entity_binding,
index(accessor = by_entity_slot, btree(columns = [entity_kind, entity_id, slot])),
index(accessor = by_asset_object_id, btree(columns = [asset_object_id]))
)]
pub struct AssetEntityBinding {
#[primary_key]
binding_id: String,
asset_object_id: String,
entity_kind: String,
entity_id: String,
slot: String,
asset_kind: String,
owner_user_id: Option<String>,
profile_id: Option<String>,
created_at: Timestamp,
updated_at: Timestamp,
}
// reducer 负责把已确认对象绑定到实体槽位,强业务资产表稳定前先用通用绑定表承接关系。
#[spacetimedb::reducer]
pub fn bind_asset_object_to_entity(
ctx: &ReducerContext,
input: AssetEntityBindingInput,
) -> Result<(), String> {
upsert_asset_entity_binding(ctx, input).map(|_| ())
}
// procedure 面向 Axum 同步绑定接口,返回最终绑定快照,避免 HTTP 层读取 private table。
#[spacetimedb::procedure]
pub fn bind_asset_object_to_entity_and_return(
ctx: &mut ProcedureContext,
input: AssetEntityBindingInput,
) -> AssetEntityBindingProcedureResult {
match ctx.try_with_tx(|tx| upsert_asset_entity_binding(tx, input.clone())) {
Ok(record) => AssetEntityBindingProcedureResult {
ok: true,
record: Some(record),
error_message: None,
},
Err(message) => AssetEntityBindingProcedureResult {
ok: false,
record: None,
error_message: Some(message),
},
}
}
fn upsert_asset_entity_binding(
ctx: &ReducerContext,
input: AssetEntityBindingInput,
) -> Result<AssetEntityBindingSnapshot, String> {
validate_asset_entity_binding_fields(
&input.binding_id,
&input.asset_object_id,
&input.entity_kind,
&input.entity_id,
&input.slot,
&input.asset_kind,
)
.map_err(|error| error.to_string())?;
if !has_asset_object(ctx, &input.asset_object_id) {
return Err("asset_entity_binding.asset_object_id 对应的 asset_object 不存在".to_string());
}
// 首版绑定按 entity_kind + entity_id + slot 幂等定位,后续访问量明确后再改为组合索引扫描。
let current = ctx.db.asset_entity_binding().iter().find(|row| {
row.entity_kind == input.entity_kind
&& row.entity_id == input.entity_id
&& row.slot == input.slot
});
let snapshot = match current {
Some(existing) => {
ctx.db
.asset_entity_binding()
.binding_id()
.delete(&existing.binding_id);
let snapshot = AssetEntityBindingSnapshot {
binding_id: existing.binding_id.clone(),
asset_object_id: input.asset_object_id.clone(),
entity_kind: input.entity_kind.clone(),
entity_id: input.entity_id.clone(),
slot: input.slot.clone(),
asset_kind: input.asset_kind.clone(),
owner_user_id: input.owner_user_id.clone(),
profile_id: input.profile_id.clone(),
created_at_micros: existing.created_at.to_micros_since_unix_epoch(),
updated_at_micros: input.updated_at_micros,
};
ctx.db
.asset_entity_binding()
.insert(build_asset_entity_binding_row(&snapshot));
snapshot
}
None => {
let snapshot = AssetEntityBindingSnapshot {
binding_id: input.binding_id.clone(),
asset_object_id: input.asset_object_id.clone(),
entity_kind: input.entity_kind.clone(),
entity_id: input.entity_id.clone(),
slot: input.slot.clone(),
asset_kind: input.asset_kind.clone(),
owner_user_id: input.owner_user_id.clone(),
profile_id: input.profile_id.clone(),
created_at_micros: input.updated_at_micros,
updated_at_micros: input.updated_at_micros,
};
ctx.db
.asset_entity_binding()
.insert(build_asset_entity_binding_row(&snapshot));
snapshot
}
};
emit_asset_entity_binding_changed_event(ctx, &snapshot);
Ok(snapshot)
}
fn build_asset_entity_binding_row(snapshot: &AssetEntityBindingSnapshot) -> AssetEntityBinding {
AssetEntityBinding {
binding_id: snapshot.binding_id.clone(),
asset_object_id: snapshot.asset_object_id.clone(),
entity_kind: snapshot.entity_kind.clone(),
entity_id: snapshot.entity_id.clone(),
slot: snapshot.slot.clone(),
asset_kind: snapshot.asset_kind.clone(),
owner_user_id: snapshot.owner_user_id.clone(),
profile_id: snapshot.profile_id.clone(),
created_at: Timestamp::from_micros_since_unix_epoch(snapshot.created_at_micros),
updated_at: Timestamp::from_micros_since_unix_epoch(snapshot.updated_at_micros),
}
}
fn emit_asset_entity_binding_changed_event(
ctx: &ReducerContext,
snapshot: &AssetEntityBindingSnapshot,
) {
let event = AssetEntityBindingChangedEvent {
binding_id: snapshot.binding_id.clone(),
asset_object_id: snapshot.asset_object_id.clone(),
entity_kind: snapshot.entity_kind.clone(),
entity_id: snapshot.entity_id.clone(),
slot: snapshot.slot.clone(),
asset_kind: snapshot.asset_kind.clone(),
owner_user_id: snapshot.owner_user_id.clone(),
profile_id: snapshot.profile_id.clone(),
occurred_at_micros: snapshot.updated_at_micros,
};
ctx.db.asset_event().insert(AssetEvent {
event_id: format!(
"assetevt_{}_{}_binding",
event.binding_id, event.occurred_at_micros
),
asset_object_id: event.asset_object_id,
binding_id: Some(event.binding_id),
event_kind: AssetEventKind::EntityBindingChanged,
asset_kind: event.asset_kind,
owner_user_id: event.owner_user_id,
profile_id: event.profile_id,
entity_kind: Some(event.entity_kind),
entity_id: Some(event.entity_id),
slot: Some(event.slot),
occurred_at: Timestamp::from_micros_since_unix_epoch(event.occurred_at_micros),
});
}