1use crate::spec::{
2 AppliedFederationLink, FederationSpecDefinitions, FederationSpecError, LinkSpecDefinitions,
3 ANY_SCALAR_NAME, ENTITIES_QUERY, ENTITY_UNION_NAME, FEDERATION_V2_DIRECTIVE_NAMES,
4 KEY_DIRECTIVE_NAME, SERVICE_SDL_QUERY, SERVICE_TYPE,
5};
6use apollo_at_link::link::LinkError;
7use apollo_at_link::link::{self, DEFAULT_LINK_NAME};
8use apollo_at_link::spec::Identity;
9use apollo_compiler::ast::{Name, NamedType};
10use apollo_compiler::schema::{ComponentStr, ExtendedType, ObjectType};
11use apollo_compiler::{Node, Schema};
12use indexmap::map::Entry;
13use indexmap::{IndexMap, IndexSet};
14use std::collections::BTreeMap;
15use std::fmt::Formatter;
16use std::sync::Arc;
17
18pub mod database;
19mod spec;
20
21#[derive(Debug)]
26pub struct SubgraphError {
27 pub msg: String,
28}
29
30impl From<apollo_compiler::Diagnostics> for SubgraphError {
31 fn from(value: apollo_compiler::Diagnostics) -> Self {
32 SubgraphError {
33 msg: value.to_string_no_color(),
34 }
35 }
36}
37
38impl From<LinkError> for SubgraphError {
39 fn from(value: LinkError) -> Self {
40 SubgraphError {
41 msg: value.to_string(),
42 }
43 }
44}
45
46impl From<FederationSpecError> for SubgraphError {
47 fn from(value: FederationSpecError) -> Self {
48 SubgraphError {
49 msg: value.to_string(),
50 }
51 }
52}
53
54pub struct Subgraph {
55 pub name: String,
56 pub url: String,
57 pub schema: Schema,
58}
59
60impl Subgraph {
61 pub fn new(name: &str, url: &str, schema_str: &str) -> Self {
62 let schema = Schema::parse(schema_str, name);
63
64 Self {
73 name: name.to_string(),
74 url: url.to_string(),
75 schema,
76 }
77 }
78
79 pub fn parse_and_expand(
80 name: &str,
81 url: &str,
82 schema_str: &str,
83 ) -> Result<Self, SubgraphError> {
84 let mut schema = Schema::builder()
85 .adopt_orphan_extensions()
86 .parse(schema_str, name)
87 .build();
88
89 let mut imported_federation_definitions: Option<FederationSpecDefinitions> = None;
90 let mut imported_link_definitions: Option<LinkSpecDefinitions> = None;
91 let link_directives = schema
92 .schema_definition
93 .directives
94 .get_all(DEFAULT_LINK_NAME);
95
96 for directive in link_directives {
97 let link_directive = link::Link::from_directive_application(directive)?;
98 if link_directive
99 .url
100 .identity
101 .eq(&Identity::federation_identity())
102 {
103 if imported_federation_definitions.is_some() {
104 return Err(SubgraphError { msg: "invalid graphql schema - multiple @link imports for the federation specification are not supported".to_owned() });
105 }
106
107 imported_federation_definitions =
108 Some(FederationSpecDefinitions::from_link(link_directive)?);
109 } else if link_directive.url.identity.eq(&Identity::link_identity()) {
110 if imported_link_definitions.is_some() {
112 return Err(SubgraphError { msg: "invalid graphql schema - multiple @link imports for the link specification are not supported".to_owned() });
113 }
114
115 imported_link_definitions = Some(LinkSpecDefinitions::new(link_directive));
116 }
117 }
118
119 Self::populate_missing_type_definitions(
121 &mut schema,
122 imported_federation_definitions,
123 imported_link_definitions,
124 )?;
125 schema.validate()?;
126 Ok(Self {
127 name: name.to_owned(),
128 url: url.to_owned(),
129 schema,
130 })
131 }
132
133 fn populate_missing_type_definitions(
134 schema: &mut Schema,
135 imported_federation_definitions: Option<FederationSpecDefinitions>,
136 imported_link_definitions: Option<LinkSpecDefinitions>,
137 ) -> Result<(), SubgraphError> {
138 let link_spec_definitions = match imported_link_definitions {
140 Some(definitions) => definitions,
141 None => {
142 let defaults = LinkSpecDefinitions::default();
144 schema
145 .schema_definition
146 .make_mut()
147 .directives
148 .push(defaults.applied_link_directive().into());
149 defaults
150 }
151 };
152 Self::populate_missing_link_definitions(schema, link_spec_definitions)?;
153
154 let fed_definitions = match imported_federation_definitions {
156 Some(definitions) => definitions,
157 None => {
158 let defaults = FederationSpecDefinitions::default()?;
161 schema
162 .schema_definition
163 .make_mut()
164 .directives
165 .push(defaults.applied_link_directive().into());
166 defaults
167 }
168 };
169 Self::populate_missing_federation_directive_definitions(schema, &fed_definitions)?;
170 Self::populate_missing_federation_types(schema, &fed_definitions)
171 }
172
173 fn populate_missing_link_definitions(
174 schema: &mut Schema,
175 link_spec_definitions: LinkSpecDefinitions,
176 ) -> Result<(), SubgraphError> {
177 schema
178 .types
179 .entry(link_spec_definitions.purpose_enum_name.as_str().into())
180 .or_insert_with(|| link_spec_definitions.link_purpose_enum_definition().into());
181 schema
182 .types
183 .entry(link_spec_definitions.import_scalar_name.as_str().into())
184 .or_insert_with(|| link_spec_definitions.import_scalar_definition().into());
185 schema
186 .directive_definitions
187 .entry(DEFAULT_LINK_NAME.into())
188 .or_insert_with(|| link_spec_definitions.link_directive_definition().into());
189 Ok(())
190 }
191
192 fn populate_missing_federation_directive_definitions(
193 schema: &mut Schema,
194 fed_definitions: &FederationSpecDefinitions,
195 ) -> Result<(), SubgraphError> {
196 schema
197 .types
198 .entry(fed_definitions.fieldset_scalar_name.as_str().into())
199 .or_insert_with(|| fed_definitions.fieldset_scalar_definition().into());
200
201 for directive_name in FEDERATION_V2_DIRECTIVE_NAMES {
202 let namespaced_directive_name =
203 fed_definitions.namespaced_type_name(directive_name, true);
204 if let Entry::Vacant(entry) = schema
205 .directive_definitions
206 .entry(namespaced_directive_name.as_str().into())
207 {
208 let directive_definition = fed_definitions.directive_definition(
209 directive_name,
210 &Some(namespaced_directive_name.to_owned()),
211 )?;
212 entry.insert(directive_definition.into());
213 }
214 }
215 Ok(())
216 }
217
218 fn populate_missing_federation_types(
219 schema: &mut Schema,
220 fed_definitions: &FederationSpecDefinitions,
221 ) -> Result<(), SubgraphError> {
222 schema
223 .types
224 .entry(NamedType::new(SERVICE_TYPE))
225 .or_insert_with(|| fed_definitions.service_object_type_definition());
226
227 let entities = Self::locate_entities(schema, fed_definitions);
228 let entities_present = !entities.is_empty();
229 if entities_present {
230 schema
231 .types
232 .entry(NamedType::new(ENTITY_UNION_NAME))
233 .or_insert_with(|| fed_definitions.entity_union_definition(entities));
234 schema
235 .types
236 .entry(NamedType::new(ANY_SCALAR_NAME))
237 .or_insert_with(|| fed_definitions.any_scalar_definition());
238 }
239
240 let query_type_name = schema
241 .schema_definition
242 .make_mut()
243 .query
244 .get_or_insert(ComponentStr::new("Query"));
245 if let ExtendedType::Object(query_type) = schema
246 .types
247 .entry(NamedType::new(query_type_name.as_str()))
248 .or_insert(ExtendedType::Object(Node::new(ObjectType {
249 description: None,
250 directives: Default::default(),
251 fields: IndexMap::new(),
252 implements_interfaces: IndexSet::new(),
253 })))
254 {
255 let query_type = query_type.make_mut();
256 query_type
257 .fields
258 .entry(Name::new(SERVICE_SDL_QUERY))
259 .or_insert_with(|| fed_definitions.service_sdl_query_field());
260 if entities_present {
261 query_type
263 .fields
264 .entry(Name::new(ENTITIES_QUERY))
265 .or_insert_with(|| fed_definitions.entities_query_field());
266 }
267 }
268 Ok(())
269 }
270
271 fn locate_entities(
272 schema: &mut Schema,
273 fed_definitions: &FederationSpecDefinitions,
274 ) -> IndexSet<ComponentStr> {
275 let mut entities = Vec::new();
276 let immutable_type_map = schema.types.to_owned();
277 for (named_type, extended_type) in immutable_type_map.iter() {
278 let is_entity = extended_type
279 .directives()
280 .iter()
281 .find(|d| {
282 d.name.eq(&Name::new(
283 fed_definitions
284 .namespaced_type_name(KEY_DIRECTIVE_NAME, true)
285 .as_str(),
286 ))
287 })
288 .map(|_| true)
289 .unwrap_or(false);
290 if is_entity {
291 entities.push(named_type);
292 }
293 }
294 let entity_set: IndexSet<ComponentStr> = entities
295 .iter()
296 .map(|e| ComponentStr::new(e.as_str()))
297 .collect();
298 entity_set
299 }
300}
301
302impl std::fmt::Debug for Subgraph {
303 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
304 write!(f, r#"name: {}, urL: {}"#, self.name, self.url)
305 }
306}
307
308pub struct Subgraphs {
309 subgraphs: BTreeMap<String, Arc<Subgraph>>,
310}
311
312#[allow(clippy::new_without_default)]
313impl Subgraphs {
314 pub fn new() -> Self {
315 Subgraphs {
316 subgraphs: BTreeMap::new(),
317 }
318 }
319
320 pub fn add(&mut self, subgraph: Subgraph) -> Result<(), SubgraphError> {
321 if self.subgraphs.contains_key(&subgraph.name) {
322 return Err(SubgraphError {
323 msg: format!("A subgraph named {} already exists", subgraph.name),
324 });
325 }
326 self.subgraphs
327 .insert(subgraph.name.clone(), Arc::new(subgraph));
328 Ok(())
329 }
330
331 pub fn get(&self, name: &str) -> Option<Arc<Subgraph>> {
332 self.subgraphs.get(name).cloned()
333 }
334}
335
336#[cfg(test)]
337mod tests {
338 use super::*;
339 use crate::database::keys;
340
341 #[test]
342 fn can_inspect_a_type_key() {
343 let schema = r#"
350 extend schema
351 @link(url: "https://specs.apollo.dev/link/v1.0", import: ["Import"])
352 @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key"])
353
354 type Query {
355 t: T
356 }
357
358 type T @key(fields: "id") {
359 id: ID!
360 x: Int
361 }
362
363 enum link__Purpose {
364 SECURITY
365 EXECUTION
366 }
367
368 scalar Import
369
370 directive @link(url: String, as: String, import: [Import], for: link__Purpose) repeatable on SCHEMA
371 "#;
372
373 let subgraph = Subgraph::new("S1", "http://s1", schema);
374 let keys = keys(&subgraph.schema, "T");
375 assert_eq!(keys.len(), 1);
376 assert_eq!(keys.get(0).unwrap().type_name, "T");
377
378 }
380
381 #[test]
382 fn can_parse_and_expand() -> Result<(), String> {
383 let schema = r#"
384 extend schema
385 @link(url: "https://specs.apollo.dev/federation/v2.3", import: [ "@key" ])
386
387 type Query {
388 t: T
389 }
390
391 type T @key(fields: "id") {
392 id: ID!
393 x: Int
394 }
395 "#;
396
397 let subgraph = Subgraph::parse_and_expand("S1", "http://s1", schema).map_err(|e| {
398 println!("{}", e.msg);
399 String::from("failed to parse and expand the subgraph, see errors above for details")
400 })?;
401 assert!(subgraph.schema.types.contains_key("T"));
402 assert!(subgraph.schema.directive_definitions.contains_key("key"));
403 assert!(subgraph
404 .schema
405 .directive_definitions
406 .contains_key("federation__requires"));
407 Ok(())
408 }
409
410 #[test]
411 fn can_parse_and_expand_with_renames() -> Result<(), String> {
412 let schema = r#"
413 extend schema
414 @link(url: "https://specs.apollo.dev/federation/v2.3", import: [ { name: "@key", as: "@myKey" }, "@provides" ])
415
416 type Query {
417 t: T @provides(fields: "x")
418 }
419
420 type T @myKey(fields: "id") {
421 id: ID!
422 x: Int
423 }
424 "#;
425
426 let subgraph = Subgraph::parse_and_expand("S1", "http://s1", schema).map_err(|e| {
427 println!("{}", e.msg);
428 String::from("failed to parse and expand the subgraph, see errors above for details")
429 })?;
430 assert!(subgraph.schema.directive_definitions.contains_key("myKey"));
431 assert!(subgraph
432 .schema
433 .directive_definitions
434 .contains_key("provides"));
435 Ok(())
436 }
437
438 #[test]
439 fn can_parse_and_expand_with_namespace() -> Result<(), String> {
440 let schema = r#"
441 extend schema
442 @link(url: "https://specs.apollo.dev/federation/v2.3", import: [ "@key" ], as: "fed" )
443
444 type Query {
445 t: T
446 }
447
448 type T @key(fields: "id") {
449 id: ID!
450 x: Int
451 }
452 "#;
453
454 let subgraph = Subgraph::parse_and_expand("S1", "http://s1", schema).map_err(|e| {
455 println!("{}", e.msg);
456 String::from("failed to parse and expand the subgraph, see errors above for details")
457 })?;
458 assert!(subgraph.schema.directive_definitions.contains_key("key"));
459 assert!(subgraph
460 .schema
461 .directive_definitions
462 .contains_key("fed__requires"));
463 Ok(())
464 }
465
466 #[test]
467 fn can_parse_and_expand_preserves_user_definitions() -> Result<(), String> {
468 let schema = r#"
469 extend schema
470 @link(url: "https://specs.apollo.dev/link/v1.0", import: ["Import", "Purpose"])
471 @link(url: "https://specs.apollo.dev/federation/v2.3", import: [ "@key" ])
472
473 type Query {
474 t: T
475 }
476
477 type T @key(fields: "id") {
478 id: ID!
479 x: Int
480 }
481
482 enum Purpose {
483 SECURITY
484 EXECUTION
485 }
486
487 scalar Import
488
489 directive @link(url: String, as: String, import: [Import], for: Purpose) repeatable on SCHEMA
490 "#;
491
492 let subgraph = Subgraph::parse_and_expand("S1", "http://s1", schema).map_err(|e| {
493 println!("{}", e.msg);
494 String::from("failed to parse and expand the subgraph, see errors above for details")
495 })?;
496 assert!(subgraph.schema.types.contains_key("Purpose"));
497 Ok(())
498 }
499
500 #[test]
501 fn can_parse_and_expand_works_with_fed_v1() -> Result<(), String> {
502 let schema = r#"
503 type Query {
504 t: T
505 }
506
507 type T @key(fields: "id") {
508 id: ID!
509 x: Int
510 }
511 "#;
512
513 let subgraph = Subgraph::parse_and_expand("S1", "http://s1", schema).map_err(|e| {
514 println!("{}", e.msg);
515 String::from("failed to parse and expand the subgraph, see errors above for details")
516 })?;
517 assert!(subgraph.schema.types.contains_key("T"));
518 assert!(subgraph.schema.directive_definitions.contains_key("key"));
519 Ok(())
520 }
521
522 #[test]
523 fn can_parse_and_expand_will_fail_when_importing_same_spec_twice() {
524 let schema = r#"
525 extend schema
526 @link(url: "https://specs.apollo.dev/federation/v2.3", import: [ "@key" ] )
527 @link(url: "https://specs.apollo.dev/federation/v2.3", import: [ "@provides" ] )
528
529 type Query {
530 t: T
531 }
532
533 type T @key(fields: "id") {
534 id: ID!
535 x: Int
536 }
537 "#;
538
539 let result = Subgraph::parse_and_expand("S1", "http://s1", schema)
540 .expect_err("importing same specification twice should fail");
541 assert_eq!("invalid graphql schema - multiple @link imports for the federation specification are not supported", result.msg);
542 }
543}