1use proc_macro::TokenStream;
2use quote::{format_ident, quote};
3use std::collections::HashSet;
4use syn::{
5 Attribute, Data, DeriveInput, Error, Field, Fields, GenericArgument, Meta, PathArguments, Type,
6 TypePath, parse_macro_input,
7};
8
9#[proc_macro_derive(Sensitive, attributes(secure, crypto))]
10pub fn derive_sensitive(input: TokenStream) -> TokenStream {
11 match derive_sensitive_impl(parse_macro_input!(input as DeriveInput)) {
12 Ok(tokens) => tokens.into(),
13 Err(err) => err.to_compile_error().into(),
14 }
15}
16
17#[proc_macro_derive(
18 Store,
19 attributes(unique, secure, foreign, table_as, crypto, relate, back_relate)
20)]
21pub fn derive_store(input: TokenStream) -> TokenStream {
22 match derive_store_impl(parse_macro_input!(input as DeriveInput)) {
23 Ok(tokens) => tokens.into(),
24 Err(err) => err.to_compile_error().into(),
25 }
26}
27
28#[proc_macro_derive(Relation, attributes(relation))]
29pub fn derive_relation(input: TokenStream) -> TokenStream {
30 match derive_relation_impl(parse_macro_input!(input as DeriveInput)) {
31 Ok(tokens) => tokens.into(),
32 Err(err) => err.to_compile_error().into(),
33 }
34}
35
36#[proc_macro_derive(Bridge)]
37pub fn derive_bridge(input: TokenStream) -> TokenStream {
38 match derive_bridge_impl(parse_macro_input!(input as DeriveInput)) {
39 Ok(tokens) => tokens.into(),
40 Err(err) => err.to_compile_error().into(),
41 }
42}
43
44fn derive_store_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
45 let struct_ident = input.ident;
46 let vis = input.vis.clone();
47 let table_alias = table_alias_target(&input.attrs)?;
48
49 let named_fields = match input.data {
50 Data::Struct(data) => match data.fields {
51 Fields::Named(fields) => fields.named,
52 _ => {
53 return Err(Error::new_spanned(
54 struct_ident,
55 "Store can only be derived for structs with named fields",
56 ));
57 }
58 },
59 _ => {
60 return Err(Error::new_spanned(
61 struct_ident,
62 "Store can only be derived for structs",
63 ));
64 }
65 };
66
67 let id_fields = named_fields
68 .iter()
69 .filter(|field| is_id_type(&field.ty))
70 .map(|field| field.ident.clone().expect("named field"))
71 .collect::<Vec<_>>();
72
73 let secure_fields = named_fields
74 .iter()
75 .filter(|field| has_secure_attr(&field.attrs))
76 .map(|field| field.ident.clone().expect("named field"))
77 .collect::<Vec<_>>();
78
79 let unique_fields = named_fields
80 .iter()
81 .filter(|field| has_unique_attr(&field.attrs))
82 .map(|field| field.ident.clone().expect("named field"))
83 .collect::<Vec<_>>();
84
85 if id_fields.len() > 1 {
86 return Err(Error::new_spanned(
87 struct_ident,
88 "Store supports at most one `Id` field for automatic HasId generation",
89 ));
90 }
91
92 if let Some(invalid_field) = named_fields
93 .iter()
94 .find(|field| has_secure_attr(&field.attrs) && has_unique_attr(&field.attrs))
95 {
96 let ident = invalid_field.ident.as_ref().expect("named field");
97 return Err(Error::new_spanned(
98 ident,
99 "#[secure] fields cannot be used as #[unique] lookup keys",
100 ));
101 }
102
103 let foreign_fields = named_fields
104 .iter()
105 .filter_map(|field| match field_foreign_attr(field) {
106 Ok(Some(attr)) => Some(parse_foreign_field(field, attr)),
107 Ok(None) => None,
108 Err(err) => Some(Err(err)),
109 })
110 .collect::<syn::Result<Vec<_>>>()?;
111
112 let relate_fields = named_fields
113 .iter()
114 .filter_map(|field| match field_relation_attr(field) {
115 Ok(Some(attr)) => Some(parse_relate_field(field, attr)),
116 Ok(None) => None,
117 Err(err) => Some(Err(err)),
118 })
119 .collect::<syn::Result<Vec<_>>>()?;
120
121 if let Some(non_store_child) = foreign_fields
122 .iter()
123 .find_map(|field| invalid_foreign_leaf_type(&field.kind.original_ty))
124 {
125 return Err(Error::new_spanned(
126 non_store_child,
127 BINDREF_BRIDGE_STORE_ONLY,
128 ));
129 }
130
131 if let Some(invalid_field) = named_fields.iter().find(|field| {
132 field_foreign_attr(field).ok().flatten().is_some() && has_unique_attr(&field.attrs)
133 }) {
134 let ident = invalid_field.ident.as_ref().expect("named field");
135 return Err(Error::new_spanned(
136 ident,
137 "#[foreign] fields cannot be used as #[unique] lookup keys",
138 ));
139 }
140
141 if let Some(invalid_field) = named_fields.iter().find_map(|field| {
142 field_relation_attr(field)
143 .ok()
144 .flatten()
145 .filter(|_| has_unique_attr(&field.attrs))
146 .map(|attr| (field, attr))
147 }) {
148 let ident = invalid_field.0.ident.as_ref().expect("named field");
149 return Err(Error::new_spanned(
150 ident,
151 format!(
152 "{} fields cannot be used as #[unique] lookup keys",
153 relation_attr_label(invalid_field.1)
154 ),
155 ));
156 }
157
158 if let Some(invalid_field) = named_fields.iter().find_map(|field| {
159 field_relation_attr(field)
160 .ok()
161 .flatten()
162 .filter(|_| has_secure_attr(&field.attrs))
163 .map(|attr| (field, attr))
164 }) {
165 let ident = invalid_field.0.ident.as_ref().expect("named field");
166 return Err(Error::new_spanned(
167 ident,
168 format!(
169 "{} fields cannot be marked #[secure]",
170 relation_attr_label(invalid_field.1)
171 ),
172 ));
173 }
174
175 if let Some(invalid_field) = named_fields.iter().find_map(|field| {
176 field_relation_attr(field)
177 .ok()
178 .flatten()
179 .filter(|_| field_foreign_attr(field).ok().flatten().is_some())
180 .map(|attr| (field, attr))
181 }) {
182 let ident = invalid_field.0.ident.as_ref().expect("named field");
183 return Err(Error::new_spanned(
184 ident,
185 format!(
186 "{} cannot be combined with #[foreign]",
187 relation_attr_label(invalid_field.1)
188 ),
189 ));
190 }
191
192 let mut seen_relation_names = HashSet::new();
193 for field in &relate_fields {
194 if !seen_relation_names.insert(field.relation_name.clone()) {
195 return Err(Error::new_spanned(
196 &field.ident,
197 format!(
198 "duplicate {} relation name is not supported within one Store model",
199 field.direction.attr_label()
200 ),
201 ));
202 }
203 }
204
205 let auto_has_id_impl = id_fields.first().map(|field| {
206 quote! {
207 impl ::appdb::model::meta::HasId for #struct_ident {
208 fn id(&self) -> ::surrealdb::types::RecordId {
209 ::surrealdb::types::RecordId::new(
210 <Self as ::appdb::model::meta::ModelMeta>::storage_table(),
211 self.#field.clone(),
212 )
213 }
214 }
215 }
216 });
217
218 let resolve_record_id_impl = if let Some(field) = id_fields.first() {
219 quote! {
220 #[::async_trait::async_trait]
221 impl ::appdb::model::meta::ResolveRecordId for #struct_ident {
222 async fn resolve_record_id(&self) -> ::anyhow::Result<::surrealdb::types::RecordId> {
223 Ok(::surrealdb::types::RecordId::new(
224 <Self as ::appdb::model::meta::ModelMeta>::storage_table(),
225 self.#field.clone(),
226 ))
227 }
228 }
229 }
230 } else {
231 quote! {
232 #[::async_trait::async_trait]
233 impl ::appdb::model::meta::ResolveRecordId for #struct_ident {
234 async fn resolve_record_id(&self) -> ::anyhow::Result<::surrealdb::types::RecordId> {
235 ::appdb::repository::Repo::<Self>::find_unique_id_for(self).await
236 }
237 }
238 }
239 };
240
241 let resolved_table_name_expr = if let Some(target_ty) = &table_alias {
242 quote! { <#target_ty as ::appdb::model::meta::ModelMeta>::table_name() }
243 } else {
244 quote! {
245 {
246 let table = ::appdb::model::meta::default_table_name(stringify!(#struct_ident));
247 ::appdb::model::meta::register_table(stringify!(#struct_ident), table)
248 }
249 }
250 };
251
252 let unique_schema_impls = unique_fields.iter().map(|field| {
253 let field_name = field.to_string();
254 let index_name = format!(
255 "{}_{}_unique",
256 resolved_schema_table_name(&struct_ident, table_alias.as_ref()),
257 field_name
258 );
259 let ddl = format!(
260 "DEFINE INDEX IF NOT EXISTS {index_name} ON {} FIELDS {field_name} UNIQUE;",
261 resolved_schema_table_name(&struct_ident, table_alias.as_ref())
262 );
263
264 quote! {
265 ::inventory::submit! {
266 ::appdb::model::schema::SchemaItem {
267 ddl: #ddl,
268 }
269 }
270 }
271 });
272
273 let lookup_fields = if unique_fields.is_empty() {
274 named_fields
275 .iter()
276 .filter_map(|field| {
277 let ident = field.ident.as_ref()?;
278 if ident == "id"
279 || secure_fields.iter().any(|secure| secure == ident)
280 || foreign_fields.iter().any(|foreign| foreign.ident == *ident)
281 || relate_fields.iter().any(|relate| relate.ident == *ident)
282 {
283 None
284 } else {
285 Some(ident.to_string())
286 }
287 })
288 .collect::<Vec<_>>()
289 } else {
290 unique_fields
291 .iter()
292 .map(|field| field.to_string())
293 .collect::<Vec<_>>()
294 };
295
296 let foreign_field_literals = foreign_fields
297 .iter()
298 .map(|field| field.ident.to_string())
299 .map(|field| quote! { #field })
300 .collect::<Vec<_>>();
301 let relate_field_literals = relate_fields
302 .iter()
303 .map(|field| field.ident.to_string())
304 .map(|field| quote! { #field })
305 .collect::<Vec<_>>();
306 if id_fields.is_empty() && lookup_fields.is_empty() {
307 return Err(Error::new_spanned(
308 struct_ident,
309 "Store requires an `Id` field or at least one non-secure lookup field for automatic record resolution",
310 ));
311 }
312 let lookup_field_literals = lookup_fields.iter().map(|field| quote! { #field });
313
314 let stored_model_impl = if !foreign_fields.is_empty() {
315 quote! {}
316 } else if secure_field_count(&named_fields) > 0 {
317 quote! {
318 impl ::appdb::StoredModel for #struct_ident {
319 type Stored = <Self as ::appdb::Sensitive>::Encrypted;
320
321 fn into_stored(self) -> ::anyhow::Result<Self::Stored> {
322 <Self as ::appdb::Sensitive>::encrypt_with_runtime_resolver(&self)
323 .map_err(::anyhow::Error::from)
324 }
325
326 fn from_stored(stored: Self::Stored) -> ::anyhow::Result<Self> {
327 <Self as ::appdb::Sensitive>::decrypt_with_runtime_resolver(&stored)
328 .map_err(::anyhow::Error::from)
329 }
330
331 fn supports_create_return_id() -> bool {
332 false
333 }
334 }
335 }
336 } else {
337 quote! {
338 impl ::appdb::StoredModel for #struct_ident {
339 type Stored = Self;
340
341 fn into_stored(self) -> ::anyhow::Result<Self::Stored> {
342 ::std::result::Result::Ok(self)
343 }
344
345 fn from_stored(stored: Self::Stored) -> ::anyhow::Result<Self> {
346 ::std::result::Result::Ok(stored)
347 }
348 }
349 }
350 };
351
352 let stored_fields = named_fields.iter().map(|field| {
353 let ident = field.ident.clone().expect("named field");
354 let ty = stored_field_type(field, &foreign_fields);
355 if is_record_id_type(&ty) {
356 quote! {
357 #[serde(deserialize_with = "::appdb::serde_utils::id::deserialize_record_id_or_compat_string")]
358 #ident: #ty
359 }
360 } else {
361 quote! { #ident: #ty }
362 }
363 });
364
365 let into_stored_assignments = named_fields.iter().map(|field| {
366 let ident = field.ident.clone().expect("named field");
367 match foreign_field_kind(&ident, &foreign_fields) {
368 Some(ForeignFieldKind { original_ty, .. }) => quote! {
369 #ident: <#original_ty as ::appdb::ForeignShape>::persist_foreign_shape(value.#ident).await?
370 },
371 None => quote! { #ident: value.#ident },
372 }
373 });
374
375 let from_stored_assignments = named_fields.iter().map(|field| {
376 let ident = field.ident.clone().expect("named field");
377 match foreign_field_kind(&ident, &foreign_fields) {
378 Some(ForeignFieldKind { original_ty, .. }) => quote! {
379 #ident: <#original_ty as ::appdb::ForeignShape>::hydrate_foreign_shape(stored.#ident).await?
380 },
381 None => quote! { #ident: stored.#ident },
382 }
383 });
384
385 let decode_foreign_fields = foreign_fields.iter().map(|field| {
386 let ident = field.ident.to_string();
387 quote! {
388 if let ::std::option::Option::Some(value) = map.get_mut(#ident) {
389 ::appdb::decode_stored_record_links(value);
390 }
391 }
392 });
393
394 let relation_methods_impl = if relate_fields.is_empty() {
395 quote! {}
396 } else {
397 let strip_relation_fields = relate_fields.iter().map(|field| {
398 let ident = field.ident.to_string();
399 quote! {
400 map.remove(#ident);
401 }
402 });
403
404 let inject_relation_values_from_model = relate_fields.iter().map(|field| {
405 let ident = &field.ident;
406 let name = ident.to_string();
407 quote! {
408 map.insert(#name.to_owned(), ::serde_json::to_value(&self.#ident)?);
409 }
410 });
411
412 let prepare_relation_writes = relate_fields.iter().map(|field| {
413 let ident = &field.ident;
414 let relation_name = &field.relation_name;
415 let field_ty = &field.field_ty;
416 let write_direction = field.direction.write_direction_tokens();
417 let write_edges = field.direction.write_edges_tokens();
418 quote! {
419 {
420 let ids = <#field_ty as ::appdb::RelateShape>::persist_relate_shape(self.#ident.clone()).await?;
421 writes.push(::appdb::RelationWrite {
422 relation: #relation_name,
423 record: record.clone(),
424 direction: #write_direction,
425 edges: #write_edges,
426 });
427 }
428 }
429 });
430
431 let inject_relation_values_from_db = relate_fields.iter().map(|field| {
432 let relation_name = &field.relation_name;
433 let field_ty = &field.field_ty;
434 let ident = field.ident.to_string();
435 let load_relation_ids = field.direction.load_edges_tokens(relation_name);
436 quote! {
437 {
438 let value = <#field_ty as ::appdb::RelateShape>::hydrate_relate_shape(#load_relation_ids).await?;
439 map.insert(#ident.to_owned(), ::serde_json::to_value(value)?);
440 }
441 }
442 });
443
444 quote! {
445 fn has_relation_fields() -> bool {
446 true
447 }
448
449 fn relation_field_names() -> &'static [&'static str] {
450 &[ #( #relate_field_literals ),* ]
451 }
452
453 fn strip_relation_fields(row: &mut ::serde_json::Value) {
454 if let ::serde_json::Value::Object(map) = row {
455 #( #strip_relation_fields )*
456 }
457 }
458
459 fn inject_relation_values_from_model(
460 &self,
461 row: &mut ::serde_json::Value,
462 ) -> ::anyhow::Result<()> {
463 if let ::serde_json::Value::Object(map) = row {
464 #( #inject_relation_values_from_model )*
465 }
466 Ok(())
467 }
468
469 fn prepare_relation_writes(
470 &self,
471 record: ::surrealdb::types::RecordId,
472 ) -> impl ::std::future::Future<Output = ::anyhow::Result<::std::vec::Vec<::appdb::RelationWrite>>> + Send {
473 async move {
474 let mut writes = ::std::vec::Vec::new();
475 #( #prepare_relation_writes )*
476 Ok(writes)
477 }
478 }
479
480 fn inject_relation_values_from_db(
481 record: ::surrealdb::types::RecordId,
482 row: &mut ::serde_json::Value,
483 ) -> impl ::std::future::Future<Output = ::anyhow::Result<()>> + Send {
484 async move {
485 if let ::serde_json::Value::Object(map) = row {
486 #( #inject_relation_values_from_db )*
487 }
488 Ok(())
489 }
490 }
491 }
492 };
493
494 let foreign_model_impl = if foreign_fields.is_empty() {
495 quote! {
496 impl ::appdb::ForeignModel for #struct_ident {
497 async fn persist_foreign(value: Self) -> ::anyhow::Result<Self::Stored> {
498 <Self as ::appdb::StoredModel>::into_stored(value)
499 }
500
501 async fn hydrate_foreign(stored: Self::Stored) -> ::anyhow::Result<Self> {
502 <Self as ::appdb::StoredModel>::from_stored(stored)
503 }
504
505 fn decode_stored_row(
506 row: ::surrealdb::types::Value,
507 ) -> ::anyhow::Result<Self::Stored>
508 where
509 Self::Stored: ::serde::de::DeserializeOwned,
510 {
511 Ok(::serde_json::from_value(row.into_json_value())?)
512 }
513
514 #relation_methods_impl
515 }
516 }
517 } else {
518 let stored_struct_ident = format_ident!("AppdbStored{}", struct_ident);
519 quote! {
520 #[derive(
521 Debug,
522 Clone,
523 ::serde::Serialize,
524 ::serde::Deserialize,
525 ::surrealdb::types::SurrealValue,
526 )]
527 #vis struct #stored_struct_ident {
528 #( #stored_fields, )*
529 }
530
531 impl ::appdb::StoredModel for #struct_ident {
532 type Stored = #stored_struct_ident;
533
534 fn into_stored(self) -> ::anyhow::Result<Self::Stored> {
535 unreachable!("foreign fields require async persist_foreign")
536 }
537
538 fn from_stored(_stored: Self::Stored) -> ::anyhow::Result<Self> {
539 unreachable!("foreign fields require async hydrate_foreign")
540 }
541 }
542
543 impl ::appdb::ForeignModel for #struct_ident {
544 async fn persist_foreign(value: Self) -> ::anyhow::Result<Self::Stored> {
545 let value = value;
546 Ok(#stored_struct_ident {
547 #( #into_stored_assignments, )*
548 })
549 }
550
551 async fn hydrate_foreign(stored: Self::Stored) -> ::anyhow::Result<Self> {
552 Ok(Self {
553 #( #from_stored_assignments, )*
554 })
555 }
556
557 fn has_foreign_fields() -> bool {
558 true
559 }
560
561 fn foreign_field_names() -> &'static [&'static str] {
562 &[ #( #foreign_field_literals ),* ]
563 }
564
565 fn decode_stored_row(
566 row: ::surrealdb::types::Value,
567 ) -> ::anyhow::Result<Self::Stored>
568 where
569 Self::Stored: ::serde::de::DeserializeOwned,
570 {
571 let mut row = row.into_json_value();
572 if let ::serde_json::Value::Object(map) = &mut row {
573 #( #decode_foreign_fields )*
574 }
575 Ok(::serde_json::from_value(row)?)
576 }
577
578 #relation_methods_impl
579 }
580 }
581 };
582
583 let store_marker_ident = format_ident!("AppdbStoreMarker{}", struct_ident);
584
585 Ok(quote! {
586 #[doc(hidden)]
587 #vis struct #store_marker_ident;
588
589 impl ::appdb::model::meta::ModelMeta for #struct_ident {
590 fn storage_table() -> &'static str {
591 #resolved_table_name_expr
592 }
593
594 fn table_name() -> &'static str {
595 static TABLE_NAME: ::std::sync::OnceLock<&'static str> = ::std::sync::OnceLock::new();
596 TABLE_NAME.get_or_init(|| {
597 let table = #resolved_table_name_expr;
598 ::appdb::model::meta::register_table(stringify!(#struct_ident), table)
599 })
600 }
601 }
602
603 impl ::appdb::model::meta::StoreModelMarker for #struct_ident {}
604 impl ::appdb::model::meta::StoreModelMarker for #store_marker_ident {}
605
606 impl ::appdb::model::meta::UniqueLookupMeta for #struct_ident {
607 fn lookup_fields() -> &'static [&'static str] {
608 &[ #( #lookup_field_literals ),* ]
609 }
610
611 fn foreign_fields() -> &'static [&'static str] {
612 &[ #( #foreign_field_literals ),* ]
613 }
614 }
615 #stored_model_impl
616 #foreign_model_impl
617
618 #auto_has_id_impl
619 #resolve_record_id_impl
620
621 #( #unique_schema_impls )*
622
623 impl ::appdb::repository::Crud for #struct_ident {}
624
625 impl #struct_ident {
626 pub async fn save(self) -> ::anyhow::Result<Self> {
632 <Self as ::appdb::repository::Crud>::save(self).await
633 }
634
635 pub async fn save_many(data: ::std::vec::Vec<Self>) -> ::anyhow::Result<::std::vec::Vec<Self>> {
637 <Self as ::appdb::repository::Crud>::save_many(data).await
638 }
639
640 pub async fn get<T>(id: T) -> ::anyhow::Result<Self>
641 where
642 ::surrealdb::types::RecordIdKey: From<T>,
643 T: Send,
644 {
645 ::appdb::repository::Repo::<Self>::get(id).await
646 }
647
648 pub async fn list() -> ::anyhow::Result<::std::vec::Vec<Self>> {
649 ::appdb::repository::Repo::<Self>::list().await
650 }
651
652 pub async fn list_limit(count: i64) -> ::anyhow::Result<::std::vec::Vec<Self>> {
653 ::appdb::repository::Repo::<Self>::list_limit(count).await
654 }
655
656 pub async fn relate_by_name<Target>(&self, target: &Target, relation: &str) -> ::anyhow::Result<()>
657 where
658 Target: ::appdb::model::meta::ResolveRecordId + Send + Sync,
659 {
660 <Self as ::appdb::graph::GraphCrud>::relate_by_name(self, target, relation).await
661 }
662
663 pub async fn back_relate_by_name<Target>(&self, target: &Target, relation: &str) -> ::anyhow::Result<()>
664 where
665 Target: ::appdb::model::meta::ResolveRecordId + Send + Sync,
666 {
667 <Self as ::appdb::graph::GraphCrud>::back_relate_by_name(self, target, relation).await
668 }
669
670 pub async fn unrelate_by_name<Target>(&self, target: &Target, relation: &str) -> ::anyhow::Result<()>
671 where
672 Target: ::appdb::model::meta::ResolveRecordId + Send + Sync,
673 {
674 <Self as ::appdb::graph::GraphCrud>::unrelate_by_name(self, target, relation).await
675 }
676
677 pub async fn outgoing_ids(&self, relation: &str) -> ::anyhow::Result<::std::vec::Vec<::surrealdb::types::RecordId>> {
678 <Self as ::appdb::repository::Crud>::outgoing_ids(self, relation).await
679 }
680
681 pub async fn outgoing<Target>(&self, relation: &str) -> ::anyhow::Result<::std::vec::Vec<Target>>
682 where
683 Target: ::appdb::model::meta::ModelMeta + ::appdb::StoredModel + ::appdb::ForeignModel,
684 <Target as ::appdb::StoredModel>::Stored: ::serde::de::DeserializeOwned,
685 {
686 <Self as ::appdb::repository::Crud>::outgoing::<Target>(self, relation).await
687 }
688
689 pub async fn outgoing_count(&self, relation: &str) -> ::anyhow::Result<i64> {
690 <Self as ::appdb::repository::Crud>::outgoing_count(self, relation).await
691 }
692
693 pub async fn outgoing_count_as<Target>(&self, relation: &str) -> ::anyhow::Result<i64>
694 where
695 Target: ::appdb::model::meta::ModelMeta + ::appdb::StoredModel + ::appdb::ForeignModel,
696 {
697 <Self as ::appdb::repository::Crud>::outgoing_count_as::<Target>(self, relation).await
698 }
699
700 pub async fn incoming_ids(&self, relation: &str) -> ::anyhow::Result<::std::vec::Vec<::surrealdb::types::RecordId>> {
701 <Self as ::appdb::repository::Crud>::incoming_ids(self, relation).await
702 }
703
704 pub async fn incoming<Target>(&self, relation: &str) -> ::anyhow::Result<::std::vec::Vec<Target>>
705 where
706 Target: ::appdb::model::meta::ModelMeta + ::appdb::StoredModel + ::appdb::ForeignModel,
707 <Target as ::appdb::StoredModel>::Stored: ::serde::de::DeserializeOwned,
708 {
709 <Self as ::appdb::repository::Crud>::incoming::<Target>(self, relation).await
710 }
711
712 pub async fn incoming_count(&self, relation: &str) -> ::anyhow::Result<i64> {
713 <Self as ::appdb::repository::Crud>::incoming_count(self, relation).await
714 }
715
716 pub async fn incoming_count_as<Target>(&self, relation: &str) -> ::anyhow::Result<i64>
717 where
718 Target: ::appdb::model::meta::ModelMeta + ::appdb::StoredModel + ::appdb::ForeignModel,
719 {
720 <Self as ::appdb::repository::Crud>::incoming_count_as::<Target>(self, relation).await
721 }
722
723 pub async fn delete_all() -> ::anyhow::Result<()> {
724 ::appdb::repository::Repo::<Self>::delete_all().await
725 }
726
727 pub async fn find_one_id(
728 k: &str,
729 v: &str,
730 ) -> ::anyhow::Result<::surrealdb::types::RecordId> {
731 ::appdb::repository::Repo::<Self>::find_one_id(k, v).await
732 }
733
734 pub async fn list_record_ids() -> ::anyhow::Result<::std::vec::Vec<::surrealdb::types::RecordId>> {
735 ::appdb::repository::Repo::<Self>::list_record_ids().await
736 }
737
738 pub async fn create_at(
739 id: ::surrealdb::types::RecordId,
740 data: Self,
741 ) -> ::anyhow::Result<Self> {
742 ::appdb::repository::Repo::<Self>::create_at(id, data).await
743 }
744
745 pub async fn upsert_at(
746 id: ::surrealdb::types::RecordId,
747 data: Self,
748 ) -> ::anyhow::Result<Self> {
749 ::appdb::repository::Repo::<Self>::upsert_at(id, data).await
750 }
751
752 pub async fn update_at(
753 self,
754 id: ::surrealdb::types::RecordId,
755 ) -> ::anyhow::Result<Self> {
756 ::appdb::repository::Repo::<Self>::update_at(id, self).await
757 }
758
759
760 pub async fn delete<T>(id: T) -> ::anyhow::Result<()>
761 where
762 ::surrealdb::types::RecordIdKey: From<T>,
763 T: Send,
764 {
765 ::appdb::repository::Repo::<Self>::delete(id).await
766 }
767 }
768 })
769}
770
771fn derive_bridge_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
772 let enum_ident = input.ident;
773
774 let variants = match input.data {
775 Data::Enum(data) => data.variants,
776 _ => {
777 return Err(Error::new_spanned(
778 enum_ident,
779 "Bridge can only be derived for enums",
780 ));
781 }
782 };
783
784 let payloads = variants
785 .iter()
786 .map(parse_bridge_variant)
787 .collect::<syn::Result<Vec<_>>>()?;
788
789 let from_impls = payloads.iter().map(|variant| {
790 let variant_ident = &variant.variant_ident;
791 let payload_ty = &variant.payload_ty;
792
793 quote! {
794 impl ::std::convert::From<#payload_ty> for #enum_ident {
795 fn from(value: #payload_ty) -> Self {
796 Self::#variant_ident(value)
797 }
798 }
799 }
800 });
801
802 let persist_match_arms = payloads.iter().map(|variant| {
803 let variant_ident = &variant.variant_ident;
804
805 quote! {
806 Self::#variant_ident(value) => <_ as ::appdb::Bridge>::persist_foreign(value).await,
807 }
808 });
809
810 let hydrate_match_arms = payloads.iter().map(|variant| {
811 let variant_ident = &variant.variant_ident;
812 let payload_ty = &variant.payload_ty;
813
814 quote! {
815 table if table == <#payload_ty as ::appdb::model::meta::ModelMeta>::storage_table() => {
816 ::std::result::Result::Ok(Self::#variant_ident(
817 <#payload_ty as ::appdb::Bridge>::hydrate_foreign(id).await?,
818 ))
819 }
820 }
821 });
822
823 Ok(quote! {
824 #( #from_impls )*
825
826 #[::async_trait::async_trait]
827 impl ::appdb::Bridge for #enum_ident {
828 async fn persist_foreign(self) -> ::anyhow::Result<::surrealdb::types::RecordId> {
829 match self {
830 #( #persist_match_arms )*
831 }
832 }
833
834 async fn hydrate_foreign(
835 id: ::surrealdb::types::RecordId,
836 ) -> ::anyhow::Result<Self> {
837 match id.table.to_string().as_str() {
838 #( #hydrate_match_arms, )*
839 table => ::anyhow::bail!(
840 "unsupported foreign table `{table}` for enum dispatcher `{}`",
841 ::std::stringify!(#enum_ident)
842 ),
843 }
844 }
845 }
846 })
847}
848
849#[derive(Clone)]
850struct BridgeVariant {
851 variant_ident: syn::Ident,
852 payload_ty: Type,
853}
854
855fn parse_bridge_variant(variant: &syn::Variant) -> syn::Result<BridgeVariant> {
856 let payload_ty = match &variant.fields {
857 Fields::Unnamed(fields) if fields.unnamed.len() == 1 => {
858 fields.unnamed.first().expect("single field").ty.clone()
859 }
860 Fields::Unnamed(_) => {
861 return Err(Error::new_spanned(
862 &variant.ident,
863 "Bridge variants must be single-field tuple variants",
864 ));
865 }
866 Fields::Unit => {
867 return Err(Error::new_spanned(
868 &variant.ident,
869 "Bridge does not support unit variants",
870 ));
871 }
872 Fields::Named(_) => {
873 return Err(Error::new_spanned(
874 &variant.ident,
875 "Bridge does not support struct variants",
876 ));
877 }
878 };
879
880 let payload_path = match &payload_ty {
881 Type::Path(path) => path,
882 _ => {
883 return Err(Error::new_spanned(
884 &payload_ty,
885 "Bridge payload must implement appdb::Bridge",
886 ));
887 }
888 };
889
890 let segment = payload_path.path.segments.last().ok_or_else(|| {
891 Error::new_spanned(&payload_ty, "Bridge payload must implement appdb::Bridge")
892 })?;
893
894 if !matches!(segment.arguments, PathArguments::None) {
895 return Err(Error::new_spanned(
896 &payload_ty,
897 "Bridge payload must implement appdb::Bridge",
898 ));
899 }
900
901 Ok(BridgeVariant {
902 variant_ident: variant.ident.clone(),
903 payload_ty,
904 })
905}
906
907fn derive_relation_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
908 let struct_ident = input.ident;
909 let relation_name = relation_name_override(&input.attrs)?
910 .unwrap_or_else(|| to_snake_case(&struct_ident.to_string()));
911
912 match input.data {
913 Data::Struct(data) => match data.fields {
914 Fields::Unit | Fields::Named(_) => {}
915 _ => {
916 return Err(Error::new_spanned(
917 struct_ident,
918 "Relation can only be derived for unit structs or structs with named fields",
919 ));
920 }
921 },
922 _ => {
923 return Err(Error::new_spanned(
924 struct_ident,
925 "Relation can only be derived for structs",
926 ));
927 }
928 }
929
930 Ok(quote! {
931 impl ::appdb::model::relation::RelationMeta for #struct_ident {
932 fn relation_name() -> &'static str {
933 static REL_NAME: ::std::sync::OnceLock<&'static str> = ::std::sync::OnceLock::new();
934 REL_NAME.get_or_init(|| ::appdb::model::relation::register_relation(#relation_name))
935 }
936 }
937
938 impl #struct_ident {
939 pub async fn relate<A, B>(a: &A, b: &B) -> ::anyhow::Result<()>
940 where
941 A: ::appdb::model::meta::ResolveRecordId + Send + Sync,
942 B: ::appdb::model::meta::ResolveRecordId + Send + Sync,
943 {
944 ::appdb::graph::relate_at(a.resolve_record_id().await?, b.resolve_record_id().await?, <Self as ::appdb::model::relation::RelationMeta>::relation_name()).await
945 }
946
947 pub async fn back_relate<A, B>(a: &A, b: &B) -> ::anyhow::Result<()>
948 where
949 A: ::appdb::model::meta::ResolveRecordId + Send + Sync,
950 B: ::appdb::model::meta::ResolveRecordId + Send + Sync,
951 {
952 ::appdb::graph::back_relate_at(a.resolve_record_id().await?, b.resolve_record_id().await?, <Self as ::appdb::model::relation::RelationMeta>::relation_name()).await
953 }
954
955 pub async fn unrelate<A, B>(a: &A, b: &B) -> ::anyhow::Result<()>
956 where
957 A: ::appdb::model::meta::ResolveRecordId + Send + Sync,
958 B: ::appdb::model::meta::ResolveRecordId + Send + Sync,
959 {
960 ::appdb::graph::unrelate_at(a.resolve_record_id().await?, b.resolve_record_id().await?, <Self as ::appdb::model::relation::RelationMeta>::relation_name()).await
961 }
962
963 pub async fn out_ids<A>(a: &A, out_table: &str) -> ::anyhow::Result<::std::vec::Vec<::surrealdb::types::RecordId>>
964 where
965 A: ::appdb::model::meta::ResolveRecordId + Send + Sync,
966 {
967 ::appdb::graph::out_ids(a.resolve_record_id().await?, <Self as ::appdb::model::relation::RelationMeta>::relation_name(), out_table).await
968 }
969
970 pub async fn in_ids<B>(b: &B, in_table: &str) -> ::anyhow::Result<::std::vec::Vec<::surrealdb::types::RecordId>>
971 where
972 B: ::appdb::model::meta::ResolveRecordId + Send + Sync,
973 {
974 ::appdb::graph::in_ids(b.resolve_record_id().await?, <Self as ::appdb::model::relation::RelationMeta>::relation_name(), in_table).await
975 }
976 }
977 })
978}
979
980fn derive_sensitive_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
981 let struct_ident = input.ident;
982 let encrypted_ident = format_ident!("Encrypted{}", struct_ident);
983 let vis = input.vis;
984 let type_crypto_config = type_crypto_config(&input.attrs)?;
985 let named_fields = match input.data {
986 Data::Struct(data) => match data.fields {
987 Fields::Named(fields) => fields.named,
988 _ => {
989 return Err(Error::new_spanned(
990 struct_ident,
991 "Sensitive can only be derived for structs with named fields",
992 ));
993 }
994 },
995 _ => {
996 return Err(Error::new_spanned(
997 struct_ident,
998 "Sensitive can only be derived for structs",
999 ));
1000 }
1001 };
1002
1003 let mut secure_field_count = 0usize;
1004 let mut encrypted_fields = Vec::new();
1005 let mut encrypt_assignments = Vec::new();
1006 let mut decrypt_assignments = Vec::new();
1007 let mut runtime_encrypt_assignments = Vec::new();
1008 let mut runtime_decrypt_assignments = Vec::new();
1009 let mut field_tag_structs = Vec::new();
1010 let mut secure_field_meta_entries = Vec::new();
1011
1012 for field in named_fields.iter() {
1013 let ident = field.ident.clone().expect("named field");
1014 let field_vis = field.vis.clone();
1015 let secure = has_secure_attr(&field.attrs);
1016 let field_crypto_config = field_crypto_config(&field.attrs)?;
1017
1018 if !secure && field_crypto_config.is_present() {
1019 return Err(Error::new_spanned(
1020 ident,
1021 "#[crypto(...)] on a field requires #[secure] on the same field",
1022 ));
1023 }
1024
1025 if secure {
1026 secure_field_count += 1;
1027 let secure_kind = secure_kind(field)?;
1028 let encrypted_ty = secure_kind.encrypted_type();
1029 let field_tag_ident = format_ident!(
1030 "AppdbSensitiveFieldTag{}{}",
1031 struct_ident,
1032 to_pascal_case(&ident.to_string())
1033 );
1034 let field_tag_literal = ident.to_string();
1035 let effective_account = field_crypto_config
1036 .field_account
1037 .clone()
1038 .or_else(|| type_crypto_config.account.clone());
1039 let service_override = type_crypto_config.service.clone();
1040 let account_literal = effective_account
1041 .as_ref()
1042 .map(|value| quote! { ::std::option::Option::Some(#value) })
1043 .unwrap_or_else(|| quote! { ::std::option::Option::None });
1044 let service_literal = service_override
1045 .as_ref()
1046 .map(|value| quote! { ::std::option::Option::Some(#value) })
1047 .unwrap_or_else(|| quote! { ::std::option::Option::None });
1048 let encrypt_expr = secure_kind.encrypt_with_context_expr(&ident);
1049 let decrypt_expr = secure_kind.decrypt_with_context_expr(&ident);
1050 let runtime_encrypt_expr =
1051 secure_kind.encrypt_with_runtime_expr(&ident, &field_tag_ident);
1052 let runtime_decrypt_expr =
1053 secure_kind.decrypt_with_runtime_expr(&ident, &field_tag_ident);
1054 encrypted_fields.push(quote! { #field_vis #ident: #encrypted_ty });
1055 encrypt_assignments.push(quote! { #ident: #encrypt_expr });
1056 decrypt_assignments.push(quote! { #ident: #decrypt_expr });
1057 runtime_encrypt_assignments.push(quote! { #ident: #runtime_encrypt_expr });
1058 runtime_decrypt_assignments.push(quote! { #ident: #runtime_decrypt_expr });
1059 secure_field_meta_entries.push(quote! {
1060 ::appdb::crypto::SensitiveFieldMetadata {
1061 model_tag: ::std::concat!(::std::module_path!(), "::", ::std::stringify!(#struct_ident)),
1062 field_tag: #field_tag_literal,
1063 service: #service_literal,
1064 account: #account_literal,
1065 secure_fields: &[],
1066 }
1067 });
1068 field_tag_structs.push(quote! {
1069 #[doc(hidden)]
1070 #vis struct #field_tag_ident;
1071
1072 impl ::appdb::crypto::SensitiveFieldTag for #field_tag_ident {
1073 fn model_tag() -> &'static str {
1074 <#struct_ident as ::appdb::crypto::SensitiveModelTag>::model_tag()
1075 }
1076
1077 fn field_tag() -> &'static str {
1078 #field_tag_literal
1079 }
1080
1081 fn crypto_metadata() -> &'static ::appdb::crypto::SensitiveFieldMetadata {
1082 static FIELD_META: ::std::sync::OnceLock<::appdb::crypto::SensitiveFieldMetadata> = ::std::sync::OnceLock::new();
1083 FIELD_META.get_or_init(|| ::appdb::crypto::SensitiveFieldMetadata {
1084 model_tag: <#struct_ident as ::appdb::crypto::SensitiveModelTag>::model_tag(),
1085 field_tag: #field_tag_literal,
1086 service: #service_literal,
1087 account: #account_literal,
1088 secure_fields: &#struct_ident::SECURE_FIELDS,
1089 })
1090 }
1091 }
1092 });
1093 } else {
1094 let ty = field.ty.clone();
1095 encrypted_fields.push(quote! { #field_vis #ident: #ty });
1096 encrypt_assignments.push(quote! { #ident: self.#ident.clone() });
1097 decrypt_assignments.push(quote! { #ident: encrypted.#ident.clone() });
1098 runtime_encrypt_assignments.push(quote! { #ident: self.#ident.clone() });
1099 runtime_decrypt_assignments.push(quote! { #ident: encrypted.#ident.clone() });
1100 }
1101 }
1102
1103 if secure_field_count == 0 {
1104 return Err(Error::new_spanned(
1105 struct_ident,
1106 "Sensitive requires at least one #[secure] field",
1107 ));
1108 }
1109
1110 Ok(quote! {
1111 #[derive(
1112 Debug,
1113 Clone,
1114 ::serde::Serialize,
1115 ::serde::Deserialize,
1116 ::surrealdb::types::SurrealValue,
1117 )]
1118 #vis struct #encrypted_ident {
1119 #( #encrypted_fields, )*
1120 }
1121
1122 impl ::appdb::crypto::SensitiveModelTag for #struct_ident {
1123 fn model_tag() -> &'static str {
1124 ::std::concat!(::std::module_path!(), "::", ::std::stringify!(#struct_ident))
1125 }
1126 }
1127
1128 #( #field_tag_structs )*
1129
1130 impl ::appdb::Sensitive for #struct_ident {
1131 type Encrypted = #encrypted_ident;
1132
1133 fn encrypt(
1134 &self,
1135 context: &::appdb::crypto::CryptoContext,
1136 ) -> ::std::result::Result<Self::Encrypted, ::appdb::crypto::CryptoError> {
1137 ::std::result::Result::Ok(#encrypted_ident {
1138 #( #encrypt_assignments, )*
1139 })
1140 }
1141
1142 fn decrypt(
1143 encrypted: &Self::Encrypted,
1144 context: &::appdb::crypto::CryptoContext,
1145 ) -> ::std::result::Result<Self, ::appdb::crypto::CryptoError> {
1146 ::std::result::Result::Ok(Self {
1147 #( #decrypt_assignments, )*
1148 })
1149 }
1150
1151 fn encrypt_with_runtime_resolver(
1152 &self,
1153 ) -> ::std::result::Result<Self::Encrypted, ::appdb::crypto::CryptoError> {
1154 ::std::result::Result::Ok(#encrypted_ident {
1155 #( #runtime_encrypt_assignments, )*
1156 })
1157 }
1158
1159 fn decrypt_with_runtime_resolver(
1160 encrypted: &Self::Encrypted,
1161 ) -> ::std::result::Result<Self, ::appdb::crypto::CryptoError> {
1162 ::std::result::Result::Ok(Self {
1163 #( #runtime_decrypt_assignments, )*
1164 })
1165 }
1166
1167 fn secure_fields() -> &'static [::appdb::crypto::SensitiveFieldMetadata] {
1168 &Self::SECURE_FIELDS
1169 }
1170 }
1171
1172 impl #struct_ident {
1173 pub const SECURE_FIELDS: [::appdb::crypto::SensitiveFieldMetadata; #secure_field_count] = [
1174 #( #secure_field_meta_entries, )*
1175 ];
1176
1177 pub fn encrypt(
1178 &self,
1179 context: &::appdb::crypto::CryptoContext,
1180 ) -> ::std::result::Result<#encrypted_ident, ::appdb::crypto::CryptoError> {
1181 <Self as ::appdb::Sensitive>::encrypt(self, context)
1182 }
1183 }
1184
1185 impl #encrypted_ident {
1186 pub fn decrypt(
1187 &self,
1188 context: &::appdb::crypto::CryptoContext,
1189 ) -> ::std::result::Result<#struct_ident, ::appdb::crypto::CryptoError> {
1190 <#struct_ident as ::appdb::Sensitive>::decrypt(self, context)
1191 }
1192 }
1193 })
1194}
1195
1196fn has_secure_attr(attrs: &[Attribute]) -> bool {
1197 attrs.iter().any(|attr| attr.path().is_ident("secure"))
1198}
1199
1200fn has_unique_attr(attrs: &[Attribute]) -> bool {
1201 attrs.iter().any(|attr| attr.path().is_ident("unique"))
1202}
1203
1204#[derive(Default, Clone)]
1205struct TypeCryptoConfig {
1206 service: Option<String>,
1207 account: Option<String>,
1208}
1209
1210#[derive(Default, Clone)]
1211struct FieldCryptoConfig {
1212 field_account: Option<String>,
1213}
1214
1215impl FieldCryptoConfig {
1216 fn is_present(&self) -> bool {
1217 self.field_account.is_some()
1218 }
1219}
1220
1221fn type_crypto_config(attrs: &[Attribute]) -> syn::Result<TypeCryptoConfig> {
1222 let mut config = TypeCryptoConfig::default();
1223 let mut seen = HashSet::new();
1224
1225 for attr in attrs {
1226 if !attr.path().is_ident("crypto") {
1227 continue;
1228 }
1229
1230 attr.parse_nested_meta(|meta| {
1231 let key = meta
1232 .path
1233 .get_ident()
1234 .cloned()
1235 .ok_or_else(|| meta.error("unsupported crypto attribute"))?;
1236
1237 if !seen.insert(key.to_string()) {
1238 return Err(meta.error("duplicate crypto attribute key"));
1239 }
1240
1241 let value = meta.value()?;
1242 let literal: syn::LitStr = value.parse()?;
1243 match key.to_string().as_str() {
1244 "service" => config.service = Some(literal.value()),
1245 "account" => config.account = Some(literal.value()),
1246 _ => {
1247 return Err(
1248 meta.error("unsupported crypto attribute; expected `service` or `account`")
1249 );
1250 }
1251 }
1252 Ok(())
1253 })?;
1254 }
1255
1256 Ok(config)
1257}
1258
1259fn field_crypto_config(attrs: &[Attribute]) -> syn::Result<FieldCryptoConfig> {
1260 let mut config = FieldCryptoConfig::default();
1261 let mut seen = HashSet::new();
1262
1263 for attr in attrs {
1264 if attr.path().is_ident("crypto") {
1265 attr.parse_nested_meta(|meta| {
1266 let key = meta
1267 .path
1268 .get_ident()
1269 .cloned()
1270 .ok_or_else(|| meta.error("unsupported crypto attribute"))?;
1271
1272 if !seen.insert(key.to_string()) {
1273 return Err(meta.error("duplicate crypto attribute key"));
1274 }
1275
1276 let value = meta.value()?;
1277 let literal: syn::LitStr = value.parse()?;
1278 match key.to_string().as_str() {
1279 "field_account" => config.field_account = Some(literal.value()),
1280 _ => {
1281 return Err(meta.error(
1282 "unsupported field crypto attribute; expected `field_account`",
1283 ));
1284 }
1285 }
1286 Ok(())
1287 })?;
1288 } else if attr.path().is_ident("secure") && matches!(attr.meta, Meta::List(_)) {
1289 return Err(Error::new_spanned(
1290 attr,
1291 "#[secure] does not accept arguments; use #[crypto(field_account = \"...\")] on the field",
1292 ));
1293 }
1294 }
1295
1296 Ok(config)
1297}
1298
1299fn table_alias_target(attrs: &[Attribute]) -> syn::Result<Option<Type>> {
1300 let mut target = None;
1301
1302 for attr in attrs {
1303 if !attr.path().is_ident("table_as") {
1304 continue;
1305 }
1306
1307 if target.is_some() {
1308 return Err(Error::new_spanned(
1309 attr,
1310 "duplicate #[table_as(...)] attribute is not supported",
1311 ));
1312 }
1313
1314 let parsed: Type = attr.parse_args().map_err(|_| {
1315 Error::new_spanned(attr, "#[table_as(...)] requires exactly one target type")
1316 })?;
1317
1318 match parsed {
1319 Type::Path(TypePath { ref path, .. }) if !path.segments.is_empty() => {
1320 target = Some(parsed);
1321 }
1322 _ => {
1323 return Err(Error::new_spanned(
1324 parsed,
1325 "#[table_as(...)] target must be a type path",
1326 ));
1327 }
1328 }
1329 }
1330
1331 Ok(target)
1332}
1333
1334fn resolved_schema_table_name(struct_ident: &syn::Ident, table_alias: Option<&Type>) -> String {
1335 match table_alias {
1336 Some(Type::Path(type_path)) => type_path
1337 .path
1338 .segments
1339 .last()
1340 .map(|segment| to_snake_case(&segment.ident.to_string()))
1341 .unwrap_or_else(|| to_snake_case(&struct_ident.to_string())),
1342 Some(_) => to_snake_case(&struct_ident.to_string()),
1343 None => to_snake_case(&struct_ident.to_string()),
1344 }
1345}
1346
1347fn field_foreign_attr(field: &Field) -> syn::Result<Option<&Attribute>> {
1348 let mut foreign_attr = None;
1349
1350 for attr in &field.attrs {
1351 if !attr.path().is_ident("foreign") {
1352 continue;
1353 }
1354
1355 if foreign_attr.is_some() {
1356 return Err(Error::new_spanned(
1357 attr,
1358 "duplicate nested-ref attribute is not supported",
1359 ));
1360 }
1361
1362 foreign_attr = Some(attr);
1363 }
1364
1365 Ok(foreign_attr)
1366}
1367
1368fn field_relation_attr(field: &Field) -> syn::Result<Option<&Attribute>> {
1369 let mut relate_attr = None;
1370
1371 for attr in &field.attrs {
1372 if !attr.path().is_ident("relate") && !attr.path().is_ident("back_relate") {
1373 continue;
1374 }
1375
1376 if let Some(previous) = relate_attr {
1377 return Err(Error::new_spanned(
1378 attr,
1379 relation_attr_conflict_message(previous, attr),
1380 ));
1381 }
1382
1383 relate_attr = Some(attr);
1384 }
1385
1386 Ok(relate_attr)
1387}
1388
1389fn validate_foreign_field(field: &Field, attr: &Attribute) -> syn::Result<Type> {
1390 if attr.path().is_ident("foreign") {
1391 return foreign_leaf_type(&field.ty)
1392 .ok_or_else(|| Error::new_spanned(&field.ty, BINDREF_ACCEPTED_SHAPES));
1393 }
1394
1395 Err(Error::new_spanned(attr, "unsupported foreign attribute"))
1396}
1397
1398const BINDREF_ACCEPTED_SHAPES: &str = "#[foreign] supports recursive Option<_> / Vec<_> shapes whose leaf type implements appdb::Bridge";
1399
1400const BINDREF_BRIDGE_STORE_ONLY: &str =
1401 "#[foreign] leaf types must derive Store or #[derive(Bridge)] dispatcher enums";
1402
1403const RELATE_ACCEPTED_SHAPES: &str = "relation-backed fields support Child / Option<Child> / Vec<Child> / Option<Vec<Child>> shapes whose leaf type implements appdb::Bridge";
1404
1405#[derive(Clone, Copy)]
1406enum RelationFieldDirection {
1407 Outgoing,
1408 Incoming,
1409}
1410
1411impl RelationFieldDirection {
1412 fn attr_label(self) -> &'static str {
1413 match self {
1414 Self::Outgoing => "#[relate(...)]",
1415 Self::Incoming => "#[back_relate(...)]",
1416 }
1417 }
1418
1419 fn write_direction_tokens(self) -> proc_macro2::TokenStream {
1420 match self {
1421 Self::Outgoing => quote!(::appdb::RelationWriteDirection::Outgoing),
1422 Self::Incoming => quote!(::appdb::RelationWriteDirection::Incoming),
1423 }
1424 }
1425
1426 fn write_edges_tokens(self) -> proc_macro2::TokenStream {
1427 match self {
1428 Self::Outgoing => quote! {
1429 ids
1430 .into_iter()
1431 .enumerate()
1432 .map(|(position, out)| ::appdb::graph::OrderedRelationEdge {
1433 _in: ::std::option::Option::Some(record.clone()),
1434 out,
1435 position: position as i64,
1436 })
1437 .collect()
1438 },
1439 Self::Incoming => quote! {
1440 ids
1441 .into_iter()
1442 .enumerate()
1443 .map(|(position, source)| ::appdb::graph::OrderedRelationEdge {
1444 _in: ::std::option::Option::Some(source),
1445 out: record.clone(),
1446 position: position as i64,
1447 })
1448 .collect()
1449 },
1450 }
1451 }
1452
1453 fn load_edges_tokens(self, relation_name: &str) -> proc_macro2::TokenStream {
1454 match self {
1455 Self::Outgoing => quote! {
1456 ::appdb::graph::GraphRepo::out_edges(record.clone(), #relation_name)
1457 .await?
1458 .into_iter()
1459 .map(|edge| edge.out)
1460 .collect()
1461 },
1462 Self::Incoming => quote! {
1463 {
1464 let mut ids = ::std::vec::Vec::new();
1465 for edge in ::appdb::graph::GraphRepo::in_edges(record.clone(), #relation_name).await? {
1466 let incoming = edge._in.ok_or_else(|| {
1467 ::anyhow::anyhow!("back relate field received relation edge without `in` record id")
1468 })?;
1469 ids.push(incoming);
1470 }
1471 ids
1472 }
1473 },
1474 }
1475 }
1476}
1477
1478#[derive(Clone)]
1479struct ForeignField {
1480 ident: syn::Ident,
1481 kind: ForeignFieldKind,
1482}
1483
1484#[derive(Clone)]
1485struct ForeignFieldKind {
1486 original_ty: Type,
1487 stored_ty: Type,
1488}
1489
1490#[derive(Clone)]
1491struct RelateField {
1492 ident: syn::Ident,
1493 relation_name: String,
1494 field_ty: Type,
1495 direction: RelationFieldDirection,
1496}
1497
1498fn parse_foreign_field(field: &Field, attr: &Attribute) -> syn::Result<ForeignField> {
1499 validate_foreign_field(field, attr)?;
1500 let ident = field.ident.clone().expect("named field");
1501
1502 let kind = ForeignFieldKind {
1503 original_ty: field.ty.clone(),
1504 stored_ty: foreign_stored_type(&field.ty)
1505 .ok_or_else(|| Error::new_spanned(&field.ty, BINDREF_ACCEPTED_SHAPES))?,
1506 };
1507
1508 Ok(ForeignField { ident, kind })
1509}
1510
1511fn parse_relate_field(field: &Field, attr: &Attribute) -> syn::Result<RelateField> {
1512 let direction = parse_relation_direction(attr)?;
1513 let relation_name = attr
1514 .parse_args::<syn::LitStr>()
1515 .map_err(|_| {
1516 Error::new_spanned(
1517 attr,
1518 format!(
1519 "{} requires exactly one string literal",
1520 direction.attr_label()
1521 ),
1522 )
1523 })?
1524 .value();
1525 if relation_name.is_empty() {
1526 return Err(Error::new_spanned(
1527 attr,
1528 format!("{} relation name must not be empty", direction.attr_label()),
1529 ));
1530 }
1531
1532 validate_relate_field(field, attr)?;
1533
1534 Ok(RelateField {
1535 ident: field.ident.clone().expect("named field"),
1536 relation_name,
1537 field_ty: field.ty.clone(),
1538 direction,
1539 })
1540}
1541
1542fn validate_relate_field(field: &Field, attr: &Attribute) -> syn::Result<Type> {
1543 if !attr.path().is_ident("relate") && !attr.path().is_ident("back_relate") {
1544 return Err(Error::new_spanned(attr, "unsupported relate attribute"));
1545 }
1546
1547 let accepted = relate_leaf_type(&field.ty).cloned().map(Type::Path);
1548
1549 accepted.ok_or_else(|| Error::new_spanned(&field.ty, RELATE_ACCEPTED_SHAPES))
1550}
1551
1552fn relate_leaf_type(ty: &Type) -> Option<&TypePath> {
1553 if let Some(leaf) = direct_store_child_type(ty) {
1554 return Some(leaf);
1555 }
1556
1557 if let Some(inner) = option_inner_type(ty) {
1558 if let Some(leaf) = direct_store_child_type(inner) {
1559 return Some(leaf);
1560 }
1561
1562 if let Some(vec_inner) = vec_inner_type(inner) {
1563 return direct_store_child_type(vec_inner);
1564 }
1565
1566 return None;
1567 }
1568
1569 if let Some(inner) = vec_inner_type(ty) {
1570 return direct_store_child_type(inner);
1571 }
1572
1573 None
1574}
1575
1576fn parse_relation_direction(attr: &Attribute) -> syn::Result<RelationFieldDirection> {
1577 if attr.path().is_ident("relate") {
1578 return Ok(RelationFieldDirection::Outgoing);
1579 }
1580 if attr.path().is_ident("back_relate") {
1581 return Ok(RelationFieldDirection::Incoming);
1582 }
1583
1584 Err(Error::new_spanned(attr, "unsupported relate attribute"))
1585}
1586
1587fn relation_attr_label(attr: &Attribute) -> &'static str {
1588 if attr.path().is_ident("back_relate") {
1589 "#[back_relate(...)]"
1590 } else {
1591 "#[relate(...)]"
1592 }
1593}
1594
1595fn relation_attr_conflict_message(previous: &Attribute, current: &Attribute) -> String {
1596 match (
1597 previous.path().is_ident("relate"),
1598 previous.path().is_ident("back_relate"),
1599 current.path().is_ident("relate"),
1600 current.path().is_ident("back_relate"),
1601 ) {
1602 (true, false, true, false) => {
1603 "duplicate #[relate(...)] attribute is not supported".to_owned()
1604 }
1605 (false, true, false, true) => {
1606 "duplicate #[back_relate(...)] attribute is not supported".to_owned()
1607 }
1608 _ => "#[relate(...)] cannot be combined with #[back_relate(...)]".to_owned(),
1609 }
1610}
1611
1612fn foreign_field_kind<'a>(
1613 ident: &syn::Ident,
1614 fields: &'a [ForeignField],
1615) -> Option<&'a ForeignFieldKind> {
1616 fields
1617 .iter()
1618 .find(|field| field.ident == *ident)
1619 .map(|field| &field.kind)
1620}
1621
1622fn stored_field_type(field: &Field, foreign_fields: &[ForeignField]) -> Type {
1623 let ident = field.ident.as_ref().expect("named field");
1624 match foreign_field_kind(ident, foreign_fields) {
1625 Some(ForeignFieldKind { stored_ty, .. }) => stored_ty.clone(),
1626 None => field.ty.clone(),
1627 }
1628}
1629
1630fn foreign_stored_type(ty: &Type) -> Option<Type> {
1631 if let Some(inner) = option_inner_type(ty) {
1632 let inner = foreign_stored_type(inner)?;
1633 return Some(syn::parse_quote!(::std::option::Option<#inner>));
1634 }
1635
1636 if let Some(inner) = vec_inner_type(ty) {
1637 let inner = foreign_stored_type(inner)?;
1638 return Some(syn::parse_quote!(::std::vec::Vec<#inner>));
1639 }
1640
1641 direct_store_child_type(ty)
1642 .cloned()
1643 .map(|_| syn::parse_quote!(::surrealdb::types::RecordId))
1644}
1645
1646fn foreign_leaf_type(ty: &Type) -> Option<Type> {
1647 if let Some(inner) = option_inner_type(ty) {
1648 return foreign_leaf_type(inner);
1649 }
1650
1651 if let Some(inner) = vec_inner_type(ty) {
1652 return foreign_leaf_type(inner);
1653 }
1654
1655 direct_store_child_type(ty).cloned().map(Type::Path)
1656}
1657
1658fn invalid_foreign_leaf_type(ty: &Type) -> Option<Type> {
1659 let leaf = foreign_leaf_type(ty)?;
1660 match &leaf {
1661 Type::Path(type_path) => {
1662 let segment = type_path.path.segments.last()?;
1663 if matches!(segment.arguments, PathArguments::None) {
1664 None
1665 } else {
1666 Some(leaf)
1667 }
1668 }
1669 _ => Some(leaf),
1670 }
1671}
1672
1673fn direct_store_child_type(ty: &Type) -> Option<&TypePath> {
1674 let Type::Path(type_path) = ty else {
1675 return None;
1676 };
1677
1678 let segment = type_path.path.segments.last()?;
1679 if !matches!(segment.arguments, PathArguments::None) {
1680 return None;
1681 }
1682
1683 if is_id_type(ty) || is_string_type(ty) || is_common_non_store_leaf_type(ty) {
1684 return None;
1685 }
1686
1687 Some(type_path)
1688}
1689
1690fn is_common_non_store_leaf_type(ty: &Type) -> bool {
1691 matches!(
1692 ty,
1693 Type::Path(TypePath { path, .. })
1694 if path.is_ident("bool")
1695 || path.is_ident("u8")
1696 || path.is_ident("u16")
1697 || path.is_ident("u32")
1698 || path.is_ident("u64")
1699 || path.is_ident("u128")
1700 || path.is_ident("usize")
1701 || path.is_ident("i8")
1702 || path.is_ident("i16")
1703 || path.is_ident("i32")
1704 || path.is_ident("i64")
1705 || path.is_ident("i128")
1706 || path.is_ident("isize")
1707 || path.is_ident("f32")
1708 || path.is_ident("f64")
1709 || path.is_ident("char")
1710 )
1711}
1712
1713fn secure_field_count(fields: &syn::punctuated::Punctuated<Field, syn::token::Comma>) -> usize {
1714 fields
1715 .iter()
1716 .filter(|field| has_secure_attr(&field.attrs))
1717 .count()
1718}
1719
1720fn relation_name_override(attrs: &[Attribute]) -> syn::Result<Option<String>> {
1721 for attr in attrs {
1722 if !attr.path().is_ident("relation") {
1723 continue;
1724 }
1725
1726 let mut name = None;
1727 attr.parse_nested_meta(|meta| {
1728 if meta.path.is_ident("name") {
1729 let value = meta.value()?;
1730 let literal: syn::LitStr = value.parse()?;
1731 name = Some(literal.value());
1732 Ok(())
1733 } else {
1734 Err(meta.error("unsupported relation attribute"))
1735 }
1736 })?;
1737 return Ok(name);
1738 }
1739
1740 Ok(None)
1741}
1742
1743enum SecureKind {
1744 Shape(Type),
1745}
1746
1747impl SecureKind {
1748 fn encrypted_type(&self) -> proc_macro2::TokenStream {
1749 match self {
1750 SecureKind::Shape(ty) => quote! { <#ty as ::appdb::SensitiveShape>::Encrypted },
1751 }
1752 }
1753
1754 fn encrypt_with_context_expr(&self, ident: &syn::Ident) -> proc_macro2::TokenStream {
1755 match self {
1756 SecureKind::Shape(ty) => {
1757 quote! { <#ty as ::appdb::SensitiveShape>::encrypt_with_context(&self.#ident, context)? }
1758 }
1759 }
1760 }
1761
1762 fn decrypt_with_context_expr(&self, ident: &syn::Ident) -> proc_macro2::TokenStream {
1763 match self {
1764 SecureKind::Shape(ty) => {
1765 quote! { <#ty as ::appdb::SensitiveShape>::decrypt_with_context(&encrypted.#ident, context)? }
1766 }
1767 }
1768 }
1769
1770 fn encrypt_with_runtime_expr(
1771 &self,
1772 ident: &syn::Ident,
1773 field_tag_ident: &syn::Ident,
1774 ) -> proc_macro2::TokenStream {
1775 match self {
1776 SecureKind::Shape(ty) => {
1777 quote! {{
1778 let context = ::appdb::crypto::resolve_crypto_context_for::<#field_tag_ident>()?;
1779 <#ty as ::appdb::SensitiveShape>::encrypt_with_context(&self.#ident, context.as_ref())?
1780 }}
1781 }
1782 }
1783 }
1784
1785 fn decrypt_with_runtime_expr(
1786 &self,
1787 ident: &syn::Ident,
1788 field_tag_ident: &syn::Ident,
1789 ) -> proc_macro2::TokenStream {
1790 match self {
1791 SecureKind::Shape(ty) => {
1792 quote! {{
1793 let context = ::appdb::crypto::resolve_crypto_context_for::<#field_tag_ident>()?;
1794 <#ty as ::appdb::SensitiveShape>::decrypt_with_context(&encrypted.#ident, context.as_ref())?
1795 }}
1796 }
1797 }
1798 }
1799}
1800
1801fn secure_kind(field: &Field) -> syn::Result<SecureKind> {
1802 if secure_shape_supported(&field.ty) {
1803 return Ok(SecureKind::Shape(field.ty.clone()));
1804 }
1805
1806 Err(Error::new_spanned(
1807 &field.ty,
1808 secure_shape_error_message(&field.ty),
1809 ))
1810}
1811
1812fn secure_shape_supported(ty: &Type) -> bool {
1813 if is_string_type(ty) {
1814 return true;
1815 }
1816
1817 if sensitive_value_wrapper_inner_type(ty).is_some() {
1818 return true;
1819 }
1820
1821 if let Some(inner) = option_inner_type(ty) {
1822 return secure_shape_supported(inner);
1823 }
1824
1825 if let Some(inner) = vec_inner_type(ty) {
1826 return secure_shape_supported(inner);
1827 }
1828
1829 direct_sensitive_child_type(ty).is_some()
1830}
1831
1832fn secure_shape_error_message(ty: &Type) -> &'static str {
1833 if invalid_secure_leaf_type(ty).is_some() {
1834 "#[secure] child shapes require a direct named Sensitive type leaf with only Option<_> and Vec<_> wrappers"
1835 } else {
1836 "#[secure] supports String, appdb::SensitiveValueOf<T>, and recursive Child / Option<Child> / Vec<Child> shapes where Child implements appdb::Sensitive"
1837 }
1838}
1839
1840fn direct_sensitive_child_type(ty: &Type) -> Option<&TypePath> {
1841 let Type::Path(type_path) = ty else {
1842 return None;
1843 };
1844
1845 let segment = type_path.path.segments.last()?;
1846 if !matches!(segment.arguments, PathArguments::None) {
1847 return None;
1848 }
1849
1850 if is_id_type(ty) || is_string_type(ty) || is_common_non_store_leaf_type(ty) {
1851 return None;
1852 }
1853
1854 Some(type_path)
1855}
1856
1857fn invalid_secure_leaf_type(ty: &Type) -> Option<Type> {
1858 if let Some(inner) = option_inner_type(ty) {
1859 return invalid_secure_leaf_type(inner);
1860 }
1861
1862 if let Some(inner) = vec_inner_type(ty) {
1863 return invalid_secure_leaf_type(inner);
1864 }
1865
1866 let leaf = direct_sensitive_child_type(ty)?.clone();
1867 let segment = leaf.path.segments.last()?;
1868 if matches!(segment.arguments, PathArguments::None) {
1869 None
1870 } else {
1871 Some(Type::Path(leaf))
1872 }
1873}
1874
1875fn is_string_type(ty: &Type) -> bool {
1876 match ty {
1877 Type::Path(TypePath { path, .. }) => path.is_ident("String"),
1878 _ => false,
1879 }
1880}
1881
1882fn is_id_type(ty: &Type) -> bool {
1883 match ty {
1884 Type::Path(TypePath { path, .. }) => path.segments.last().is_some_and(|segment| {
1885 let ident = segment.ident.to_string();
1886 ident == "Id"
1887 }),
1888 _ => false,
1889 }
1890}
1891
1892fn is_record_id_type(ty: &Type) -> bool {
1893 match ty {
1894 Type::Path(TypePath { path, .. }) => path.segments.last().is_some_and(|segment| {
1895 let ident = segment.ident.to_string();
1896 ident == "RecordId"
1897 }),
1898 _ => false,
1899 }
1900}
1901
1902fn option_inner_type(ty: &Type) -> Option<&Type> {
1903 let Type::Path(TypePath { path, .. }) = ty else {
1904 return None;
1905 };
1906 let segment = path.segments.last()?;
1907 if segment.ident != "Option" {
1908 return None;
1909 }
1910 let PathArguments::AngleBracketed(args) = &segment.arguments else {
1911 return None;
1912 };
1913 let GenericArgument::Type(inner) = args.args.first()? else {
1914 return None;
1915 };
1916 Some(inner)
1917}
1918
1919fn vec_inner_type(ty: &Type) -> Option<&Type> {
1920 let Type::Path(TypePath { path, .. }) = ty else {
1921 return None;
1922 };
1923 let segment = path.segments.last()?;
1924 if segment.ident != "Vec" {
1925 return None;
1926 }
1927 let PathArguments::AngleBracketed(args) = &segment.arguments else {
1928 return None;
1929 };
1930 let GenericArgument::Type(inner) = args.args.first()? else {
1931 return None;
1932 };
1933 Some(inner)
1934}
1935
1936fn sensitive_value_wrapper_inner_type(ty: &Type) -> Option<&Type> {
1937 let Type::Path(TypePath { path, .. }) = ty else {
1938 return None;
1939 };
1940 let segment = path.segments.last()?;
1941 if segment.ident != "SensitiveValueOf" {
1942 return None;
1943 }
1944 let PathArguments::AngleBracketed(args) = &segment.arguments else {
1945 return None;
1946 };
1947 let GenericArgument::Type(inner) = args.args.first()? else {
1948 return None;
1949 };
1950 Some(inner)
1951}
1952
1953fn to_snake_case(input: &str) -> String {
1954 let mut out = String::with_capacity(input.len() + 4);
1955 let mut prev_is_lower_or_digit = false;
1956
1957 for ch in input.chars() {
1958 if ch.is_ascii_uppercase() {
1959 if prev_is_lower_or_digit {
1960 out.push('_');
1961 }
1962 out.push(ch.to_ascii_lowercase());
1963 prev_is_lower_or_digit = false;
1964 } else {
1965 out.push(ch);
1966 prev_is_lower_or_digit = ch.is_ascii_lowercase() || ch.is_ascii_digit();
1967 }
1968 }
1969
1970 out
1971}
1972
1973fn to_pascal_case(input: &str) -> String {
1974 let mut out = String::with_capacity(input.len());
1975 let mut uppercase_next = true;
1976
1977 for ch in input.chars() {
1978 if ch == '_' || ch == '-' {
1979 uppercase_next = true;
1980 continue;
1981 }
1982
1983 if uppercase_next {
1984 out.push(ch.to_ascii_uppercase());
1985 uppercase_next = false;
1986 } else {
1987 out.push(ch);
1988 }
1989 }
1990
1991 out
1992}