apollo-federation 2.13.1

Apollo Federation
Documentation
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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
use std::collections::HashSet;
use std::sync::Arc;

use apollo_compiler::Name;
use apollo_compiler::ast::Directive;
use apollo_compiler::schema::Component;
use apollo_compiler::ty;

use crate::bail;
use crate::error::FederationError;
use crate::error::MultipleFederationErrors;
use crate::error::SingleFederationError;
use crate::error::suggestion::did_you_mean;
use crate::error::suggestion::suggestion_list;
use crate::link::DEFAULT_LINK_NAME;
use crate::link::Link;
use crate::link::federation_spec_definition::FED_1;
use crate::link::federation_spec_definition::FEDERATION_FIELDS_ARGUMENT_NAME;
use crate::link::federation_spec_definition::FEDERATION_KEY_DIRECTIVE_NAME_IN_SPEC;
use crate::link::federation_spec_definition::FEDERATION_PROVIDES_DIRECTIVE_NAME_IN_SPEC;
use crate::link::federation_spec_definition::FEDERATION_REQUIRES_DIRECTIVE_NAME_IN_SPEC;
use crate::link::federation_spec_definition::FEDERATION_VERSIONS;
use crate::link::federation_spec_definition::FederationSpecDefinition;
use crate::link::federation_spec_definition::fed1_link_imports;
use crate::link::federation_spec_definition::get_federation_spec_definition_from_subgraph;
use crate::link::link_spec_definition::LinkSpecDefinition;
use crate::link::spec::Identity;
use crate::link::spec::Url;
use crate::link::spec_definition::SpecDefinition;
use crate::schema::FederationSchema;
use crate::schema::ValidFederationSchema;
use crate::schema::compute_subgraph_metadata;
use crate::schema::position::DirectiveDefinitionPosition;
use crate::schema::validators::access_control::validate_no_access_control_on_interfaces;
use crate::schema::validators::cache_tag::validate_cache_tag_directives;
use crate::schema::validators::context::validate_context_directives;
use crate::schema::validators::cost::validate_cost_directives;
use crate::schema::validators::external::validate_external_directives;
use crate::schema::validators::from_context::validate_from_context_directives;
use crate::schema::validators::interface_object::validate_interface_object_directives;
use crate::schema::validators::key::validate_key_directives;
use crate::schema::validators::list_size::validate_list_size_directives;
use crate::schema::validators::provides::validate_provides_directives;
use crate::schema::validators::requires::validate_requires_directives;
use crate::schema::validators::shareable::validate_shareable_directives;
use crate::schema::validators::tag::validate_tag_directives;
use crate::subgraph::typestate::has_federation_spec_link;
use crate::supergraph::FEDERATION_ENTITIES_FIELD_NAME;
use crate::supergraph::FEDERATION_SERVICE_FIELD_NAME;

pub(crate) struct FederationBlueprint {}

impl FederationBlueprint {
    pub(crate) fn on_missing_directive_definition(
        schema: &mut FederationSchema,
        directive: &Component<Directive>,
    ) -> Result<Option<DirectiveDefinitionPosition>, FederationError> {
        if directive.name == DEFAULT_LINK_NAME {
            let (alias, imports) =
                LinkSpecDefinition::extract_alias_and_imports_on_missing_link_directive_definition(
                    directive,
                )?;
            LinkSpecDefinition::latest().add_definitions_to_schema(schema, alias, imports)?;
            Ok(schema.get_directive_definition(&directive.name))
        } else {
            Ok(None)
        }
    }

    pub(crate) fn on_directive_definition_and_schema_parsed(
        schema: &mut FederationSchema,
    ) -> Result<(), FederationError> {
        Self::complete_subgraph_schema(schema)
    }

    #[allow(unused)]
    pub(crate) fn ignore_parsed_field(schema: &FederationSchema, field_name: &str) -> bool {
        // Historically, federation 1 has accepted invalid schema, including some where the Query
        // type included the definition of `_entities` (so `_entities(representations: [_Any!]!):
        // [_Entity]!`) but _without_ defining the `_Any` or `_Entity` type. So while we want to be
        // stricter for fed2 (so this kind of really weird case can be fixed), we want fed2 to
        // accept as much fed1 schema as possible.
        //
        // So, to avoid this problem, we ignore the _entities and _service fields if we parse them
        // from a fed1 input schema. Those will be added back anyway (along with the proper types)
        // post-parsing.
        if !(FEDERATION_OPERATION_FIELDS.iter().any(|f| *f == field_name)) {
            return false;
        }
        if let Some(metadata) = &schema.subgraph_metadata {
            !metadata.is_fed_2_schema()
        } else {
            false
        }
    }

