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            for name in [
85                "user_id",
86                "expires_at",
87                "ip_address",
88                "user_agent",
89                "impersonated_by",
90                "active_organization_id",
91                "active_team_id",
92            ] {
93                let field = Ident::new(name, Span::call_site());
94                value(name, quote! { create_session.#field });
95            }
96        }
97        EntityRole::Account => {
98            value("created_at", quote! { now });
99            value("updated_at", quote! { now });
100            for name in [
101                "account_id",
102                "provider_id",
103                "user_id",
104                "access_token",
105                "refresh_token",
106                "id_token",
107                "access_token_expires_at",
108                "refresh_token_expires_at",
109                "scope",
110                "password",
111            ] {
112                let field = Ident::new(name, Span::call_site());
113                value(name, quote! { create_account.#field });
114            }
115        }
116        EntityRole::Verification => {
117            value("created_at", quote! { now });
118            value("updated_at", quote! { now });
119            for name in ["identifier", "value", "expires_at"] {
120                let field = Ident::new(name, Span::call_site());
121                value(name, quote! { verification.#field });
122            }
123        }
124    }
125    fields
126        .named
127        .iter()
128        .filter_map(|field| field.ident.clone())
129        .filter(|ident| ident != "id")
130        .map(|ident| {
131            let insert = values
132                .iter()
133                .find(|(name, _)| ident == name && has(name))
134                .map_or(Insert::Default, |(_, insert)| insert.clone());
135            (ident, insert)
136        })
137        .collect()
138}
139
140/// Renders `field = value` on the active row, where `Insert::Value` is
141/// convertible into the field type with `Into`. `Insert::Default` is never
142/// passed.
143pub type SetField<'a> = &'a dyn Fn(&Ident, &Insert) -> TokenStream;
144
145/// The `apply_update` body of the role over `update` and `now`: every field
146/// present in `update` is written; absent plugin fields are skipped.
147#[must_use]
148pub fn update_statements(role: EntityRole, fields: &FieldsNamed, set: SetField<'_>) -> TokenStream {
149    let has = |name: &str| has_field(fields, name);
150    let ident = |name: &str| Ident::new(name, Span::call_site());
151    // `if let Some(x) = update.x { set(x, <wrap>(x)) }`
152    let some = |name: &str, wrap: fn(&Ident) -> TokenStream| {
153        let field = ident(name);
154        let value = wrap(&field);
155        let assign = set(&field, &Insert::Value(value));
156        quote! { if let ::std::option::Option::Some(#field) = update.#field { #assign } }
157    };
158    let wrapped = |field: &Ident| quote! { ::std::option::Option::Some(#field) };
159    let direct = |field: &Ident| quote! { #field };
160    let mut statements = Vec::new();
161    match role {
162        EntityRole::User => {
163            for name in ["email", "name", "image"] {
164                let wrap = if optional_field(fields, name) {
165                    wrapped
166                } else {
167                    direct
168                };
169                statements.push(some(name, wrap));
170            }
171            statements.push(some("email_verified", direct));
172            for name in ["username", "display_username", "role"] {
173                if has(name) {
174                    statements.push(some(name, wrapped));
175                }
176            }
177            for name in ["two_factor_enabled", "metadata"] {
178                if has(name) {
179                    statements.push(some(name, direct));
180                }
181            }
182            if has("banned") && has("ban_reason") && has("ban_expires") {
183                let banned = set(&ident("banned"), &Insert::Value(quote! { banned }));
184                let clear_reason = set(&ident("ban_reason"), &Insert::Null);
185                let clear_expires = set(&ident("ban_expires"), &Insert::Null);
186                let reason = set(
187                    &ident("ban_reason"),
188                    &Insert::Value(quote! { ::std::option::Option::Some(ban_reason) }),
189                );
190                let expires = set(
191                    &ident("ban_expires"),
192                    &Insert::Value(quote! { ban_expires }),
193                );
194                statements.push(quote! {
195                    if let ::std::option::Option::Some(banned) = update.banned {
196                        #banned
197                        if !banned {
198                            #clear_reason
199                            #clear_expires
200                        }
201                    }
202                    if update.banned != ::std::option::Option::Some(false) {
203                        if let ::std::option::Option::Some(ban_reason) = update.ban_reason { #reason }
204                        if let ::std::option::Option::Some(ban_expires) = update.ban_expires { #expires }
205                    }
206                });
207            } else if has("banned") {
208                statements.push(some("banned", direct));
209            }
210            for name in ["is_anonymous", "phone_number_verified"] {
211                if has(name) {
212                    statements.push(some(name, wrapped));
213                }
214            }
215            for name in ["phone_number", "last_login_method"] {
216                if has(name) {
217                    statements.push(some(name, direct));
218                }
219            }
220        }
221        EntityRole::Account => {
222            for (index, name) in ["access_token", "refresh_token", "id_token"]
223                .into_iter()
224                .enumerate()
225            {
226                let clear = set(&ident(name), &Insert::Null);
227                statements.push(quote! { if update.provider_token_nulls[#index] { #clear } });
228            }
229            for name in [
230                "access_token",
231                "refresh_token",
232                "id_token",
233                "access_token_expires_at",
234                "refresh_token_expires_at",
235                "scope",
236                "password",
237            ] {
238                statements.push(some(name, wrapped));
239            }
240        }
241        EntityRole::Session | EntityRole::Verification => {}
242    }
243    let updated_at = set(&ident("updated_at"), &Insert::Value(quote! { now }));
244    quote! { #(#statements)* #updated_at }
245}
246
247/// Generate an application-selected auth identifier, retaining UUID defaults.
248#[must_use]
249pub fn generated_id(attributes: &AuthAttributes, core_root: &TokenStream) -> TokenStream {
250    attributes.id_generator.as_ref().map_or_else(
251        || quote! { #core_root::uuid::Uuid::new_v4().to_string() },
252        |generator| quote! { #generator() },
253    )
254}