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
//! GraphQL subscriptions.
//!
//! A subscription is served as a **graphql-sse** event stream (see
//! [`crate::stream::serve_graphql_subscription`]): the subscription's root field names a
//! messaging topic; a mutation (or any producer) publishes an execution result to that
//! topic, and each is framed as a graphql-sse `next` event so a standard graphql-sse
//! client consumes it directly. This module only needs to *detect* a subscription
//! operation and *derive its topic* — the transport (subscribe, `Last-Event-ID` resume,
//! heartbeat, connection caps, graphql-sse framing) is the `stream` module's job.
//!
//! boatramp stays GraphQL-*aware*, not an engine: the payload published to the topic is
//! the subscription result your producer computes; the host just fans it out (framed).
use async_graphql_parser::types::{
DocumentOperations, OperationDefinition, OperationType, Selection,
};
use async_graphql_parser::Positioned;
/// If `query` is a subscription operation, return the messaging topic it streams from —
/// its single root field's name (a subscription has exactly one root field per the
/// GraphQL spec). Returns `None` for a query/mutation or a malformed subscription.
pub(crate) fn subscription_topic(query: &str) -> Option<String> {
let doc = async_graphql_parser::parse_query(query).ok()?;
let ops: Vec<&OperationDefinition> = match &doc.operations {
DocumentOperations::Single(op) => vec![&op.node],
DocumentOperations::Multiple(map) => map.values().map(|op| &op.node).collect(),
};
ops.into_iter().find_map(|op| {
if op.ty != OperationType::Subscription {
return None;
}
match op.selection_set.node.items.first() {
Some(Positioned {
node: Selection::Field(field),
..
}) => Some(field.node.name.node.to_string()),
_ => None,
}
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_subscription_yields_its_root_field_as_the_topic() {
assert_eq!(
subscription_topic("subscription { messageAdded { id body } }"),
Some("messageAdded".to_string())
);
// A named subscription works too.
assert_eq!(
subscription_topic("subscription Live { ticks }"),
Some("ticks".to_string())
);
}
#[test]
fn a_query_or_mutation_is_not_a_subscription() {
assert_eq!(subscription_topic("{ me { name } }"), None);
assert_eq!(subscription_topic("mutation { post(x: 1) }"), None);
assert_eq!(subscription_topic("query Q { a }"), None);
}
#[test]
fn a_malformed_query_is_not_a_subscription() {
assert_eq!(subscription_topic("subscription { "), None);
}
}