    pub(crate) fn on_constructed(schema: &mut FederationSchema) -> Result<(), FederationError> {
        if schema.subgraph_metadata.is_none() {
            schema.subgraph_metadata = compute_subgraph_metadata(schema)?.map(Box::new);
        }
        Ok(())
    }

    #[allow(unused)]
    fn on_added_core_feature(
        schema: &mut FederationSchema,
        feature: &Link,
    ) -> Result<(), FederationError> {
        if feature.url.identity == Identity::federation_identity() {
            FEDERATION_VERSIONS
                .find(&feature.url.version)
                .iter()
                .try_for_each(|spec| spec.add_elements_to_schema(schema))?;
        }
        Ok(())
    }

    pub(crate) fn on_validation(schema: &ValidFederationSchema) -> Result<(), FederationError> {
        let mut error_collector = MultipleFederationErrors { errors: Vec::new() };
        let Some(meta) = schema.subgraph_metadata() else {
            bail!("ValidFederationSchema should contain subgraph metadata");
        };

        // We skip the rest of validation for fed1 schemas because there is a number of validations that is stricter than what fed 1
        // accepted, and some of those issues are fixed by `SchemaUpgrader`. So insofar as any fed 1 schma is ultimately converted
        // to a fed 2 one before composition, then skipping some validation on fed 1 schema is fine.
        if !meta.is_fed_2_schema() {
            return error_collector.into_result();
        }

        let context_map = validate_context_directives(schema, &mut error_collector)?;
        validate_from_context_directives(schema, meta, &context_map, &mut error_collector)?;
        validate_key_directives(schema, meta, &mut error_collector)?;
        validate_provides_directives(schema, meta, &mut error_collector)?;
        validate_requires_directives(schema, meta, &mut error_collector)?;
        validate_external_directives(schema, meta, &mut error_collector)?;
        validate_interface_object_directives(schema, meta, &mut error_collector)?;
        validate_shareable_directives(schema, meta, &mut error_collector)?;
        validate_cost_directives(schema, &mut error_collector)?;
        validate_list_size_directives(schema, &mut error_collector)?;
        validate_tag_directives(schema, &mut error_collector)?;
        validate_cache_tag_directives(schema, &mut error_collector)?;
        validate_no_access_control_on_interfaces(schema, meta, &mut error_collector)?;

        error_collector.into_result()
    }

