extern crate self as appdb;
pub mod auth;
pub mod connection;
pub mod crypto;
pub mod error;
pub mod graph;
pub mod model;
pub mod prelude;
pub mod query;
pub mod repository;
pub mod serde_utils;
pub mod tx;
pub use appdb_macros::{Bridge, Relation, Sensitive, Store};
pub use auth::*;
pub use connection::*;
pub use crypto::*;
pub use error::*;
pub use graph::*;
pub use repository::*;
pub use serde_utils::id::*;
pub use tx::*;
use surrealdb::types::RecordId;
use surrealdb_types::Kind;
#[::async_trait::async_trait]
pub trait Bridge: Sized + Send {
async fn persist_foreign(self) -> anyhow::Result<surrealdb::types::RecordId>;
async fn hydrate_foreign(id: surrealdb::types::RecordId) -> anyhow::Result<Self>;
}
#[::async_trait::async_trait]
pub trait ForeignShape: Sized + Send {
type Stored: Clone
+ serde::Serialize
+ serde::de::DeserializeOwned
+ surrealdb::types::SurrealValue
+ Send;
async fn persist_foreign_shape(self) -> anyhow::Result<Self::Stored>;
async fn hydrate_foreign_shape(stored: Self::Stored) -> anyhow::Result<Self>;
}
#[derive(Debug, Clone)]
pub struct RelationWrite {
pub relation: &'static str,
pub record: surrealdb::types::RecordId,
pub direction: RelationWriteDirection,
pub edges: Vec<crate::graph::OrderedRelationEdge>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RelationWriteDirection {
Outgoing,
Incoming,
}
#[::async_trait::async_trait]
pub trait RelateShape: Sized + Send {
async fn persist_relate_shape(self) -> anyhow::Result<Vec<surrealdb::types::RecordId>>;
async fn hydrate_relate_shape(stored: Vec<surrealdb::types::RecordId>) -> anyhow::Result<Self>;
}
pub trait Sensitive: Sized {
type Encrypted;
fn encrypt(
&self,
context: &crate::crypto::CryptoContext,
) -> Result<Self::Encrypted, crate::crypto::CryptoError>;
fn decrypt(
encrypted: &Self::Encrypted,
context: &crate::crypto::CryptoContext,
) -> Result<Self, crate::crypto::CryptoError>;
fn encrypt_with_runtime_resolver(&self) -> Result<Self::Encrypted, crate::crypto::CryptoError>;
fn decrypt_with_runtime_resolver(
encrypted: &Self::Encrypted,
) -> Result<Self, crate::crypto::CryptoError>;
fn secure_fields() -> &'static [crate::crypto::SensitiveFieldMetadata];
fn secure_field_index(field_tag: &'static str) -> usize {
Self::secure_fields()
.iter()
.position(|meta| meta.field_tag == field_tag)
.expect("generated secure field metadata should contain every secure field tag")
}
}
pub trait SensitiveShape: Sized {
type Encrypted: Clone
+ serde::Serialize
+ serde::de::DeserializeOwned
+ surrealdb::types::SurrealValue;
fn encrypt_with_context(
&self,
context: &crate::crypto::CryptoContext,
) -> Result<Self::Encrypted, crate::crypto::CryptoError>;
fn decrypt_with_context(
encrypted: &Self::Encrypted,
context: &crate::crypto::CryptoContext,
) -> Result<Self, crate::crypto::CryptoError>;
}
pub trait SensitiveValue: Sized {
type Encrypted: Clone
+ serde::Serialize
+ serde::de::DeserializeOwned
+ surrealdb::types::SurrealValue;
fn encrypt_value(
&self,
context: &crate::crypto::CryptoContext,
) -> Result<Self::Encrypted, crate::crypto::CryptoError>;
fn decrypt_value(
encrypted: &Self::Encrypted,
context: &crate::crypto::CryptoContext,
) -> Result<Self, crate::crypto::CryptoError>;
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct SensitiveValueOf<T>(pub T);
impl<T> SensitiveValueOf<T> {
pub fn into_inner(self) -> T {
self.0
}
}
impl<T> std::ops::Deref for SensitiveValueOf<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<T> From<T> for SensitiveValueOf<T> {
fn from(value: T) -> Self {
Self(value)
}
}
impl<T> surrealdb::types::SurrealValue for SensitiveValueOf<T>
where
T: surrealdb::types::SurrealValue,
{
fn kind_of() -> Kind {
T::kind_of()
}
fn is_value(value: &surrealdb::types::Value) -> bool {
T::is_value(value)
}
fn into_value(self) -> surrealdb::types::Value {
self.0.into_value()
}
fn from_value(value: surrealdb::types::Value) -> Result<Self, surrealdb::Error> {
T::from_value(value).map(Self)
}
}
impl SensitiveShape for String {
type Encrypted = Vec<u8>;
fn encrypt_with_context(
&self,
context: &crate::crypto::CryptoContext,
) -> Result<Self::Encrypted, crate::crypto::CryptoError> {
crate::crypto::encrypt_string(self, context)
}
fn decrypt_with_context(
encrypted: &Self::Encrypted,
context: &crate::crypto::CryptoContext,
) -> Result<Self, crate::crypto::CryptoError> {
crate::crypto::decrypt_string(encrypted, context)
}
}
impl<T> SensitiveValue for SensitiveValueOf<T>
where
T: serde::Serialize + serde::de::DeserializeOwned,
{
type Encrypted = Vec<u8>;
fn encrypt_value(
&self,
context: &crate::crypto::CryptoContext,
) -> Result<Self::Encrypted, crate::crypto::CryptoError> {
let plaintext =
serde_json::to_string(&self.0).map_err(|_| crate::crypto::CryptoError::Encrypt)?;
crate::crypto::encrypt_string(&plaintext, context)
}
fn decrypt_value(
encrypted: &Self::Encrypted,
context: &crate::crypto::CryptoContext,
) -> Result<Self, crate::crypto::CryptoError> {
let plaintext = crate::crypto::decrypt_string(encrypted, context)?;
serde_json::from_str(&plaintext)
.map(Self)
.map_err(|_| crate::crypto::CryptoError::Decrypt)
}
}
impl<T> SensitiveShape for T
where
T: Sensitive,
T::Encrypted:
Clone + serde::Serialize + serde::de::DeserializeOwned + surrealdb::types::SurrealValue,
{
type Encrypted = T::Encrypted;
fn encrypt_with_context(
&self,
context: &crate::crypto::CryptoContext,
) -> Result<Self::Encrypted, crate::crypto::CryptoError> {
<T as Sensitive>::encrypt(self, context)
}
fn decrypt_with_context(
encrypted: &Self::Encrypted,
context: &crate::crypto::CryptoContext,
) -> Result<Self, crate::crypto::CryptoError> {
<T as Sensitive>::decrypt(encrypted, context)
}
}
impl<T> SensitiveShape for SensitiveValueOf<T>
where
T: serde::Serialize + serde::de::DeserializeOwned,
{
type Encrypted = Vec<u8>;
fn encrypt_with_context(
&self,
context: &crate::crypto::CryptoContext,
) -> Result<Self::Encrypted, crate::crypto::CryptoError> {
<Self as SensitiveValue>::encrypt_value(self, context)
}
fn decrypt_with_context(
encrypted: &Self::Encrypted,
context: &crate::crypto::CryptoContext,
) -> Result<Self, crate::crypto::CryptoError> {
<Self as SensitiveValue>::decrypt_value(encrypted, context)
}
}
impl<T> SensitiveShape for Option<T>
where
T: SensitiveShape,
{
type Encrypted = Option<T::Encrypted>;
fn encrypt_with_context(
&self,
context: &crate::crypto::CryptoContext,
) -> Result<Self::Encrypted, crate::crypto::CryptoError> {
self.as_ref()
.map(|value| T::encrypt_with_context(value, context))
.transpose()
}
fn decrypt_with_context(
encrypted: &Self::Encrypted,
context: &crate::crypto::CryptoContext,
) -> Result<Self, crate::crypto::CryptoError> {
encrypted
.as_ref()
.map(|value| T::decrypt_with_context(value, context))
.transpose()
}
}
impl<T> SensitiveShape for Vec<T>
where
T: SensitiveShape,
{
type Encrypted = Vec<T::Encrypted>;
fn encrypt_with_context(
&self,
context: &crate::crypto::CryptoContext,
) -> Result<Self::Encrypted, crate::crypto::CryptoError> {
self.iter()
.map(|value| T::encrypt_with_context(value, context))
.collect()
}
fn decrypt_with_context(
encrypted: &Self::Encrypted,
context: &crate::crypto::CryptoContext,
) -> Result<Self, crate::crypto::CryptoError> {
encrypted
.iter()
.map(|value| T::decrypt_with_context(value, context))
.collect()
}
}
pub trait StoredModel: Sized {
type Stored: Clone
+ serde::Serialize
+ serde::de::DeserializeOwned
+ surrealdb::types::SurrealValue
+ Send;
fn into_stored(self) -> anyhow::Result<Self::Stored>;
fn from_stored(stored: Self::Stored) -> anyhow::Result<Self>;
fn supports_create_return_id() -> bool {
true
}
}
pub trait ForeignModel: StoredModel {
fn persist_foreign(
value: Self,
) -> impl std::future::Future<Output = anyhow::Result<Self::Stored>> + Send;
fn hydrate_foreign(
stored: Self::Stored,
) -> impl std::future::Future<Output = anyhow::Result<Self>> + Send;
fn has_foreign_fields() -> bool {
false
}
fn foreign_field_names() -> &'static [&'static str] {
&[]
}
fn has_relation_fields() -> bool {
false
}
fn relation_field_names() -> &'static [&'static str] {
&[]
}
fn strip_relation_fields(_row: &mut serde_json::Value) {}
fn inject_relation_values_from_model(
&self,
_row: &mut serde_json::Value,
) -> anyhow::Result<()> {
Ok(())
}
fn prepare_relation_writes(
&self,
_record: surrealdb::types::RecordId,
) -> impl std::future::Future<Output = anyhow::Result<Vec<RelationWrite>>> + Send {
async { Ok(vec![]) }
}
fn inject_relation_values_from_db(
_record: surrealdb::types::RecordId,
_row: &mut serde_json::Value,
) -> impl std::future::Future<Output = anyhow::Result<()>> + Send {
async { Ok(()) }
}
fn decode_stored_row(row: surrealdb::types::Value) -> anyhow::Result<Self::Stored>
where
Self::Stored: serde::de::DeserializeOwned,
{
Ok(serde_json::from_value(row.into_json_value())?)
}
}
pub trait ForeignPersistence: Send + Sync {
fn exists_record(
&self,
record: surrealdb::types::RecordId,
) -> impl std::future::Future<Output = anyhow::Result<bool>> + Send;
fn ensure_at<T>(
&self,
id: surrealdb::types::RecordId,
data: T,
) -> impl std::future::Future<Output = anyhow::Result<T>> + Send
where
T: model::meta::ModelMeta + repository::Crud + StoredModel + ForeignModel + Send;
fn create<T>(&self, data: T) -> impl std::future::Future<Output = anyhow::Result<T>> + Send
where
T: model::meta::ModelMeta + repository::Crud + StoredModel + ForeignModel + Send;
}
#[derive(Clone, Copy, Default)]
struct RepoForeignPersistence;
impl ForeignPersistence for RepoForeignPersistence {
async fn exists_record(&self, record: surrealdb::types::RecordId) -> anyhow::Result<bool> {
repository::record_exists(record).await
}
async fn ensure_at<T>(&self, id: surrealdb::types::RecordId, data: T) -> anyhow::Result<T>
where
T: model::meta::ModelMeta + repository::Crud + StoredModel + ForeignModel + Send,
{
repository::Repo::<T>::upsert_at(id, data).await
}
async fn create<T>(&self, data: T) -> anyhow::Result<T>
where
T: model::meta::ModelMeta + repository::Crud + StoredModel + ForeignModel + Send,
{
repository::Repo::<T>::create(data).await
}
}
tokio::task_local! {
static FOREIGN_SAVE_CLEANUP_STACK: std::cell::RefCell<Vec<surrealdb::types::RecordId>>;
}
fn foreign_cleanup_enabled() -> bool {
FOREIGN_SAVE_CLEANUP_STACK.try_with(|_| ()).is_ok()
}
fn foreign_cleanup_push(id: surrealdb::types::RecordId) {
let _ = FOREIGN_SAVE_CLEANUP_STACK.try_with(|stack| {
stack.borrow_mut().push(id);
});
}
pub(crate) async fn run_with_foreign_cleanup_scope<F, Fut, T>(
f: F,
) -> anyhow::Result<(T, Vec<surrealdb::types::RecordId>)>
where
F: FnOnce() -> Fut,
Fut: std::future::Future<Output = anyhow::Result<T>>,
{
FOREIGN_SAVE_CLEANUP_STACK
.scope(std::cell::RefCell::new(Vec::new()), async move {
let value = f().await?;
let cleanup = FOREIGN_SAVE_CLEANUP_STACK.with(|stack| stack.borrow().clone());
Ok((value, cleanup))
})
.await
}
pub fn rewrite_foreign_json_value(value: &mut serde_json::Value) {
match value {
serde_json::Value::Object(map) => {
if let Some(id) = map.get("id") {
*value = id.clone();
return;
}
for nested in map.values_mut() {
rewrite_foreign_json_value(nested);
}
}
serde_json::Value::Array(items) => {
for nested in items {
rewrite_foreign_json_value(nested);
}
}
_ => {}
}
}
pub(crate) fn parse_record_link_string(text: &str) -> Option<serde_json::Value> {
let (table, raw_key) = text.split_once(':')?;
if table.is_empty() || !is_valid_record_link_table(table) {
return None;
}
let key = if raw_key.starts_with('`') && raw_key.ends_with('`') {
raw_key.trim_matches('`')
} else {
if !looks_like_plain_record_link_key(raw_key) {
return None;
}
raw_key
};
if key.is_empty() {
return None;
}
let key = match key.parse::<i64>() {
Ok(number) => serde_json::json!({ "Number": number }),
Err(_) => serde_json::json!({ "String": key }),
};
Some(serde_json::json!({ "table": table, "key": key }))
}
pub(crate) fn parse_record_id_compat_string(text: &str) -> Option<RecordId> {
let (table, raw_key) = text.split_once(':')?;
if table.is_empty() || !is_valid_record_link_table(table) {
return None;
}
let key = raw_key.trim_matches('`');
if key.is_empty() {
return None;
}
Some(match key.parse::<i64>() {
Ok(number) => RecordId::new(table, number),
Err(_) => RecordId::new(table, key),
})
}
fn is_valid_record_link_table(table: &str) -> bool {
table
.chars()
.all(|ch| ch.is_ascii_alphanumeric() || ch == '_')
}
fn looks_like_plain_record_link_key(key: &str) -> bool {
!key.is_empty()
&& key
.chars()
.all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '_' | '-'))
}
fn rewrite_foreign_record_link_value(value: &mut serde_json::Value) {
match value {
serde_json::Value::String(text) => {
if let Some(record) = parse_record_link_string(text) {
*value = record;
}
}
serde_json::Value::Object(map) => {
if let Some(id) = map.get_mut("id") {
rewrite_foreign_record_link_value(id);
*value = id.clone();
return;
}
for nested in map.values_mut() {
rewrite_foreign_record_link_value(nested);
}
}
serde_json::Value::Array(items) => {
for nested in items {
rewrite_foreign_record_link_value(nested);
}
}
_ => {}
}
}
pub fn decode_record_link_value(value: &mut serde_json::Value) {
match value {
serde_json::Value::String(text) => {
if let Some(record) = parse_record_link_string(text) {
*value = record;
}
}
serde_json::Value::Object(map) => {
for nested in map.values_mut() {
decode_record_link_value(nested);
}
}
serde_json::Value::Array(items) => {
for nested in items {
decode_record_link_value(nested);
}
}
_ => {}
}
}
pub fn decode_stored_record_links(value: &mut serde_json::Value) {
rewrite_foreign_record_link_value(value);
}
pub async fn resolve_foreign_record_id<T>(value: T) -> anyhow::Result<surrealdb::types::RecordId>
where
T: model::meta::ModelMeta
+ model::meta::ResolveRecordId
+ repository::Crud
+ ForeignModel
+ Clone
+ Send
+ Sync,
{
resolve_foreign_record_id_with(&RepoForeignPersistence, value).await
}
pub async fn resolve_foreign_record_id_with<P, T>(
persistence: &P,
value: T,
) -> anyhow::Result<surrealdb::types::RecordId>
where
P: ForeignPersistence,
T: model::meta::ModelMeta
+ model::meta::ResolveRecordId
+ repository::Crud
+ ForeignModel
+ Clone
+ Send
+ Sync,
{
let explicit_record_id = explicit_foreign_record_id(&value)?;
if let Some(explicit_record_id) = explicit_record_id {
return persist_foreign_explicit_id(persistence, explicit_record_id, value).await;
}
persist_foreign_find_or_create(persistence, value).await
}
async fn persist_foreign_explicit_id<P, T>(
persistence: &P,
explicit_record_id: surrealdb::types::RecordId,
value: T,
) -> anyhow::Result<surrealdb::types::RecordId>
where
P: ForeignPersistence,
T: model::meta::ModelMeta
+ model::meta::ResolveRecordId
+ repository::Crud
+ ForeignModel
+ Clone
+ Send
+ Sync,
{
match value.resolve_record_id().await {
Ok(record_id) if record_id == explicit_record_id => {
persist_foreign_upsert_at(persistence, record_id, value).await
}
Ok(record_id) => Err(crate::error::DBError::InvalidModel(format!(
"foreign explicit id mismatch: serialized explicit id `{:?}` diverged from resolved record id `{:?}`",
explicit_record_id, record_id
))
.into()),
Err(err) => match crate::error::classify_db_error(&err) {
crate::error::DBError::MissingTable(_) | crate::error::DBError::NotFound => {
save_foreign_created(persistence.ensure_at(explicit_record_id, value).await?).await
}
other => Err(other.into()),
},
}
}
async fn persist_foreign_find_or_create<P, T>(
persistence: &P,
value: T,
) -> anyhow::Result<surrealdb::types::RecordId>
where
P: ForeignPersistence,
T: model::meta::ModelMeta
+ model::meta::ResolveRecordId
+ repository::Crud
+ ForeignModel
+ Clone
+ Send
+ Sync,
{
match value.resolve_record_id().await {
Ok(record_id) => persist_foreign_upsert_at(persistence, record_id, value).await,
Err(err) => match crate::error::classify_db_error(&err) {
crate::error::DBError::NotFound => {
save_foreign_created(persistence.create(value).await?).await
}
other => Err(other.into()),
},
}
}
async fn persist_foreign_upsert_at<P, T>(
persistence: &P,
record_id: surrealdb::types::RecordId,
value: T,
) -> anyhow::Result<surrealdb::types::RecordId>
where
P: ForeignPersistence,
T: model::meta::ModelMeta
+ model::meta::ResolveRecordId
+ repository::Crud
+ ForeignModel
+ Clone
+ Send
+ Sync,
{
if persistence.exists_record(record_id.clone()).await? {
Ok(persistence
.ensure_at(record_id, value)
.await?
.resolve_record_id()
.await?)
} else {
save_foreign_created(persistence.ensure_at(record_id, value).await?).await
}
}
async fn save_foreign_created<T>(saved: T) -> anyhow::Result<surrealdb::types::RecordId>
where
T: model::meta::ResolveRecordId,
{
let saved_id = saved.resolve_record_id().await?;
if foreign_cleanup_enabled() {
foreign_cleanup_push(saved_id.clone());
}
Ok(saved_id)
}
fn explicit_foreign_record_id<T>(value: &T) -> anyhow::Result<Option<surrealdb::types::RecordId>>
where
T: model::meta::ModelMeta + serde::Serialize,
{
let serde_json::Value::Object(map) = serde_json::to_value(value)? else {
return Ok(None);
};
let key = match map.get("id") {
Some(serde_json::Value::String(id)) if !id.is_empty() => {
surrealdb::types::RecordIdKey::from(id.clone())
}
Some(serde_json::Value::Number(id)) => {
surrealdb::types::RecordIdKey::from(id.as_i64().ok_or_else(|| {
anyhow::anyhow!(
"model `{}` has `id` but numeric id is out of i64 range",
std::any::type_name::<T>()
)
})?)
}
_ => return Ok(None),
};
Ok(Some(surrealdb::types::RecordId::new(
T::storage_table(),
key,
)))
}
#[::async_trait::async_trait]
impl<T> Bridge for T
where
T: model::meta::ModelMeta
+ model::meta::ResolveRecordId
+ repository::Crud
+ ForeignModel
+ Clone
+ Send
+ Sync,
{
async fn persist_foreign(self) -> anyhow::Result<surrealdb::types::RecordId> {
resolve_foreign_record_id(self).await
}
async fn hydrate_foreign(id: surrealdb::types::RecordId) -> anyhow::Result<Self> {
let mut value = serde_json::to_value(repository::Repo::<Self>::get_record(id).await?)?;
crate::serde_utils::id::normalize_public_root_id_value(&mut value);
Ok(serde_json::from_value(value)?)
}
}
#[::async_trait::async_trait]
impl<T> ForeignShape for T
where
T: Bridge + Send,
{
type Stored = surrealdb::types::RecordId;
async fn persist_foreign_shape(self) -> anyhow::Result<Self::Stored> {
<Self as Bridge>::persist_foreign(self).await
}
async fn hydrate_foreign_shape(stored: Self::Stored) -> anyhow::Result<Self> {
<Self as Bridge>::hydrate_foreign(stored).await
}
}
#[::async_trait::async_trait]
impl<T> ForeignShape for Option<T>
where
T: ForeignShape + Send,
T::Stored: Send,
{
type Stored = Option<T::Stored>;
async fn persist_foreign_shape(self) -> anyhow::Result<Self::Stored> {
match self {
Some(value) => Ok(Some(
<T as ForeignShape>::persist_foreign_shape(value).await?,
)),
None => Ok(None),
}
}
async fn hydrate_foreign_shape(stored: Self::Stored) -> anyhow::Result<Self> {
match stored {
Some(value) => Ok(Some(
<T as ForeignShape>::hydrate_foreign_shape(value).await?,
)),
None => Ok(None),
}
}
}
#[::async_trait::async_trait]
impl<T> ForeignShape for Vec<T>
where
T: ForeignShape + Send,
T::Stored: Send,
{
type Stored = Vec<T::Stored>;
async fn persist_foreign_shape(self) -> anyhow::Result<Self::Stored> {
let mut out = Vec::with_capacity(self.len());
for value in self {
out.push(<T as ForeignShape>::persist_foreign_shape(value).await?);
}
Ok(out)
}
async fn hydrate_foreign_shape(stored: Self::Stored) -> anyhow::Result<Self> {
let mut out = Vec::with_capacity(stored.len());
for value in stored {
out.push(<T as ForeignShape>::hydrate_foreign_shape(value).await?);
}
Ok(out)
}
}
#[::async_trait::async_trait]
impl<T> RelateShape for T
where
T: Bridge + Send,
{
async fn persist_relate_shape(self) -> anyhow::Result<Vec<surrealdb::types::RecordId>> {
Ok(vec![<Self as Bridge>::persist_foreign(self).await?])
}
async fn hydrate_relate_shape(stored: Vec<surrealdb::types::RecordId>) -> anyhow::Result<Self> {
match stored.as_slice() {
[record] => <Self as Bridge>::hydrate_foreign(record.clone()).await,
[] => Err(anyhow::anyhow!(
"required relate field is missing its relation edge"
)),
_ => Err(anyhow::anyhow!(
"required relate field received multiple relation edges"
)),
}
}
}
#[::async_trait::async_trait]
impl<T> RelateShape for Option<T>
where
T: Bridge + Send,
{
async fn persist_relate_shape(self) -> anyhow::Result<Vec<surrealdb::types::RecordId>> {
match self {
Some(value) => Ok(vec![<T as Bridge>::persist_foreign(value).await?]),
None => Ok(vec![]),
}
}
async fn hydrate_relate_shape(stored: Vec<surrealdb::types::RecordId>) -> anyhow::Result<Self> {
match stored.as_slice() {
[] => Ok(None),
[record] => Ok(Some(<T as Bridge>::hydrate_foreign(record.clone()).await?)),
_ => Err(anyhow::anyhow!(
"optional relate field received multiple relation edges"
)),
}
}
}
#[::async_trait::async_trait]
impl<T> RelateShape for Vec<T>
where
T: Bridge + Send,
{
async fn persist_relate_shape(self) -> anyhow::Result<Vec<surrealdb::types::RecordId>> {
let mut out = Vec::with_capacity(self.len());
for value in self {
out.push(<T as Bridge>::persist_foreign(value).await?);
}
Ok(out)
}
async fn hydrate_relate_shape(stored: Vec<surrealdb::types::RecordId>) -> anyhow::Result<Self> {
let mut out = Vec::with_capacity(stored.len());
for value in stored {
out.push(<T as Bridge>::hydrate_foreign(value).await?);
}
Ok(out)
}
}
#[::async_trait::async_trait]
impl<T> RelateShape for Option<Vec<T>>
where
T: Bridge + Send,
{
async fn persist_relate_shape(self) -> anyhow::Result<Vec<surrealdb::types::RecordId>> {
match self {
Some(values) => {
let mut out = Vec::with_capacity(values.len());
for value in values {
out.push(<T as Bridge>::persist_foreign(value).await?);
}
Ok(out)
}
None => Ok(vec![]),
}
}
async fn hydrate_relate_shape(stored: Vec<surrealdb::types::RecordId>) -> anyhow::Result<Self> {
if stored.is_empty() {
return Ok(None);
}
let mut out = Vec::with_capacity(stored.len());
for value in stored {
out.push(<T as Bridge>::hydrate_foreign(value).await?);
}
Ok(Some(out))
}
}