Skip to main content

alibi_entity_codegen/
writes.rs

1use super::*;
2/// What a fresh row stores in one declared field.
3#[derive(Clone)]
4pub enum Insert {
5    /// An expression over the role's creation input, `now` and (for
6    /// sessions) `token`, convertible into the field type with `Into`.
7    Value(TokenStream),
8    /// SQL `NULL` of the field's optional type.
9    Null,
10    /// Omitted, so the database default applies.
11    Default,
12}
13
14/// The value a fresh row stores in every declared field except `id`, in
15/// declaration order. Each backend binds its own identifier type.
16#[must_use]
17pub fn insert_values(role: EntityRole, fields: &FieldsNamed) -> Vec<(Ident, Insert)> {
18    let has = |name: &str| has_field(fields, name);
19    let optional = |name: &str| optional_field(fields, name);
20    let mut values: Vec<(&str, Insert)> = Vec::new();
21    let mut value =
22        |name: &'static str, expr: TokenStream| values.push((name, Insert::Value(expr)));
23    match role {
24        EntityRole::User => {
25            for name in ["email", "name"] {
26                let field = Ident::new(name, Span::call_site());
27                let expression = if optional(name) {
28                    quote! { create_user.#field }
29                } else {
30                    quote! { create_user.#field.unwrap_or_default() }
31                };
32                value(name, expression);
33            }
34            value("image", quote! { create_user.image });
35            value(
36                "email_verified",
37                quote! { create_user.email_verified.unwrap_or(false) },
38            );
39            value(
40                "created_at",
41                quote! { create_user.created_at.unwrap_or(now) },
42            );
43            value(
44                "updated_at",
45                quote! { create_user.updated_at.unwrap_or(now) },
46            );
47            for name in [
48                "username",
49                "display_username",
50                "role",
51                "is_anonymous",
52                "phone_number",
53                "phone_number_verified",
54                "last_login_method",
55            ] {
56                let field = Ident::new(name, Span::call_site());
57                value(name, quote! { create_user.#field });
58            }
59            // Flags that may be nullable columns keep the caller's `Option`;
60            // non-null columns default to `false`.
61            for name in ["two_factor_enabled", "banned"] {
62                let field = Ident::new(name, Span::call_site());
63                if optional(name) {
64                    value(name, quote! { create_user.#field });
65                } else {
66                    value(name, quote! { create_user.#field.unwrap_or(false) });
67                }
68            }
69            value(
70                "metadata",
71                quote! { create_user.metadata.unwrap_or(::serde_json::json!({})) },
72            );
73            values.push(("ban_reason", Insert::Null));
74            values.push(("ban_expires", Insert::Null));
75        }
76        EntityRole::Session => {
77            value("token", quote! { token });
78            value("created_at", quote! { now });
79            value("updated_at", quote! { now });
80            value("active", quote! { true });
81            for name in [
82                "user_id",
83                "expires_at",
84                "ip_address",
85                "user_agent",
86                "impersonated_by",
87                "active_organization_id",
88                "active_team_id",
89            ] {
90                let field = Ident::new(name, Span::call_site());
91                value(name, quote! { create_session.#field });
92            }
93        }
94        EntityRole::Account => {
95            value("created_at", quote! { now });
96            value("updated_at", quote! { now });
97            for name in [
98                "account_id",
99                "provider_id",
100                "user_id",
101                "access_token",
102                "refresh_token",
103                "id_token",
104                "access_token_expires_at",
105                "refresh_token_expires_at",
106                "scope",
107                "password",
108            ] {
109                let field = Ident::new(name, Span::call_site());
110                value(name, quote! { create_account.#field });
111            }
112        }
113        EntityRole::Verification => {
114            value("created_at", quote! { now });
115            value("updated_at", quote! { now });
116            for name in ["identifier", "value", "expires_at"] {
117                let field = Ident::new(name, Span::call_site());
118                value(name, quote! { verification.#field });
119            }
120        }
121    }
122    fields
123        .named
124        .iter()
125        .filter_map(|field| field.ident.clone())
126        .filter(|ident| ident != "id")
127        .map(|ident| {
128            let insert = values
129                .iter()
130                .find(|(name, _)| ident == name && has(name))
131                .map_or(Insert::Default, |(_, insert)| insert.clone());
132            (ident, insert)
133        })
134        .collect()
135}
136
137/// Renders `field = value` on the active row, where `Insert::Value` is
138/// convertible into the field type with `Into`. `Insert::Default` is never
139/// passed.
140pub type SetField<'a> = &'a dyn Fn(&Ident, &Insert) -> TokenStream;
141
142/// The `apply_update` body of the role over `update` and `now`: every field
143/// present in `update` is written; absent plugin fields are skipped.
144#[must_use]
145pub fn update_statements(role: EntityRole, fields: &FieldsNamed, set: SetField<'_>) -> TokenStream {
146    let has = |name: &str| has_field(fields, name);
147    let ident = |name: &str| Ident::new(name, Span::call_site());
148    // `if let Some(x) = update.x { set(x, <wrap>(x)) }`
149    let some = |name: &str, wrap: fn(&Ident) -> TokenStream| {
150        let field = ident(name);
151        let value = wrap(&field);
152        let assign = set(&field, &Insert::Value(value));
153        quote! { if let ::std::option::Option::Some(#field) = update.#field { #assign } }
154    };
155    let wrapped = |field: &Ident| quote! { ::std::option::Option::Some(#field) };
156    let direct = |field: &Ident| quote! { #field };
157    let mut statements = Vec::new();
158    match role {
159        EntityRole::User => {
160            for name in ["email", "name", "image"] {
161                let wrap = if optional_field(fields, name) {
162                    wrapped
163                } else {
164                    direct
165                };
166                statements.push(some(name, wrap));
167            }
168            statements.push(some("email_verified", direct));
169            for name in ["username", "display_username", "role"] {
170                if has(name) {
171                    statements.push(some(name, wrapped));
172                }
173            }
174            for name in ["two_factor_enabled", "metadata"] {
175                if has(name) {
176                    statements.push(some(name, direct));
177                }
178            }
179            if has("banned") && has("ban_reason") && has("ban_expires") {
180                let banned = set(&ident("banned"), &Insert::Value(quote! { banned }));
181                let clear_reason = set(&ident("ban_reason"), &Insert::Null);
182                let clear_expires = set(&ident("ban_expires"), &Insert::Null);
183                let reason = set(
184                    &ident("ban_reason"),
185                    &Insert::Value(quote! { ::std::option::Option::Some(ban_reason) }),
186                );
187                let expires = set(
188                    &ident("ban_expires"),
189                    &Insert::Value(quote! { ban_expires }),
190                );
191                statements.push(quote! {
192                    if let ::std::option::Option::Some(banned) = update.banned {
193                        #banned
194                        if !banned {
195                            #clear_reason
196                            #clear_expires
197                        }
198                    }
199                    if update.banned != ::std::option::Option::Some(false) {
200                        if let ::std::option::Option::Some(ban_reason) = update.ban_reason { #reason }
201                        if let ::std::option::Option::Some(ban_expires) = update.ban_expires { #expires }
202                    }
203                });
204            } else if has("banned") {
205                statements.push(some("banned", direct));
206            }
207            for name in ["is_anonymous", "phone_number_verified"] {
208                if has(name) {
209                    statements.push(some(name, wrapped));
210                }
211            }
212            for name in ["phone_number", "last_login_method"] {
213                if has(name) {
214                    statements.push(some(name, direct));
215                }
216            }
217        }
218        EntityRole::Account => {
219            for (index, name) in ["access_token", "refresh_token", "id_token"]
220                .into_iter()
221                .enumerate()
222            {
223                let clear = set(&ident(name), &Insert::Null);
224                statements.push(quote! { if update.provider_token_nulls[#index] { #clear } });
225            }
226            for name in [
227                "access_token",
228                "refresh_token",
229                "id_token",
230                "access_token_expires_at",
231                "refresh_token_expires_at",
232                "scope",
233                "password",
234            ] {
235                statements.push(some(name, wrapped));
236            }
237        }
238        EntityRole::Session | EntityRole::Verification => {}
239    }
240    let updated_at = set(&ident("updated_at"), &Insert::Value(quote! { now }));
241    quote! { #(#statements)* #updated_at }
242}
243
244/// Generate an application-selected auth identifier, retaining UUID defaults.
245#[must_use]
246pub fn generated_id(attributes: &AuthAttributes, core_root: &TokenStream) -> TokenStream {
247    attributes.id_generator.as_ref().map_or_else(
248        || quote! { #core_root::uuid::Uuid::new_v4().to_string() },
249        |generator| quote! { #generator() },
250    )
251}