Skip to main content

alibi_entity_codegen/
writes.rs

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