    // Allows to intercept some apollo-compiler error messages when we can provide additional
    // guidance to users.
    pub(crate) fn on_invalid_graphql_error(
        schema: &FederationSchema,
        message: String,
    ) -> SingleFederationError {
        // PORT_NOTE: The following comment is from the JS version.
        // For now, the main additional guidance we provide is around directives, where we could
        // provide additional help in 2 main ways:
        // - if a directive name is likely misspelled.
        // - for fed 2 schema, if a federation directive is referred under it's "default" naming
        //   but is not properly imported (not enforced in the method but rather in the
        //   `FederationBlueprint`).
        //
        // Note that intercepting/parsing error messages to modify them is never ideal, but
        // pragmatically, it's probably better than rewriting the relevant rules entirely (in that
        // case, our "copied" rule may not benefit any potential apollo-compiler's improvements for
        // instance). And while such parsing is fragile, in that it'll break if the original
        // message change, we have unit tests to surface any such breakage so it's not really a
        // risk.

        let matcher = regex::Regex::new(r#"^Error: cannot find directive `@([^`]+)`"#).unwrap();
        let Some(capture) = matcher.captures(&message) else {
            // return as-is
            return SingleFederationError::InvalidGraphQL { message };
        };
        let Some(matched) = capture.get(1) else {
            // return as-is
            return SingleFederationError::InvalidGraphQL { message };
        };

        let directive_name = matched.as_str();
        let options: Vec<_> = schema
            .get_directive_definitions()
            .map(|d| d.directive_name.to_string())
            .collect();
        let suggestions = suggestion_list(directive_name, options);
        if suggestions.is_empty() {
            return Self::on_unknown_directive_validation_error(schema, directive_name, &message);
        }

        let did_you_mean = did_you_mean(suggestions.iter().map(|s| format!("@{s}")));
        SingleFederationError::InvalidGraphQL {
            message: format!("{message}{did_you_mean}\n"),
        }
    }

    fn on_unknown_directive_validation_error(
        schema: &FederationSchema,
        unknown_directive_name: &str,
        error_message: &str,
    ) -> SingleFederationError {
        let Some(metadata) = &schema.subgraph_metadata else {
            return SingleFederationError::Internal {
                message: "Missing subgraph metadata".to_string(),
            };
        };
        let is_fed2 = metadata.is_fed_2_schema();
        let all_directive_names = all_default_federation_directive_names();
        if all_directive_names.contains(unknown_directive_name) {
            // The directive name is "unknown" but it is a default federation directive name. So it
            // means one of a few things happened:
            //  1. it's a fed1 schema but the directive is fed2 only (only possible case for
            //     fed1 schema).
            //  2. the directive has not been imported at all (so needs to be prefixed for it to
            //     work).
            //  3. the directive has an `import`, but it's been aliased to another name.

            if !is_fed2 {
                // Case #1.
                return SingleFederationError::InvalidGraphQL {
                    message: format!(
                        r#"{error_message} If you meant the "@{unknown_directive_name}" federation 2 directive, note that this schema is a federation 1 schema. To be a federation 2 schema, it needs to @link to the federation specification v2."#
                    ),
                };
            }

            let Ok(Some(name_in_schema)) = metadata
                .federation_spec_definition()
                .directive_name_in_schema(schema, &Name::new_unchecked(unknown_directive_name))
            else {
                return SingleFederationError::Internal {
                    message: format!(
                        "Unexpectedly could not find directive \"@{unknown_directive_name}\" in schema"
                    ),
                };
            };
            let federation_link_name = &metadata.federation_spec_definition().identity().name;
            let federation_prefix = format!("{federation_link_name}__");
            if name_in_schema.starts_with(&federation_prefix) {
                // Case #2. There is no import for that directive.
                return SingleFederationError::InvalidGraphQL {
                    message: format!(
                        r#"{error_message} If you meant the "@{unknown_directive_name}" federation directive, you should use fully-qualified name "@{name_in_schema}" or add "@{unknown_directive_name}" to the \`import\` argument of the @link to the federation specification."#
                    ),
                };
            } else {
                // Case #3. There's an import, but it's renamed.
                return SingleFederationError::InvalidGraphQL {
                    message: format!(
                        r#"{error_message} If you meant the "@{unknown_directive_name}" federation directive, you should use "@{name_in_schema}" as it is imported under that name in the @link to the federation specification of this schema."#
                    ),
                };
            }
        } else if !is_fed2 {
            // We could get here when a fed1 schema tried to use a fed2 directive but misspelled it.
            let suggestions = suggestion_list(
                unknown_directive_name,
                all_directive_names.iter().map(|name| name.to_string()),
            );
            if !suggestions.is_empty() {
                let did_you_mean = did_you_mean(suggestions.iter().map(|s| format!("@{s}")));
                let note = if suggestions.len() == 1 {
                    "it is a federation 2 directive"
                } else {
                    "they are federation 2 directives"
                };
                return SingleFederationError::InvalidGraphQL {
                    message: format!(
                        "{error_message}{did_you_mean} If so, note that {note} but this schema is a federation 1 one. To be a federation 2 schema, it needs to @link to the federation specification v2."
                    ),
                };
            }
            // fall-through
        }
        SingleFederationError::InvalidGraphQL {
            message: error_message.to_string(),
        }
    }

    pub(crate) fn complete_subgraph_schema(
        schema: &mut FederationSchema,
    ) -> Result<(), FederationError> {
        if schema.is_fed_2() {
            // subgraph metadata was already computed which means we have @link with fed spec and its definition
            Self::complete_fed_2_subgraph_schema(schema)
        } else if has_federation_spec_link(schema.schema()) {
            // we have @link with fed spec but we don't have @link directive definition
            LinkSpecDefinition::latest().add_to_schema(schema, None)?;
            Self::complete_fed_2_subgraph_schema(schema)
        } else {
            Self::complete_fed_1_subgraph_schema(schema)
        }
    }

    fn complete_fed_2_subgraph_schema(
        schema: &mut FederationSchema,
    ) -> Result<(), FederationError> {
        let federation_spec = get_federation_spec_definition_from_subgraph(schema)?;
        federation_spec.add_elements_to_schema(schema)?;
        Self::expand_known_features(schema)
    }

    fn complete_fed_1_subgraph_schema(
        schema: &mut FederationSchema,
    ) -> Result<(), FederationError> {
        Self::remove_federation_definitions_broken_in_known_ways(schema)?;
        // fed 1 schema won't have @link so we cannot use FederationSpecDefinition#add_elements_to_schema
        let mut errors = MultipleFederationErrors { errors: vec![] };
        let fed_1_link_spec_definition = LinkSpecDefinition::fed1_latest();
        let fed_1_link = Arc::new(Link {
            url: Url {
                identity: fed_1_link_spec_definition.url().identity.clone(),
                version: fed_1_link_spec_definition.url().version.clone(),
            },
            imports: fed1_link_imports(),
            spec_alias: None,
            purpose: None,
        });
        for type_spec in &FED_1.type_specs() {
            if let Err(err) = type_spec.check_or_add(schema, Some(&fed_1_link)) {
                errors.push(err);
            }
        }

        for directive_spec in &FED_1.directive_specs() {
            if let Err(err) = directive_spec.check_or_add(schema, Some(&fed_1_link)) {
                errors.push(err);
            }
        }

        match errors.errors.as_slice() {
            [] => Self::expand_known_features(schema),
            [error] => Err(FederationError::SingleFederationError(error.clone())),
            _ => Err(FederationError::MultipleFederationErrors(errors)),
        }
    }

    fn remove_federation_definitions_broken_in_known_ways(
        schema: &mut FederationSchema,
    ) -> Result<(), FederationError> {
        // We special case @key, @requires and @provides because we've seen existing user schemas where those
        // have been defined in an invalid way, but in a way that fed1 wasn't rejecting. So for convenience,
        // if we detect one of those case, we just remove the definition and let the code afteward add the
        // proper definition back.
        // Note that, in a perfect world, we'd do this within the `SchemaUpgrader`. But the way the code
        // is organised, this method is called before we reach the `SchemaUpgrader`, and it doesn't seem
        // worth refactoring things drastically for that minor convenience.
        for directive_name in &[
            FEDERATION_KEY_DIRECTIVE_NAME_IN_SPEC,
            FEDERATION_PROVIDES_DIRECTIVE_NAME_IN_SPEC,
            FEDERATION_REQUIRES_DIRECTIVE_NAME_IN_SPEC,
        ] {
            if let Some(pos) = schema.get_directive_definition(directive_name) {
                let directive = pos.get(schema.schema())?;
                // We shouldn't have applications at the time of this writing because `completeSubgraphSchema`, which calls this,
                // is only called:
                // 1. during schema parsing, by `FederationBluePrint.onDirectiveDefinitionAndSchemaParsed`, and that is called
                //   before we process any directive applications.
                // 2. by `setSchemaAsFed2Subgraph`, but as the name imply, this trickles to `completeFed2SubgraphSchema`, not
                //   this one method.
                // In other words, there is currently no way to create a full fed1 schema first, and get that method called
                // second. If that changes (no real reason but...), we'd have to modify this because when we remove the
                // definition to re-add the "correct" version, we'd have to re-attach existing applications (doable but not
                // done). This assert is so we notice it quickly if that ever happens (again, unlikely, because fed1 schema
                // is a backward compatibility thing and there is no reason to expand that too much in the future).
                if !schema
                    .referencers()
                    .get_directive(directive_name)
                    .is_empty()
                {
                    bail!(
                        "Subgraph has applications of @{directive_name} but we are trying to remove the definition."
                    );
                }

                // The patterns we recognize and "correct" (by essentially ignoring the definition) are:
                //  1. if the definition has no arguments at all.
                //  2. if the `fields` argument is declared as nullable.
                //  3. if the `fields` argument type is named "FieldSet" instead of "_FieldSet".
                // All of these correspond to things we've seen in user schemas.
                //
                // To be on the safe side, we check that `fields` is the only argument. That's because
                // fed2 accepts the optional `resolvable` arg for @key, fed1 only ever had one arguemnt.
                // If the user had defined more arguments _and_ provided values for the extra argument,
                // removing the definition would create validation errors that would be hard to understand.
                if directive.arguments.is_empty()
                    || (directive.arguments.len() == 1
                        && directive
                            .argument_by_name(&FEDERATION_FIELDS_ARGUMENT_NAME)
                            .is_some_and(|fields| {
                                *fields.ty == ty!(String)
                                    || *fields.ty == ty!(_FieldSet)
                                    || *fields.ty == ty!(FieldSet)
                            }))
                {
                    pos.remove(schema)?;
                }
            }
        }
        Ok(())
    }

    fn expand_known_features(schema: &mut FederationSchema) -> Result<(), FederationError> {
        for feature in schema.all_features()? {
            feature.add_elements_to_schema(schema)?;
        }

        Ok(())
    }

    #[allow(unused)]
    fn apply_directives_after_parsing() -> bool {
        true
    }
}

pub(crate) const FEDERATION_OPERATION_FIELDS: [Name; 2] = [
    FEDERATION_SERVICE_FIELD_NAME,
    FEDERATION_ENTITIES_FIELD_NAME,
];

fn all_default_federation_directive_names() -> HashSet<Name> {
    FederationSpecDefinition::latest()
        .directive_specs()
        .iter()
        .map(|spec| spec.name().clone())
        .collect()
}