Skip to main content

cratestack_sql/
descriptor.rs

1use std::fmt::Write;
2use std::marker::PhantomData;
3
4use cratestack_core::ModelEventKind;
5use cratestack_policy::ReadPolicy;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub struct ModelColumn {
9    pub rust_name: &'static str,
10    pub sql_name: &'static str,
11}
12
13#[derive(Debug, Clone, Copy)]
14pub struct ModelDescriptor<M, PK> {
15    pub schema_name: &'static str,
16    pub table_name: &'static str,
17    pub columns: &'static [ModelColumn],
18    pub primary_key: &'static str,
19    pub allowed_fields: &'static [&'static str],
20    pub allowed_includes: &'static [&'static str],
21    pub allowed_sorts: &'static [&'static str],
22    pub read_allow_policies: &'static [ReadPolicy],
23    pub read_deny_policies: &'static [ReadPolicy],
24    pub detail_allow_policies: &'static [ReadPolicy],
25    pub detail_deny_policies: &'static [ReadPolicy],
26    pub create_allow_policies: &'static [ReadPolicy],
27    pub create_deny_policies: &'static [ReadPolicy],
28    pub update_allow_policies: &'static [ReadPolicy],
29    pub update_deny_policies: &'static [ReadPolicy],
30    pub delete_allow_policies: &'static [ReadPolicy],
31    pub delete_deny_policies: &'static [ReadPolicy],
32    pub create_defaults: &'static [CreateDefault],
33    pub emitted_events: &'static [ModelEventKind],
34    /// Column name of the optimistic-locking version field, set when the
35    /// model declares an `@version` field. `None` for non-versioned models,
36    /// which keeps update semantics unchanged.
37    pub version_column: Option<&'static str>,
38    /// `true` when the model declared `@@audit`. Mutations on audit-enabled
39    /// models capture before/after snapshots and persist them into
40    /// `cratestack_audit` inside the same transaction.
41    pub audit_enabled: bool,
42    /// SQL column names of fields declared `@pii`. The audit-log writer
43    /// replaces these values with `"[redacted-pii]"` in the persisted JSON
44    /// snapshots; a follow-up will extend the same redaction to error
45    /// detail and tracing.
46    pub pii_columns: &'static [&'static str],
47    /// SQL column names of fields declared `@sensitive`. Redacted in audit
48    /// snapshots as `"[redacted-sensitive]"`.
49    pub sensitive_columns: &'static [&'static str],
50    /// Column name for the soft-delete timestamp. When `Some`, DELETE
51    /// operations become UPDATE-of-`deleted_at` and every SELECT through
52    /// `push_scoped_conditions` filters out rows where the column is
53    /// non-null. Defaults to `Some("deleted_at")` when `@@soft_delete` is
54    /// declared.
55    pub soft_delete_column: Option<&'static str>,
56    /// Retention window in days for soft-deleted rows. The runtime does
57    /// not auto-GC; banks run their own scheduled job that deletes rows
58    /// where `deleted_at < NOW() - retention`. Surfaced here so the GC
59    /// can read the policy from one place.
60    pub retention_days: Option<u32>,
61    /// Columns the upsert primitive is allowed to overwrite on conflict.
62    /// Populated by the macro to be every scalar column *except* the
63    /// primary key, `created_at`, and the `@version` column. Empty when
64    /// the model has no eligible columns (e.g. PK-only); in that case
65    /// the macro doesn't emit an `UpsertModelInput` impl either, so this
66    /// is just a belt-and-braces.
67    pub upsert_update_columns: &'static [&'static str],
68    _marker: PhantomData<fn() -> (M, PK)>,
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub enum CreateDefaultType {
73    Bool,
74    Int,
75    String,
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub struct CreateDefault {
80    pub column: &'static str,
81    pub auth_field: &'static str,
82    pub ty: CreateDefaultType,
83    pub nullable: bool,
84}
85
86impl<M, PK> ModelDescriptor<M, PK> {
87    pub const fn new(
88        schema_name: &'static str,
89        table_name: &'static str,
90        columns: &'static [ModelColumn],
91        primary_key: &'static str,
92        allowed_fields: &'static [&'static str],
93        allowed_includes: &'static [&'static str],
94        allowed_sorts: &'static [&'static str],
95        read_allow_policies: &'static [ReadPolicy],
96        read_deny_policies: &'static [ReadPolicy],
97        detail_allow_policies: &'static [ReadPolicy],
98        detail_deny_policies: &'static [ReadPolicy],
99        create_allow_policies: &'static [ReadPolicy],
100        create_deny_policies: &'static [ReadPolicy],
101        update_allow_policies: &'static [ReadPolicy],
102        update_deny_policies: &'static [ReadPolicy],
103        delete_allow_policies: &'static [ReadPolicy],
104        delete_deny_policies: &'static [ReadPolicy],
105        create_defaults: &'static [CreateDefault],
106        emitted_events: &'static [ModelEventKind],
107        version_column: Option<&'static str>,
108        audit_enabled: bool,
109        pii_columns: &'static [&'static str],
110        sensitive_columns: &'static [&'static str],
111        soft_delete_column: Option<&'static str>,
112        retention_days: Option<u32>,
113        upsert_update_columns: &'static [&'static str],
114    ) -> Self {
115        Self {
116            schema_name,
117            table_name,
118            columns,
119            primary_key,
120            allowed_fields,
121            allowed_includes,
122            allowed_sorts,
123            read_allow_policies,
124            read_deny_policies,
125            detail_allow_policies,
126            detail_deny_policies,
127            create_allow_policies,
128            create_deny_policies,
129            update_allow_policies,
130            update_deny_policies,
131            delete_allow_policies,
132            delete_deny_policies,
133            create_defaults,
134            emitted_events,
135            version_column,
136            audit_enabled,
137            pii_columns,
138            sensitive_columns,
139            soft_delete_column,
140            retention_days,
141            upsert_update_columns,
142            _marker: PhantomData,
143        }
144    }
145
146    pub fn emits(&self, operation: ModelEventKind) -> bool {
147        self.emitted_events.contains(&operation)
148    }
149
150    pub fn select_projection(&self) -> String {
151        let mut sql = String::new();
152        for (index, column) in self.columns.iter().enumerate() {
153            if index > 0 {
154                sql.push_str(", ");
155            }
156            let _ = write!(sql, "{} AS \"{}\"", column.sql_name, column.rust_name);
157        }
158        sql
159    }
160}