1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
//! The engine's notice that a mutation committed (#1340, #1440).
//!
//! `after:mutation` functions run once a write has committed. Deciding *that* a write
//! committed, and *what* it wrote, is the engine's knowledge: it holds the parsed outcome
//! of the mutation function. A transport holds only the response it is about to serve.
//!
//! Dispatch used to live in the transports, reading the response, and that placement was
//! wrong three ways:
//!
//! - **Only two transports dispatched.** GraphQL and REST did; MCP, gRPC and the functions bridge
//! commit through the same executor and fired nothing (#1440).
//! - **A failure looked like a success.** Under `auto_error_union` a failed write is *data*: an
//! error member of the union, not a GraphQL error. A dispatcher reading the response could not
//! tell the two apart unless the client happened to select `__typename` (#1340).
//! - **The row was the client's selection.** The event's row image was whatever fields the client
//! selected, so a `when` predicate on an unselected field never matched.
//!
//! The executor therefore calls an [`AfterMutationObserver`] from the mutation runner, the
//! single chokepoint every transport converges on. It does so once per committed success,
//! with the type the write produced and the full entity the function returned. A failed
//! write, a dry run and a refused write never reach it. Like
//! [`BeforeMutationGate`](crate::security::BeforeMutationGate), the observer is a trait
//! object the application supplies, because `fraiseql-core` knows nothing about function
//! runtimes. `fraiseql-server` installs one that dispatches into the compiled schema's
//! `functions` section.
use Future;
use crate::;
/// A mutation that committed successfully, as the engine saw it.
/// Receives every committed mutation, from every transport.
///
/// Called on the request path, after the commit and before the response is returned, so an
/// implementation must not block: hand the work to a task and return. Whatever it does
/// cannot change the outcome of a write that has already committed, which is why the method
/// returns nothing.
///
/// # Example
///
/// ```
/// use std::sync::Arc;
///
/// use fraiseql_core::runtime::{AfterMutationObserver, CommittedMutation, RuntimeConfig};
///
/// /// Log every committed write.
/// struct AuditLog;
///
/// impl AfterMutationObserver for AuditLog {
/// fn on_committed(&self, mutation: &CommittedMutation<'_>) {
/// tracing::info!(mutation = mutation.mutation_name, entity = mutation.entity_type);
/// }
/// }
///
/// let config = RuntimeConfig::default().with_after_mutation_observer(Arc::new(AuditLog));
/// ```
task_local!
/// Run `future` as the work of a function dispatched `depth` levels deep, so every write it
/// makes is observed with that [`CommittedMutation::dispatch_depth`].
///
/// A dispatched function's writes pass the same chokepoint as a request's, so an
/// `after:mutation` function that writes the entity it is triggered by would trigger itself
/// without end. The depth is what lets the observer stop such a chain. It is carried in a
/// task-local rather than on the [`SecurityContext`] because a client controls neither, and
/// cannot set this one: only code that wraps its own executor calls in this scope can.
pub async
/// The dispatch depth of the current task: `0` outside any [`dispatched_at`] scope.