Skip to main content

better_auth_core/user_fields/
adapter.rs

1use super::{UserConfig, UserFieldConfig, UserFieldType};
2use crate::AuthResult;
3use crate::store::schema::resolve_field_name;
4use crate::{FieldMap, FieldValue as Value};
5
6/// Adapter capabilities for decoding values after an application output callback.
7#[derive(Clone, Copy, Debug)]
8pub struct FieldOutputCapabilities {
9    /// JSON values need no text decoding when the adapter supports native JSON.
10    pub supports_native_json: bool,
11    /// Array values need no text decoding when the adapter supports arrays.
12    pub supports_arrays: bool,
13    /// Boolean values need no numeric decoding when the adapter supports booleans.
14    pub supports_booleans: bool,
15}
16
17impl FieldOutputCapabilities {
18    pub(crate) const fn json_only(supports_native_json: bool) -> Self {
19        Self {
20            supports_native_json,
21            supports_arrays: true,
22            supports_booleans: true,
23        }
24    }
25}
26
27impl UserConfig {
28    /// Resolve schema order and replace the adapter-owned ID policy in its existing slot.
29    #[doc(hidden)]
30    pub fn adapter_fields(&self, native: &[&str]) -> Self {
31        let mut fields: indexmap::IndexMap<_, _> = self
32            .ordered_fields(native)
33            .into_iter()
34            .map(|(name, field)| (name.to_owned(), field.clone()))
35            .collect();
36        let _ = fields.insert("id".into(), UserFieldConfig::default());
37        Self {
38            additional_fields: Some(fields),
39        }
40    }
41
42    /// Apply creation policies and resolve the current ID policy at its schema slot.
43    #[doc(hidden)]
44    pub async fn create_adapter_storage_fields<I>(
45        &self,
46        input: FieldMap,
47        generate_id: impl FnMut() -> AuthResult<Option<I>>,
48        bind: impl Fn(&str, &UserFieldConfig, Value) -> AuthResult<Value>,
49    ) -> AuthResult<(FieldMap, Option<I>)> {
50        self.storage_fields_with_adapter_id(input, true, generate_id, bind)
51            .await
52    }
53
54    /// Resolve the adapter-owned ID between field policies without applying application ID callbacks.
55    #[doc(hidden)]
56    pub async fn storage_fields_with_adapter_id<I>(
57        &self,
58        input: FieldMap,
59        create: bool,
60        mut resolve_id: impl FnMut() -> AuthResult<Option<I>>,
61        bind: impl Fn(&str, &UserFieldConfig, Value) -> AuthResult<Value>,
62    ) -> AuthResult<(FieldMap, Option<I>)> {
63        let mut id = None;
64        let output = self
65            .storage_fields_with_bound_id(
66                input,
67                create,
68                || {
69                    id = resolve_id()?;
70                    Ok(None)
71                },
72                bind,
73            )
74            .await?;
75        Ok((output, id))
76    }
77
78    /// Apply update policies and preserve ID writes in schema order with aliased fields.
79    #[doc(hidden)]
80    pub async fn update_adapter_storage_fields(
81        &self,
82        input: FieldMap,
83        resolve_id: impl FnMut() -> AuthResult<Option<Value>>,
84        bind: impl Fn(&str, &UserFieldConfig, Value) -> AuthResult<Value>,
85    ) -> AuthResult<FieldMap> {
86        self.storage_fields_with_bound_id(input, false, resolve_id, bind)
87            .await
88    }
89
90    /// Bind ID and application fields to the same storage keys in schema order.
91    #[doc(hidden)]
92    pub async fn storage_fields_with_bound_id(
93        &self,
94        input: FieldMap,
95        create: bool,
96        mut resolve_id: impl FnMut() -> AuthResult<Option<Value>>,
97        bind: impl Fn(&str, &UserFieldConfig, Value) -> AuthResult<Value>,
98    ) -> AuthResult<FieldMap> {
99        let schema = self.adapter_fields(&[]);
100        let mut output = FieldMap::new();
101        for (name, field) in schema.fields() {
102            let value = if name == "id" {
103                resolve_id()?
104            } else {
105                field.storage_input(input.get(name), create).await?
106            };
107            if let Some(value) = value {
108                let storage_name = resolve_field_name(field.field_name.as_deref(), name);
109                let value = if name == "id" {
110                    value
111                } else {
112                    bind(storage_name, field, value)?
113                };
114                if !value.is_undefined() {
115                    let _ = output.insert(storage_name.to_owned(), value);
116                }
117            }
118        }
119        Ok(output)
120    }
121
122    pub(crate) fn ordered_fields(&self, native: &[&str]) -> Vec<(&str, &UserFieldConfig)> {
123        let mut fields: Vec<_> = self
124            .fields()
125            .iter()
126            .map(|(name, field)| (name.as_str(), field))
127            .collect();
128        // Native replacements retain schema positions. Integer keys use JavaScript property order.
129        fields.sort_by_key(|(name, _)| {
130            let native = native
131                .iter()
132                .position(|field| field == name)
133                .unwrap_or(native.len());
134            crate::utils::json::array_index(name)
135                .map_or((true, 0, native), |index| (false, index, 0))
136        });
137        fields
138    }
139
140    /// Apply storage policies before converting JSON for the selected adapter.
141    pub async fn storage_fields_for_adapter(
142        &self,
143        input: FieldMap,
144        create: bool,
145        supports_native_json: bool,
146        native_json_field: impl Fn(&str) -> bool,
147    ) -> AuthResult<FieldMap> {
148        self.storage_fields_with_binding(input, create, |name, field, value| {
149            field.adapter_input(value, supports_native_json, native_json_field(name))
150        })
151        .await
152    }
153
154    /// Bind transformed fields to an adapter without repeating defaults or application transforms.
155    pub async fn storage_fields_with_binding(
156        &self,
157        input: FieldMap,
158        create: bool,
159        bind: impl Fn(&str, &UserFieldConfig, Value) -> AuthResult<Value>,
160    ) -> AuthResult<FieldMap> {
161        self.storage_fields_async(input, create, false, bind).await
162    }
163}
164
165impl UserFieldConfig {
166    /// Convert a transformed value to the selected adapter's database binding.
167    /// JSON text uses JavaScript property ordering and number formatting.
168    /// Serialization errors propagate before the adapter writes the value.
169    pub fn adapter_input(
170        &self,
171        value: Value,
172        supports_native_json: bool,
173        native_json_field: bool,
174    ) -> AuthResult<Value> {
175        if self.references_id() {
176            match (&self.field_type, &value) {
177                (UserFieldType::Boolean, Value::Bool(value)) if !supports_native_json => {
178                    return Ok(Value::from(i64::from(*value)));
179                }
180                (UserFieldType::StringArray | UserFieldType::NumberArray, Value::Array(_))
181                | (UserFieldType::Json, Value::Object(_) | Value::Array(_) | Value::Date(_)) => {
182                    return json_text(value);
183                }
184                _ => {}
185            }
186        }
187        if !supports_native_json
188            && matches!(self.field_type, UserFieldType::Json)
189            && (value.is_null()
190                || (!native_json_field
191                    && (value.is_object() || value.is_array() || matches!(value, Value::Date(_)))))
192        {
193            json_text(value)
194        } else {
195            Ok(value)
196        }
197    }
198
199    /// Whether this field uses the adapter's reference-to-ID conversion.
200    pub fn references_id(&self) -> bool {
201        self.references
202            .as_ref()
203            .is_some_and(|reference| reference.field == "id")
204    }
205
206    pub(crate) fn uses_id_output(&self) -> bool {
207        self.references_id() || self.field_name.as_deref() == Some("id")
208    }
209
210    /// Await the output policy before decoding adapter storage values.
211    pub async fn adapter_output(
212        &self,
213        value: Value,
214        supports_native_json: bool,
215    ) -> AuthResult<Value> {
216        self.adapter_output_with_capabilities(
217            value,
218            FieldOutputCapabilities::json_only(supports_native_json),
219        )
220        .await
221    }
222
223    pub(crate) async fn adapter_output_with_capabilities(
224        &self,
225        value: Value,
226        capabilities: FieldOutputCapabilities,
227    ) -> AuthResult<Value> {
228        let value = self.prepare_output(value, capabilities.supports_native_json)?;
229        self.adapter_output_from_raw(value, capabilities).await
230    }
231
232    pub(crate) async fn adapter_output_from_raw(
233        &self,
234        mut value: Value,
235        capabilities: FieldOutputCapabilities,
236    ) -> AuthResult<Value> {
237        if let Some(transform) = self.output_transform() {
238            value = transform.call(value).await?;
239        }
240        self.finish_output(value, capabilities)
241    }
242
243    fn prepare_output(&self, value: Value, supports_native_json: bool) -> AuthResult<Value> {
244        if !supports_native_json
245            && self.field_name.as_deref() != Some("id")
246            && matches!(self.field_type, UserFieldType::Json)
247        {
248            match value {
249                Value::Object(_) | Value::Array(_) | Value::Date(_) => json_text(value),
250                value => Ok(value),
251            }
252        } else {
253            Ok(value)
254        }
255    }
256
257    fn finish_output(
258        &self,
259        value: Value,
260        capabilities: FieldOutputCapabilities,
261    ) -> AuthResult<Value> {
262        if self.uses_id_output() {
263            return if value.is_null() || value.is_undefined() {
264                Ok(value)
265            } else {
266                value.display_utf16().map(Value::from)
267            };
268        }
269        match value {
270            Value::String(text)
271                if (!capabilities.supports_native_json
272                    && matches!(self.field_type, UserFieldType::Json))
273                    || (!capabilities.supports_arrays
274                        && matches!(
275                            self.field_type,
276                            UserFieldType::StringArray | UserFieldType::NumberArray
277                        )) =>
278            {
279                revive_json(crate::utils::json::safe_json_parse(&text))
280            }
281            Value::Number(number)
282                if !capabilities.supports_booleans
283                    && matches!(self.field_type, UserFieldType::Boolean) =>
284            {
285                Ok(Value::Bool(number == 1.0))
286            }
287            value => Ok(value),
288        }
289    }
290}
291
292fn json_text(value: Value) -> AuthResult<Value> {
293    Ok(value.stringify()?.map(Value::String).unwrap_or_default())
294}
295
296fn revive_json(value: serde_json::Value) -> AuthResult<Value> {
297    match value {
298        serde_json::Value::String(text) => Ok(crate::utils::json::parse_json_date(&text)
299            .map_or_else(|| Value::String(text), Value::from)),
300        serde_json::Value::Array(values) => values
301            .into_iter()
302            .map(revive_json)
303            .collect::<AuthResult<Vec<_>>>()
304            .map(Value::from),
305        serde_json::Value::Object(values) => values
306            .into_iter()
307            .map(|(name, value)| Ok((name, revive_json(value)?)))
308            .collect::<AuthResult<FieldMap>>()
309            .map(Value::from),
310        value => Value::from_json(value),
311    }
312}