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    _marker: PhantomData<fn() -> (M, PK)>,
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub enum CreateDefaultType {
66    Bool,
67    Int,
68    String,
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub struct CreateDefault {
73    pub column: &'static str,
74    pub auth_field: &'static str,
75    pub ty: CreateDefaultType,
76    pub nullable: bool,
77}
78
79impl<M, PK> ModelDescriptor<M, PK> {
80    pub const fn new(
81        schema_name: &'static str,
82        table_name: &'static str,
83        columns: &'static [ModelColumn],
84        primary_key: &'static str,
85        allowed_fields: &'static [&'static str],
86        allowed_includes: &'static [&'static str],
87        allowed_sorts: &'static [&'static str],
88        read_allow_policies: &'static [ReadPolicy],
89        read_deny_policies: &'static [ReadPolicy],
90        detail_allow_policies: &'static [ReadPolicy],
91        detail_deny_policies: &'static [ReadPolicy],
92        create_allow_policies: &'static [ReadPolicy],
93        create_deny_policies: &'static [ReadPolicy],
94        update_allow_policies: &'static [ReadPolicy],
95        update_deny_policies: &'static [ReadPolicy],
96        delete_allow_policies: &'static [ReadPolicy],
97        delete_deny_policies: &'static [ReadPolicy],
98        create_defaults: &'static [CreateDefault],
99        emitted_events: &'static [ModelEventKind],
100        version_column: Option<&'static str>,
101        audit_enabled: bool,
102        pii_columns: &'static [&'static str],
103        sensitive_columns: &'static [&'static str],
104        soft_delete_column: Option<&'static str>,
105        retention_days: Option<u32>,
106    ) -> Self {
107        Self {
108            schema_name,
109            table_name,
110            columns,
111            primary_key,
112            allowed_fields,
113            allowed_includes,
114            allowed_sorts,
115            read_allow_policies,
116            read_deny_policies,
117            detail_allow_policies,
118            detail_deny_policies,
119            create_allow_policies,
120            create_deny_policies,
121            update_allow_policies,
122            update_deny_policies,
123            delete_allow_policies,
124            delete_deny_policies,
125            create_defaults,
126            emitted_events,
127            version_column,
128            audit_enabled,
129            pii_columns,
130            sensitive_columns,
131            soft_delete_column,
132            retention_days,
133            _marker: PhantomData,
134        }
135    }
136
137    pub fn emits(&self, operation: ModelEventKind) -> bool {
138        self.emitted_events.contains(&operation)
139    }
140
141    pub fn select_projection(&self) -> String {
142        let mut sql = String::new();
143        for (index, column) in self.columns.iter().enumerate() {
144            if index > 0 {
145                sql.push_str(", ");
146            }
147            let _ = write!(sql, "{} AS \"{}\"", column.sql_name, column.rust_name);
148        }
149        sql
150    }
151}