Skip to main content

better_auth_core/user_fields/
organization.rs

1use super::{UserConfig, UserFieldConfig, UserFieldType};
2use crate::store::schema::resolve_field_name;
3#[cfg(test)]
4use crate::user_fields::{FieldTransforms, UserFieldTransform};
5use crate::{AuthError, AuthResult};
6use crate::{FieldMap, FieldValue as Value};
7
8impl UserConfig {
9    /// Apply configured policies once to logical core fields and application fields.
10    /// The adapter owns `id`; custom field attributes cannot replace its policy.
11    pub async fn organization_storage_fields(
12        &self,
13        core: FieldMap,
14        extras: FieldMap,
15        create: bool,
16    ) -> AuthResult<FieldMap> {
17        self.organization_storage_fields_with_binding(core, extras, create, |_, _, value| Ok(value))
18            .await
19    }
20
21    /// Bind each configured field before omitting Undefined or merging its storage column.
22    #[doc(hidden)]
23    pub async fn organization_storage_fields_with_binding(
24        &self,
25        core: FieldMap,
26        extras: FieldMap,
27        create: bool,
28        bind: impl Fn(&str, &UserFieldConfig, Value) -> AuthResult<Value>,
29    ) -> AuthResult<FieldMap> {
30        let mut output = core.clone();
31        output.retain(|name, _| name == "id" || !self.fields().contains_key(name));
32        let mut input = extras;
33        input.extend(core);
34        output.extend(self.storage_fields_async(input, create, true, bind).await?);
35        Ok(output)
36    }
37
38    /// Validate Organization route fields without applying adapter defaults or transforms.
39    pub fn parse_organization_input(
40        &self,
41        input: &FieldMap,
42        prefix: &str,
43        partial: bool,
44    ) -> AuthResult<FieldMap> {
45        let mut parsed = FieldMap::new();
46        let mut errors = Vec::new();
47        for (name, field) in self.fields() {
48            match field.validate_organization_input(
49                input.get(name),
50                &format!("{prefix}.{name}"),
51                partial,
52            ) {
53                Ok(Some(value)) => {
54                    let _ = parsed.insert(name.clone(), value);
55                }
56                Ok(None) => {}
57                Err(AuthError::FieldInput { message, .. }) => errors.push(message),
58                Err(error) => return Err(error),
59            }
60        }
61        if errors.is_empty() {
62            Ok(parsed)
63        } else {
64            Err(AuthError::FieldInput {
65                code: "VALIDATION_ERROR",
66                message: errors.join("; "),
67            })
68        }
69    }
70
71    /// Apply the adapter's field mapping and output transforms before route visibility filtering.
72    pub async fn output_fields(&self, storage: &FieldMap) -> AuthResult<FieldMap> {
73        // Projection preserves the one input row.
74        Ok(self
75            .output_fields_many(std::slice::from_ref(storage))
76            .await?
77            .remove(0))
78    }
79
80    /// Project a database result while retaining row order and per-row field order.
81    pub async fn output_fields_many(&self, storage: &[FieldMap]) -> AuthResult<Vec<FieldMap>> {
82        self.output_fields_many_with_json(storage, |_| true).await
83    }
84
85    pub(crate) async fn output_memory_fields_many(
86        &self,
87        storage: &[FieldMap],
88    ) -> AuthResult<Vec<FieldMap>> {
89        self.output_fields_many_with_json(storage, UserFieldConfig::references_id)
90            .await
91    }
92
93    async fn output_fields_many_with_json(
94        &self,
95        storage: &[FieldMap],
96        supports_native_json: impl Fn(&UserFieldConfig) -> bool + Sync,
97    ) -> AuthResult<Vec<FieldMap>> {
98        let mut rows: Vec<_> = storage
99            .iter()
100            .map(|storage| (storage, FieldMap::new()))
101            .collect();
102        super::batch::project_fields(
103            &mut rows,
104            self.fields(),
105            |(storage, output), name, field| {
106                let supports_native_json = supports_native_json(field);
107                Box::pin(async move {
108                    let value = storage
109                        .get(resolve_field_name(field.field_name.as_deref(), name))
110                        .cloned();
111                    assign_output(
112                        output,
113                        name,
114                        field,
115                        field
116                            .adapter_output(value.unwrap_or_default(), supports_native_json)
117                            .await?,
118                    )
119                })
120            },
121        )
122        .await?;
123        Ok(rows.into_iter().map(|(_, output)| output).collect())
124    }
125
126    /// Apply only `returned` flags to fields already projected by the adapter.
127    pub fn filter_returned_fields(&self, fields: &mut FieldMap) {
128        fields.retain(|name, _| self.fields().get(name).is_none_or(|field| field.returned()));
129    }
130}
131
132impl UserFieldConfig {
133    /// Validate one Organization field without applying adapter defaults or transforms.
134    pub fn validate_organization_input(
135        &self,
136        value: Option<&Value>,
137        location: &str,
138        partial: bool,
139    ) -> AuthResult<Option<Value>> {
140        if !self.input() || value.is_none() && (partial || self.required == Some(false)) {
141            return Ok(None);
142        }
143        if matches!(value, Some(Value::Null | Value::Undefined)) && self.required == Some(false) {
144            return Ok(value.cloned());
145        }
146        let expected = match &self.field_type {
147            UserFieldType::Enum(_) => return Ok(value.cloned()),
148            UserFieldType::String => "string",
149            UserFieldType::Number => "number",
150            UserFieldType::Boolean => "boolean",
151            UserFieldType::Date => "date",
152            UserFieldType::Json => "json",
153            UserFieldType::StringArray | UserFieldType::NumberArray => "array",
154        };
155        let mut errors = Vec::new();
156        let valid = match (&self.field_type, value) {
157            (UserFieldType::String, Some(Value::String(_) | Value::Utf16String(_)))
158            | (UserFieldType::Boolean, Some(Value::Bool(_))) => true,
159            (UserFieldType::Number, Some(Value::Number(value))) => value.is_finite(),
160            (UserFieldType::Json, Some(value)) => json_input(value),
161            (UserFieldType::Date, Some(Value::Date(value))) => value.milliseconds().is_finite(),
162            (
163                UserFieldType::StringArray | UserFieldType::NumberArray,
164                Some(Value::Array(values)),
165            ) => {
166                let item_type = if matches!(self.field_type, UserFieldType::StringArray) {
167                    "string"
168                } else {
169                    "number"
170                };
171                for (index, value) in values.iter().enumerate() {
172                    if type_name(Some(value)) != item_type
173                        || matches!(value, Value::Number(number) if !number.is_finite())
174                    {
175                        errors.push(invalid_type(
176                            &format!("{location}.{index}"),
177                            item_type,
178                            Some(value),
179                        ));
180                    }
181                }
182                true
183            }
184            _ => false,
185        };
186        if !valid {
187            errors.push(if matches!(self.field_type, UserFieldType::Json) {
188                format!("[{location}] Invalid input")
189            } else {
190                invalid_type(location, expected, value)
191            });
192        }
193        if errors.is_empty() {
194            Ok(value.cloned())
195        } else {
196            Err(AuthError::FieldInput {
197                code: "VALIDATION_ERROR",
198                message: errors.join("; "),
199            })
200        }
201    }
202}
203
204fn json_input(value: &Value) -> bool {
205    match value {
206        Value::Null | Value::Bool(_) | Value::String(_) | Value::Utf16String(_) => true,
207        Value::Number(value) => value.is_finite(),
208        Value::Array(values) => values.iter().all(json_input),
209        Value::Object(values) => values.values().all(json_input),
210        Value::Undefined | Value::Date(_) => false,
211    }
212}
213
214fn type_name(value: Option<&Value>) -> &'static str {
215    match value {
216        None | Some(Value::Undefined) => "undefined",
217        Some(Value::Null) => "null",
218        Some(Value::Bool(_)) => "boolean",
219        Some(Value::Number(value)) if value.is_nan() => "NaN",
220        Some(Value::Number(value)) if *value == f64::INFINITY => "Infinity",
221        Some(Value::Number(value)) if *value == f64::NEG_INFINITY => "-Infinity",
222        Some(Value::Number(_)) => "number",
223        Some(Value::String(_) | Value::Utf16String(_)) => "string",
224        Some(Value::Date(_)) => "Date",
225        Some(Value::Array(_)) => "array",
226        Some(Value::Object(_)) => "object",
227    }
228}
229
230fn invalid_type(location: &str, expected: &str, value: Option<&Value>) -> String {
231    format!(
232        "[{location}] Invalid input: expected {expected}, received {}",
233        type_name(value)
234    )
235}
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240    use crate::user_fields::UserFieldConfig;
241    macro_rules! json {
242        ($($token:tt)*) => { Value::from_json(serde_json::json!($($token)*)).expect("valid JSON field") };
243    }
244    use std::sync::Arc;
245
246    #[test]
247    fn native_json_fields_reject_values_that_json_would_omit_or_change() {
248        let field = UserFieldConfig {
249            field_type: UserFieldType::Json,
250            ..Default::default()
251        };
252        for value in [
253            Value::Undefined,
254            Value::from(crate::FieldDate::from_milliseconds(0.0)),
255            Value::from(f64::NAN),
256            Value::from(vec![Value::from(f64::INFINITY)]),
257            Value::from(FieldMap::from([("omitted".into(), Value::Undefined)])),
258        ] {
259            let error = field
260                .validate_organization_input(Some(&value), "body.payload", false)
261                .unwrap_err();
262            assert!(
263                matches!(error, AuthError::FieldInput { code: "VALIDATION_ERROR", message } if message == "[body.payload] Invalid input")
264            );
265        }
266        let lone_surrogate = Value::from(crate::Utf16String::from_units(vec![0xd800]));
267        let object = Value::from(FieldMap::from([("text".into(), lone_surrogate)]));
268        let output = field
269            .validate_organization_input(Some(&object), "body.payload", false)
270            .unwrap()
271            .unwrap();
272        assert!(output.strict_equals(&object));
273    }
274
275    #[test]
276    fn native_schema_validation_preserves_dates_and_rejects_nonfinite_numbers() {
277        let field = UserFieldConfig {
278            field_type: UserFieldType::Date,
279            required: Some(false),
280            ..Default::default()
281        };
282        let date = Value::from(crate::FieldDate::from_milliseconds(0.0));
283        let output = field
284            .validate_organization_input(Some(&date), "body.date", false)
285            .unwrap()
286            .unwrap();
287        assert!(output.strict_equals(&date));
288        assert_eq!(
289            field
290                .validate_organization_input(Some(&Value::Undefined), "body.date", false)
291                .unwrap(),
292            Some(Value::Undefined)
293        );
294        let invalid = Value::from(crate::FieldDate::invalid());
295        assert!(
296            matches!(field.validate_organization_input(Some(&invalid), "body.date", false).unwrap_err(), AuthError::FieldInput { message, .. } if message == "[body.date] Invalid input: expected date, received Date")
297        );
298        let field = UserFieldConfig {
299            field_type: UserFieldType::Number,
300            ..Default::default()
301        };
302        for (number, label) in [
303            (f64::NAN, "NaN"),
304            (f64::INFINITY, "Infinity"),
305            (f64::NEG_INFINITY, "-Infinity"),
306        ] {
307            assert!(
308                matches!(field.validate_organization_input(Some(&Value::from(number)), "body.number", false).unwrap_err(), AuthError::FieldInput { message, .. } if message == format!("[body.number] Invalid input: expected number, received {label}"))
309            );
310        }
311    }
312
313    #[tokio::test]
314    async fn organization_validation_keeps_route_and_adapter_policies_separate() {
315        let schema = UserConfig {
316            additional_fields: Some(
317                [
318                    (
319                        "implicit".into(),
320                        UserFieldConfig {
321                            default_value: Some(json!("default")),
322                            ..Default::default()
323                        },
324                    ),
325                    (
326                        "label".into(),
327                        UserFieldConfig {
328                            required: Some(false),
329                            field_name: Some("stored_label".into()),
330                            validator: Some(Arc::new(|_| {
331                                Err(AuthError::bad_request("must not run"))
332                            })),
333                            transform: Some(FieldTransforms {
334                                input: Some(UserFieldTransform::new(|value| {
335                                    Ok(json!(format!("{}:in", value.as_str().unwrap())))
336                                })),
337                                ..Default::default()
338                            }),
339                            ..Default::default()
340                        },
341                    ),
342                    (
343                        "protected".into(),
344                        UserFieldConfig {
345                            input: Some(false),
346                            default_value: Some(json!("server")),
347                            ..Default::default()
348                        },
349                    ),
350                ]
351                .into(),
352            ),
353        };
354        let error = schema
355            .parse_organization_input(&FieldMap::new(), "body", false)
356            .unwrap_err();
357        assert!(
358            error
359                .to_string()
360                .contains("[body.implicit] Invalid input: expected string, received undefined")
361        );
362        let input =
363            json!({"implicit":"supplied", "label":"raw", "protected":"client", "unknown":true});
364        let parsed = schema
365            .parse_organization_input(input.as_object().unwrap(), "body", false)
366            .unwrap();
367        assert_eq!(
368            parsed,
369            json!({"implicit":"supplied", "label":"raw"})
370                .as_object()
371                .unwrap()
372                .clone()
373        );
374        let stored = schema.storage_fields(parsed, true).await.unwrap();
375        assert_eq!(
376            stored,
377            json!({"implicit":"supplied", "stored_label":"raw:in", "protected":"server"})
378                .as_object()
379                .unwrap()
380                .clone()
381        );
382        assert!(
383            schema
384                .parse_organization_input(&FieldMap::new(), "body.data", true)
385                .unwrap()
386                .is_empty()
387        );
388    }
389
390    #[test]
391    fn organization_partial_fields_keep_nullable_and_array_contracts() {
392        let schema = UserConfig {
393            additional_fields: Some(
394                [
395                    (
396                        "required".into(),
397                        UserFieldConfig {
398                            required: Some(true),
399                            ..Default::default()
400                        },
401                    ),
402                    (
403                        "optional".into(),
404                        UserFieldConfig {
405                            required: Some(false),
406                            ..Default::default()
407                        },
408                    ),
409                    (
410                        "tags".into(),
411                        UserFieldConfig {
412                            required: Some(false),
413                            field_type: UserFieldType::StringArray,
414                            ..Default::default()
415                        },
416                    ),
417                    (
418                        "category".into(),
419                        UserFieldConfig {
420                            field_type: UserFieldType::Enum(vec!["basic".into()]),
421                            ..Default::default()
422                        },
423                    ),
424                ]
425                .into(),
426            ),
427        };
428        let valid = json!({"optional":null,"category":"unlisted"});
429        assert_eq!(
430            schema
431                .parse_organization_input(valid.as_object().unwrap(), "body.data", true)
432                .unwrap(),
433            valid.as_object().unwrap().clone()
434        );
435        let invalid = json!({"required":null,"tags":["valid",4]});
436        assert!(
437            schema
438                .parse_organization_input(invalid.as_object().unwrap(), "body.data", true)
439                .unwrap_err()
440                .to_string()
441                .contains("[body.data.tags.1] Invalid input: expected string, received number")
442        );
443    }
444}
445
446pub(crate) fn assign_output(
447    output: &mut FieldMap,
448    name: &str,
449    field: &UserFieldConfig,
450    mut value: Value,
451) -> AuthResult<()> {
452    if !field.uses_id_output() {
453        field.normalize_date(&mut value)?;
454    }
455    let _ = output.insert(name.to_owned(), value);
456    Ok(())
457}