backbone_core/workflow/
schema.rs1pub const STATUSES: &[&str] = &[
8 "running",
9 "waiting",
10 "completed",
11 "failed",
12 "expired",
13 "compensating",
14 "compensated",
15 "cancelled",
16];
17
18pub fn valid_schema(schema: &str) -> bool {
20 let mut chars = schema.chars();
21 matches!(chars.next(), Some(c) if c.is_ascii_lowercase() || c == '_')
22 && chars.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
23 && schema.len() <= 48
24}
25
26pub fn migration_sql(schema: &str) -> String {
35 assert!(
36 valid_schema(schema),
37 "`{schema}` is not a plain schema name"
38 );
39 let statuses = STATUSES
40 .iter()
41 .map(|s| format!("'{s}'"))
42 .collect::<Vec<_>>()
43 .join(", ");
44 format!(
45 r#"CREATE SCHEMA IF NOT EXISTS {schema};
46
47CREATE TABLE IF NOT EXISTS {schema}.bb_workflow_instances (
48 id uuid PRIMARY KEY,
49 workflow text NOT NULL,
50 version int NOT NULL,
51 company_id uuid NOT NULL,
52 org_unit_id uuid,
53 status text NOT NULL CHECK (status IN ({statuses})),
54 current_step text,
55 context jsonb NOT NULL DEFAULT '{{}}'::jsonb,
56 trigger_event_id uuid NOT NULL,
57 correlation_id text,
58 deadline_at timestamptz,
59 wait_event text,
60 wait_key text,
61 wait_due_at timestamptz,
62 attempts int NOT NULL DEFAULT 0,
63 last_error text,
64 created_at timestamptz NOT NULL DEFAULT now(),
65 updated_at timestamptz NOT NULL DEFAULT now(),
66 CONSTRAINT uq_bb_workflow_instances_trigger UNIQUE (workflow, trigger_event_id)
67);
68
69CREATE INDEX IF NOT EXISTS idx_bb_workflow_instances_due
70 ON {schema}.bb_workflow_instances (wait_due_at)
71 WHERE status IN ('waiting', 'running') AND wait_due_at IS NOT NULL;
72CREATE INDEX IF NOT EXISTS idx_bb_workflow_instances_wait
73 ON {schema}.bb_workflow_instances (wait_event, wait_key)
74 WHERE status = 'waiting';
75CREATE INDEX IF NOT EXISTS idx_bb_workflow_instances_unfinished
76 ON {schema}.bb_workflow_instances (status, updated_at)
77 WHERE status IN ('running', 'compensating');
78
79CREATE TABLE IF NOT EXISTS {schema}.bb_workflow_steps (
80 instance_id uuid NOT NULL REFERENCES {schema}.bb_workflow_instances (id) ON DELETE CASCADE,
81 company_id uuid NOT NULL,
82 org_unit_id uuid,
83 seq int NOT NULL,
84 step text NOT NULL,
85 outcome text NOT NULL,
86 error text,
87 started_at timestamptz NOT NULL DEFAULT now(),
88 finished_at timestamptz NOT NULL DEFAULT now(),
89 PRIMARY KEY (instance_id, seq)
90);
91
92ALTER TABLE {schema}.bb_workflow_instances ENABLE ROW LEVEL SECURITY;
93ALTER TABLE {schema}.bb_workflow_instances FORCE ROW LEVEL SECURITY;
94ALTER TABLE {schema}.bb_workflow_steps ENABLE ROW LEVEL SECURITY;
95ALTER TABLE {schema}.bb_workflow_steps FORCE ROW LEVEL SECURITY;
96{policies}"#,
97 policies = policies_sql(schema)
98 )
99}
100
101fn policies_sql(schema: &str) -> String {
102 format!(
106 r#"CREATE POLICY bb_workflow_instances_company_isolation ON {schema}.bb_workflow_instances FOR ALL
107 USING (company_id = NULLIF(current_setting('app.company_id', true), '')::uuid
108 OR current_setting('app.workflow_sweep', true) = 'on')
109 WITH CHECK (company_id = NULLIF(current_setting('app.company_id', true), '')::uuid);
110CREATE POLICY bb_workflow_steps_company_isolation ON {schema}.bb_workflow_steps FOR ALL
111 USING (company_id = NULLIF(current_setting('app.company_id', true), '')::uuid)
112 WITH CHECK (company_id = NULLIF(current_setting('app.company_id', true), '')::uuid);
113"#
114 )
115}
116
117pub fn org_fence_sql(schema: &str) -> String {
126 assert!(
127 valid_schema(schema),
128 "`{schema}` is not a plain schema name"
129 );
130 let mut out = String::new();
131 for table in ["bb_workflow_instances", "bb_workflow_steps"] {
132 let sweep = if table == "bb_workflow_instances" {
133 "\n OR current_setting('app.workflow_sweep', true) = 'on'"
134 } else {
135 ""
136 };
137 out.push_str(&format!(
138 r#"DROP POLICY IF EXISTS {table}_company_isolation ON {schema}.{table};
139DROP POLICY IF EXISTS {table}_org_unit_isolation ON {schema}.{table};
140CREATE POLICY {table}_org_unit_isolation ON {schema}.{table} FOR ALL
141 USING (org_unit_id = ANY(string_to_array(current_setting('app.scope_unit_ids', true), ',')::uuid[]){sweep})
142 WITH CHECK (org_unit_id = ANY(string_to_array(current_setting('app.scope_unit_ids', true), ',')::uuid[]));
143CREATE OR REPLACE FUNCTION {schema}.{table}_org_unit_fill() RETURNS trigger AS $$
144BEGIN
145 IF NEW.org_unit_id IS NULL THEN
146 NEW.org_unit_id := nullif(current_setting('app.acting_unit_id', true), '')::uuid;
147 END IF;
148 RETURN NEW;
149END;
150$$ LANGUAGE plpgsql;
151DROP TRIGGER IF EXISTS {table}_org_unit_fill ON {schema}.{table};
152CREATE TRIGGER {table}_org_unit_fill BEFORE INSERT ON {schema}.{table}
153 FOR EACH ROW EXECUTE FUNCTION {schema}.{table}_org_unit_fill();
154"#
155 ));
156 }
157 out
158}
159
160pub fn drop_and_create_policies(schema: &str) -> String {
162 assert!(
163 valid_schema(schema),
164 "`{schema}` is not a plain schema name"
165 );
166 format!(
167 "DROP POLICY IF EXISTS bb_workflow_instances_company_isolation ON {schema}.bb_workflow_instances;\n\
168 DROP POLICY IF EXISTS bb_workflow_steps_company_isolation ON {schema}.bb_workflow_steps;\n{}",
169 policies_sql(schema)
170 )
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176
177 #[test]
178 fn the_migration_names_the_tables_the_fence_and_the_dedup_table() {
179 let sql = migration_sql("hr");
180 for needle in [
181 "hr.bb_workflow_instances",
182 "hr.bb_workflow_steps",
183 "UNIQUE (workflow, trigger_event_id)",
184 "FORCE ROW LEVEL SECURITY",
185 "'compensating'",
186 ] {
187 assert!(sql.contains(needle), "{needle}");
188 }
189 }
190
191 #[test]
192 fn a_schema_that_is_not_a_plain_identifier_is_refused() {
193 assert!(valid_schema("selling_2"));
194 for bad in ["", "Hr", "hr; drop", "9x", "a-b"] {
195 assert!(!valid_schema(bad), "{bad}");
196 }
197 }
198}