Skip to main content

better_auth_core/user_fields/
record.rs

1use super::{
2    FieldOutputCapabilities, UserConfig, UserFieldConfig, UserFieldFactory, UserFieldReference,
3    UserFieldType,
4};
5use crate::AuthResult;
6use crate::store::schema::resolve_field_name;
7use crate::{FieldMap, FieldValue as Value};
8use std::sync::Arc;
9
10/// Raw logical fields and mapped storage fields, before adapter output policies run.
11pub struct AdapterRecord {
12    output: FieldMap,
13    storage: FieldMap,
14    raw_storage: Option<FieldOutputCapabilities>,
15}
16
17impl AdapterRecord {
18    /// Preserve unmapped core fields and defer configured policies to the batch boundary.
19    pub fn new(core: FieldMap, storage: FieldMap) -> Self {
20        Self {
21            output: core,
22            storage,
23            raw_storage: None,
24        }
25    }
26
27    /// Restore raw adapter values before output policies. `None` retains the extracted value.
28    /// The restored values bypass the model's JSON conversion before callbacks.
29    pub fn map_storage_fields(
30        &mut self,
31        fields: &UserConfig,
32        capabilities: FieldOutputCapabilities,
33        map: impl Fn(&str, &UserFieldConfig) -> AuthResult<Option<Value>>,
34    ) -> AuthResult<()> {
35        for (name, field) in fields.fields() {
36            if name == "id" {
37                continue;
38            }
39            let storage = resolve_field_name(field.field_name.as_deref(), name);
40            if let Some(value) = map(storage, field)? {
41                let _ = self.storage.insert(storage.to_owned(), value);
42            }
43        }
44        self.raw_storage = Some(capabilities);
45        Ok(())
46    }
47
48    /// Restore unconfigured core values without applying application field policies.
49    pub fn map_native_fields(
50        &mut self,
51        fields: &UserConfig,
52        map: impl Fn(&str) -> AuthResult<Option<Value>>,
53    ) -> AuthResult<()> {
54        for (name, value) in &mut self.output {
55            if !fields.fields().contains_key(name)
56                && let Some(raw) = map(name)?
57            {
58                *value = raw;
59            }
60        }
61        Ok(())
62    }
63}
64
65fn field(field_type: UserFieldType, required: bool) -> UserFieldConfig {
66    UserFieldConfig {
67        field_type,
68        required: Some(required),
69        ..Default::default()
70    }
71}
72
73fn timestamp(default: bool, update: bool) -> UserFieldConfig {
74    let now: UserFieldFactory = Arc::new(|| Ok(chrono::Utc::now().into()));
75    UserFieldConfig {
76        default_value_fn: default.then(|| now.clone()),
77        on_update: update.then_some(now),
78        ..field(UserFieldType::Date, true)
79    }
80}
81
82impl crate::config::AccountConfig {
83    /// Compose the adapter schema. A replacement field replaces all built-in attributes.
84    pub fn field_schema(&self) -> UserConfig {
85        let mut fields = [
86            ("accountId".into(), field(UserFieldType::String, true)),
87            ("providerId".into(), field(UserFieldType::String, true)),
88            (
89                "userId".into(),
90                UserFieldConfig {
91                    references: Some(UserFieldReference {
92                        model: "user".into(),
93                        field: "id".into(),
94                    }),
95                    ..field(UserFieldType::String, true)
96                },
97            ),
98            (
99                "accessToken".into(),
100                UserFieldConfig {
101                    returned: Some(false),
102                    ..field(UserFieldType::String, false)
103                },
104            ),
105            (
106                "refreshToken".into(),
107                UserFieldConfig {
108                    returned: Some(false),
109                    ..field(UserFieldType::String, false)
110                },
111            ),
112            (
113                "idToken".into(),
114                UserFieldConfig {
115                    returned: Some(false),
116                    ..field(UserFieldType::String, false)
117                },
118            ),
119            (
120                "accessTokenExpiresAt".into(),
121                UserFieldConfig {
122                    returned: Some(false),
123                    ..field(UserFieldType::Date, false)
124                },
125            ),
126            (
127                "refreshTokenExpiresAt".into(),
128                UserFieldConfig {
129                    returned: Some(false),
130                    ..field(UserFieldType::Date, false)
131                },
132            ),
133            ("scope".into(), field(UserFieldType::String, false)),
134            (
135                "password".into(),
136                UserFieldConfig {
137                    returned: Some(false),
138                    ..field(UserFieldType::String, false)
139                },
140            ),
141            ("createdAt".into(), timestamp(true, false)),
142            ("updatedAt".into(), timestamp(false, true)),
143        ]
144        .into_iter()
145        .collect::<indexmap::IndexMap<_, _>>();
146        fields.extend(self.additional_fields.clone());
147        UserConfig {
148            additional_fields: Some(fields),
149        }
150    }
151}
152
153impl crate::config::VerificationConfig {
154    /// Compose database field policies without applying them to secondary-only values.
155    pub fn field_schema(&self) -> UserConfig {
156        let mut fields = [
157            ("identifier".into(), field(UserFieldType::String, true)),
158            ("value".into(), field(UserFieldType::String, true)),
159            ("expiresAt".into(), field(UserFieldType::Date, true)),
160            ("createdAt".into(), timestamp(true, false)),
161            ("updatedAt".into(), timestamp(true, true)),
162        ]
163        .into_iter()
164        .collect::<indexmap::IndexMap<_, _>>();
165        fields.extend(self.additional_fields.clone());
166        UserConfig {
167            additional_fields: Some(fields),
168        }
169    }
170}
171
172impl UserConfig {
173    /// Project Organization rows in one batch while retaining adapter-owned IDs.
174    pub async fn organization_output_records(
175        &self,
176        records: Vec<AdapterRecord>,
177        supports_native_json: bool,
178    ) -> AuthResult<Vec<FieldMap>> {
179        self.organization_output_records_then(records, supports_native_json, |_, output| {
180            std::future::ready(Ok(output))
181        })
182        .await
183    }
184
185    /// Continue each Organization row after projection without cancelling started peers.
186    pub async fn organization_output_records_then<R: Send, F>(
187        &self,
188        records: Vec<AdapterRecord>,
189        supports_native_json: bool,
190        complete: impl Fn(usize, FieldMap) -> F + Sync,
191    ) -> AuthResult<Vec<R>>
192    where
193        F: std::future::Future<Output = AuthResult<R>> + Send,
194    {
195        self.organization_output_records_with_json(records, |_| supports_native_json, complete)
196            .await
197    }
198
199    pub(crate) async fn organization_output_memory_records(
200        &self,
201        records: Vec<AdapterRecord>,
202    ) -> AuthResult<Vec<FieldMap>> {
203        self.organization_output_records_with_json(
204            records,
205            UserFieldConfig::references_id,
206            |_, output| std::future::ready(Ok(output)),
207        )
208        .await
209    }
210
211    async fn organization_output_records_with_json<R: Send, F>(
212        &self,
213        records: Vec<AdapterRecord>,
214        supports_native_json: impl Fn(&UserFieldConfig) -> bool + Sync,
215        complete: impl Fn(usize, FieldMap) -> F + Sync,
216    ) -> AuthResult<Vec<R>>
217    where
218        F: std::future::Future<Output = AuthResult<R>> + Send,
219    {
220        let mut rows = self.organization_records(records)?;
221        super::batch::project_fields_then(
222            &mut rows,
223            self.fields(),
224            |row, name, field| {
225                Box::pin(project_organization_field(
226                    row,
227                    name,
228                    field,
229                    supports_native_json(field),
230                ))
231            },
232            |index, record| complete(index, std::mem::take(&mut record.output)),
233        )
234        .await
235    }
236
237    /// Decode ready Organization rows before continuing their synchronous adapter reads together.
238    pub async fn organization_output_records_batches_then<V: Send, R: Send, F>(
239        &self,
240        records: Vec<AdapterRecord>,
241        supports_native_json: bool,
242        decode: impl Fn(usize, FieldMap) -> AuthResult<V> + Sync,
243        complete: impl Fn(Vec<(usize, V)>) -> F + Sync,
244    ) -> AuthResult<Vec<R>>
245    where
246        F: std::future::Future<Output = AuthResult<Vec<(usize, R)>>> + Send,
247    {
248        self.organization_output_records_batches_with_json(
249            records,
250            |_| supports_native_json,
251            decode,
252            complete,
253        )
254        .await
255    }
256
257    pub(crate) async fn organization_output_memory_records_batches_then<V: Send, R: Send, F>(
258        &self,
259        records: Vec<AdapterRecord>,
260        decode: impl Fn(usize, FieldMap) -> AuthResult<V> + Sync,
261        complete: impl Fn(Vec<(usize, V)>) -> F + Sync,
262    ) -> AuthResult<Vec<R>>
263    where
264        F: std::future::Future<Output = AuthResult<Vec<(usize, R)>>> + Send,
265    {
266        self.organization_output_records_batches_with_json(
267            records,
268            UserFieldConfig::references_id,
269            decode,
270            complete,
271        )
272        .await
273    }
274
275    async fn organization_output_records_batches_with_json<V: Send, R: Send, F>(
276        &self,
277        records: Vec<AdapterRecord>,
278        supports_native_json: impl Fn(&UserFieldConfig) -> bool + Sync,
279        decode: impl Fn(usize, FieldMap) -> AuthResult<V> + Sync,
280        complete: impl Fn(Vec<(usize, V)>) -> F + Sync,
281    ) -> AuthResult<Vec<R>>
282    where
283        F: std::future::Future<Output = AuthResult<Vec<(usize, R)>>> + Send,
284    {
285        let mut rows = self.organization_records(records)?;
286        super::batch::project_fields_batches_then(
287            &mut rows,
288            self.fields(),
289            |row, name, field| {
290                Box::pin(project_organization_field(
291                    row,
292                    name,
293                    field,
294                    supports_native_json(field),
295                ))
296            },
297            |index, record| decode(index, std::mem::take(&mut record.output)),
298            complete,
299        )
300        .await
301    }
302
303    fn organization_records(
304        &self,
305        records: Vec<AdapterRecord>,
306    ) -> AuthResult<Vec<OrganizationRecord>> {
307        records
308            .into_iter()
309            .map(|mut record| {
310                record
311                    .output
312                    .retain(|name, _| name == "id" || !self.fields().contains_key(name));
313                Ok(record)
314            })
315            .collect()
316    }
317
318    /// Resolve a logical field in an adapter-owned record.
319    pub fn record_storage_key<'a>(&'a self, name: &'a str) -> &'a str {
320        if name == "id" {
321            return name;
322        }
323        resolve_field_name(
324            self.fields()
325                .get(name)
326                .and_then(|field| field.field_name.as_deref()),
327            name,
328        )
329    }
330
331    /// Project a raw adapter record without retaining unmapped storage field names.
332    pub async fn project_record(
333        &self,
334        storage: &FieldMap,
335        supports_native_json: bool,
336        supports_native_dates: bool,
337    ) -> AuthResult<FieldMap> {
338        // Projection preserves the one input row.
339        Ok(self
340            .project_records(
341                std::slice::from_ref(storage),
342                supports_native_json,
343                supports_native_dates,
344            )
345            .await?
346            .remove(0))
347    }
348
349    /// Project raw adapter records together without retaining unmapped storage field names.
350    pub async fn project_records(
351        &self,
352        storage: &[FieldMap],
353        supports_native_json: bool,
354        supports_native_dates: bool,
355    ) -> AuthResult<Vec<FieldMap>> {
356        self.project_records_then(
357            storage,
358            supports_native_json,
359            supports_native_dates,
360            |_, fields| std::future::ready(Ok(fields)),
361        )
362        .await
363    }
364
365    /// Keep raw Memory reference values and decode ordinary JSON after output callbacks.
366    pub(crate) async fn project_memory_records(
367        &self,
368        storage: &[FieldMap],
369    ) -> AuthResult<Vec<FieldMap>> {
370        self.project_memory_adapter_records(adapter_records(storage)?)
371            .await
372    }
373
374    /// Preserve Memory field conversion while continuing each ready batch with its original indices.
375    pub(crate) async fn project_memory_records_batches_then<R: Send, F>(
376        &self,
377        storage: &[FieldMap],
378        complete: impl Fn(Vec<(usize, FieldMap)>) -> F + Sync,
379    ) -> AuthResult<Vec<R>>
380    where
381        F: std::future::Future<Output = AuthResult<Vec<(usize, R)>>> + Send,
382    {
383        let mut records = adapter_records(storage)?;
384        super::batch::project_fields_batches_then(
385            &mut records,
386            self.fields(),
387            |record, name, field| {
388                Box::pin(project_adapter_field(
389                    record,
390                    name,
391                    field,
392                    field.references_id(),
393                    true,
394                ))
395            },
396            |_, record| Ok(std::mem::take(&mut record.output)),
397            complete,
398        )
399        .await
400    }
401
402    /// Complete each successfully projected row without cancelling other started rows.
403    /// The original row index remains available for adapter-owned association data.
404    pub async fn project_records_then<R: Send, F>(
405        &self,
406        storage: &[FieldMap],
407        supports_native_json: bool,
408        supports_native_dates: bool,
409        complete: impl Fn(usize, FieldMap) -> F + Sync,
410    ) -> AuthResult<Vec<R>>
411    where
412        F: std::future::Future<Output = AuthResult<R>> + Send,
413    {
414        let records = adapter_records(storage)?;
415        self.project_adapter_records_then(
416            records,
417            supports_native_json,
418            supports_native_dates,
419            complete,
420        )
421        .await
422    }
423
424    /// Continue ready raw records as a batch, retaining each original row index.
425    pub async fn project_records_batches_then<R: Send, F>(
426        &self,
427        storage: &[FieldMap],
428        supports_native_json: bool,
429        supports_native_dates: bool,
430        complete: impl Fn(Vec<(usize, FieldMap)>) -> F + Sync,
431    ) -> AuthResult<Vec<R>>
432    where
433        F: std::future::Future<Output = AuthResult<Vec<(usize, R)>>> + Send,
434    {
435        self.project_adapter_records_batches_then(
436            adapter_records(storage)?,
437            supports_native_json,
438            supports_native_dates,
439            complete,
440        )
441        .await
442    }
443
444    /// Apply adapter input policies once to a complete logical record or update patch.
445    /// Native adapter writes do not run HTTP input validation or remove `input: false` fields.
446    pub async fn record_storage_fields_for_adapter(
447        &self,
448        input: FieldMap,
449        create: bool,
450        supports_native_json: bool,
451        native_json_field: impl Fn(&str) -> bool,
452    ) -> AuthResult<FieldMap> {
453        self.record_storage_fields_with_binding(input, create, |name, field, value| {
454            field.adapter_input(value, supports_native_json, native_json_field(name))
455        })
456        .await
457    }
458
459    /// Bind a complete record after its storage policies, while retaining force-allowed IDs.
460    pub async fn record_storage_fields_with_binding(
461        &self,
462        input: FieldMap,
463        create: bool,
464        bind: impl Fn(&str, &super::UserFieldConfig, Value) -> AuthResult<Value>,
465    ) -> AuthResult<FieldMap> {
466        let mut output = FieldMap::new();
467        if let Some(id) = input.get("id") {
468            let _ = output.insert("id".into(), id.clone());
469        }
470        output.extend(self.storage_fields_async(input, create, true, bind).await?);
471        Ok(output)
472    }
473
474    /// Project stored values without applying endpoint visibility or decoding replacement types.
475    /// The caller must invoke this after the write and before cache writes or database after hooks.
476    pub async fn record_output_fields(
477        &self,
478        core: FieldMap,
479        storage: &FieldMap,
480        supports_native_json: bool,
481        supports_native_dates: bool,
482    ) -> AuthResult<FieldMap> {
483        // Projection preserves the one input row.
484        Ok(self
485            .project_adapter_records(
486                vec![AdapterRecord::new(core, storage.clone())],
487                supports_native_json,
488                supports_native_dates,
489            )
490            .await?
491            .remove(0))
492    }
493
494    /// Project extracted records in row order, interleaving synchronous callbacks by field.
495    pub async fn project_adapter_records(
496        &self,
497        records: Vec<AdapterRecord>,
498        supports_native_json: bool,
499        supports_native_dates: bool,
500    ) -> AuthResult<Vec<FieldMap>> {
501        self.project_adapter_records_then(
502            records,
503            supports_native_json,
504            supports_native_dates,
505            |_, fields| std::future::ready(Ok(fields)),
506        )
507        .await
508    }
509
510    /// Project adapter fields with the database's JSON, array, boolean, and date conversions.
511    pub async fn project_adapter_records_with_capabilities(
512        &self,
513        mut records: Vec<AdapterRecord>,
514        capabilities: FieldOutputCapabilities,
515        supports_native_dates: bool,
516    ) -> AuthResult<Vec<FieldMap>> {
517        super::batch::project_fields(&mut records, self.fields(), |record, name, field| {
518            Box::pin(project_adapter_field_with_capabilities(
519                record,
520                name,
521                field,
522                capabilities,
523                supports_native_dates,
524            ))
525        })
526        .await?;
527        Ok(records.into_iter().map(|record| record.output).collect())
528    }
529
530    /// Preserve Memory reference values until callbacks run, then decode ordinary JSON text.
531    pub(crate) async fn project_memory_adapter_records(
532        &self,
533        mut records: Vec<AdapterRecord>,
534    ) -> AuthResult<Vec<FieldMap>> {
535        super::batch::project_fields(&mut records, self.fields(), |record, name, field| {
536            Box::pin(project_adapter_field(
537                record,
538                name,
539                field,
540                field.references_id(),
541                true,
542            ))
543        })
544        .await?;
545        Ok(records.into_iter().map(|record| record.output).collect())
546    }
547
548    /// Complete each extracted row after its output policies, retaining the original row order.
549    /// A failed projection skips its completion; other started rows still finish before an error returns.
550    pub async fn project_adapter_records_then<R: Send, F>(
551        &self,
552        mut records: Vec<AdapterRecord>,
553        supports_native_json: bool,
554        supports_native_dates: bool,
555        complete: impl Fn(usize, FieldMap) -> F + Sync,
556    ) -> AuthResult<Vec<R>>
557    where
558        F: std::future::Future<Output = AuthResult<R>> + Send,
559    {
560        super::batch::project_fields_then(
561            &mut records,
562            self.fields(),
563            |record, name, field| {
564                Box::pin(project_adapter_field(
565                    record,
566                    name,
567                    field,
568                    supports_native_json,
569                    supports_native_dates,
570                ))
571            },
572            |index, record| complete(index, std::mem::take(&mut record.output)),
573        )
574        .await
575    }
576
577    /// Continue ready extracted records together, retaining their original row indices.
578    pub async fn project_adapter_records_batches_then<R: Send, F>(
579        &self,
580        mut records: Vec<AdapterRecord>,
581        supports_native_json: bool,
582        supports_native_dates: bool,
583        complete: impl Fn(Vec<(usize, FieldMap)>) -> F + Sync,
584    ) -> AuthResult<Vec<R>>
585    where
586        F: std::future::Future<Output = AuthResult<Vec<(usize, R)>>> + Send,
587    {
588        super::batch::project_fields_batches_then(
589            &mut records,
590            self.fields(),
591            |record, name, field| {
592                Box::pin(project_adapter_field(
593                    record,
594                    name,
595                    field,
596                    supports_native_json,
597                    supports_native_dates,
598                ))
599            },
600            |_, record| Ok(std::mem::take(&mut record.output)),
601            complete,
602        )
603        .await
604    }
605}
606
607type OrganizationRecord = AdapterRecord;
608
609async fn project_organization_field(
610    record: &mut OrganizationRecord,
611    name: &str,
612    field: &UserFieldConfig,
613    supports_native_json: bool,
614) -> AuthResult<()> {
615    if name == "id" {
616        return Ok(());
617    }
618    let value = record
619        .storage
620        .get(resolve_field_name(field.field_name.as_deref(), name))
621        .cloned()
622        .unwrap_or_default();
623    let value = if let Some(capabilities) = record.raw_storage {
624        field.adapter_output_from_raw(value, capabilities).await?
625    } else {
626        field.adapter_output(value, supports_native_json).await?
627    };
628    super::organization::assign_output(&mut record.output, name, field, value)
629}
630
631fn adapter_records(storage: &[FieldMap]) -> AuthResult<Vec<AdapterRecord>> {
632    storage
633        .iter()
634        .map(|storage| {
635            let mut core = FieldMap::new();
636            if let Some(id) = storage.get("id") {
637                let _ = core.insert(
638                    "id".into(),
639                    if id.is_null() || id.is_undefined() {
640                        id.clone()
641                    } else {
642                        Value::String(
643                            crate::SchemaValue::<String>::from_field(id.clone())
644                                .display_string()?,
645                        )
646                    },
647                );
648            }
649            Ok(AdapterRecord::new(core, storage.clone()))
650        })
651        .collect()
652}
653
654async fn project_adapter_field(
655    record: &mut AdapterRecord,
656    name: &str,
657    field: &UserFieldConfig,
658    supports_native_json: bool,
659    supports_native_dates: bool,
660) -> AuthResult<()> {
661    project_adapter_field_with_capabilities(
662        record,
663        name,
664        field,
665        FieldOutputCapabilities::json_only(supports_native_json),
666        supports_native_dates,
667    )
668    .await
669}
670
671async fn project_adapter_field_with_capabilities(
672    record: &mut AdapterRecord,
673    name: &str,
674    field: &UserFieldConfig,
675    capabilities: FieldOutputCapabilities,
676    supports_native_dates: bool,
677) -> AuthResult<()> {
678    if name == "id" {
679        return Ok(());
680    }
681    let value = record
682        .storage
683        .get(resolve_field_name(field.field_name.as_deref(), name))
684        .cloned()
685        .unwrap_or_default();
686    let value = if let Some(capabilities) = record.raw_storage {
687        field.adapter_output_from_raw(value, capabilities).await?
688    } else {
689        field
690            .adapter_output_with_capabilities(value, capabilities)
691            .await?
692    };
693    let value = project_output_value(value, field, supports_native_dates);
694    let _ = record.output.insert(name.to_owned(), value);
695    Ok(())
696}
697
698pub(crate) async fn project_adapter_value(
699    value: Value,
700    field: &UserFieldConfig,
701    supports_native_json: bool,
702    supports_native_dates: bool,
703) -> AuthResult<Value> {
704    let value = field.adapter_output(value, supports_native_json).await?;
705    Ok(project_output_value(value, field, supports_native_dates))
706}
707
708fn project_output_value(
709    value: Value,
710    field: &UserFieldConfig,
711    supports_native_dates: bool,
712) -> Value {
713    if !supports_native_dates
714        && !field.uses_id_output()
715        && matches!(field.field_type, UserFieldType::Date)
716    {
717        match value {
718            Value::String(text) => Value::Date(
719                crate::utils::date::parse_adapter_date(&text)
720                    .map(crate::FieldDate::from)
721                    .unwrap_or_else(crate::FieldDate::invalid),
722            ),
723            value => value,
724        }
725    } else {
726        value
727    }
728}
729
730#[cfg(test)]
731mod tests {
732    use super::*;
733    use crate::user_fields::{FieldTransforms, UserFieldTransform};
734    macro_rules! json {
735        ($($token:tt)*) => { Value::from_json(serde_json::json!($($token)*)).expect("valid JSON field") };
736    }
737
738    #[tokio::test]
739    async fn raw_adapter_json_reaches_callbacks_before_output_decoding() -> AuthResult<()> {
740        for raw in [Value::Null, json!({"owner": "Alpha"}), json!(["Alpha"])] {
741            let expected = raw.clone();
742            let fields = UserConfig {
743                additional_fields: Some(
744                    [(
745                        "payload".into(),
746                        UserFieldConfig {
747                            field_type: UserFieldType::Json,
748                            transform: Some(FieldTransforms {
749                                output: Some(UserFieldTransform::new(move |value| {
750                                    assert_eq!(value, expected.clone());
751                                    Ok(json!("[3,4]"))
752                                })),
753                                ..Default::default()
754                            }),
755                            ..Default::default()
756                        },
757                    )]
758                    .into(),
759                ),
760            };
761            let mut record = AdapterRecord::new(FieldMap::new(), FieldMap::new());
762            record.map_storage_fields(
763                &fields,
764                FieldOutputCapabilities::json_only(false),
765                |_, _| Ok(Some(raw.clone())),
766            )?;
767            let projected = fields
768                .project_adapter_records_with_capabilities(
769                    vec![record],
770                    FieldOutputCapabilities::json_only(false),
771                    true,
772                )
773                .await?;
774            assert_eq!(
775                projected.first().and_then(|row| row.get("payload")),
776                Some(&json!([3, 4]))
777            );
778        }
779        Ok(())
780    }
781}