diff --git a/packages/fuel-indexer-lib/src/graphql/parser.rs b/packages/fuel-indexer-lib/src/graphql/parser.rs index 0b2abb357..7956a26e7 100644 --- a/packages/fuel-indexer-lib/src/graphql/parser.rs +++ b/packages/fuel-indexer-lib/src/graphql/parser.rs @@ -600,6 +600,8 @@ impl SchemaDecoder { node: TypeDefinition, u: &UnionType, ) { + GraphQLSchemaValidator::check_disallowed_graphql_typedef_name(&union_name); + self.parsed_graphql_schema .parsed_typedef_names .insert(union_name.clone()); @@ -611,7 +613,7 @@ impl SchemaDecoder { .union_names .insert(union_name.clone()); - GraphQLSchemaValidator::check_derived_union_is_well_formed( + GraphQLSchemaValidator::check_derived_union_virtuality_is_well_formed( &node, &mut self.parsed_graphql_schema.virtual_type_names, ); @@ -625,6 +627,8 @@ impl SchemaDecoder { // count the distinct fields across all members of the union. let mut child_position = 0; + let mut union_member_field_types = HashMap::new(); + u.members.iter().for_each(|m| { let member_name = m.node.to_string(); if let Some(name) = self @@ -661,11 +665,22 @@ impl SchemaDecoder { let ftype = field_type_name(&f.node); let field_id = field_id(&union_name, &f.node.name.to_string()); + union_member_field_types + .entry(field_id.clone()) + .or_insert(HashSet::new()) + .insert(ftype.clone()); + + GraphQLSchemaValidator::derived_field_type_is_consistent( + &union_name, + &f.node.name.to_string(), + union_member_field_types.get(&field_id).unwrap(), + ); + if processed_fields.contains(&field_id) { return; } - processed_fields.insert(field_id); + processed_fields.insert(field_id.clone()); // Manual foreign key check, same as above if self @@ -747,6 +762,8 @@ impl SchemaDecoder { node: TypeDefinition, o: &ObjectType, ) { + GraphQLSchemaValidator::check_disallowed_graphql_typedef_name(&obj_name); + // Only parse `TypeDefinition`s with the `@entity` directive. let is_entity = node .directives @@ -754,7 +771,7 @@ impl SchemaDecoder { .any(|d| d.node.name.to_string() == "entity"); if !is_entity { - //continue; + println!("Skipping TypeDefinition '{obj_name}', which is not marked with an @entity directive."); return; } self.parsed_graphql_schema @@ -764,18 +781,44 @@ impl SchemaDecoder { .parsed_typedef_names .insert(obj_name.clone()); + let is_virtual = node + .directives + .iter() + .flat_map(|d| d.node.arguments.clone()) + .any(|t| t.0.node == "virtual"); + + if is_virtual { + self.parsed_graphql_schema + .virtual_type_names + .insert(obj_name.clone()); + + GraphQLSchemaValidator::virtual_type_has_no_id_field(o, &obj_name); + } + + // Since we have to use this manual `is_list_type` for each field, we might as well + // keep track of how many m2m fields we have for this object here. We could also move this + // logic to the `GraphQLSchemaValidator` itself, but that means we'd have to copy over the + // `is_list_type` logic there as well. + let mut m2m_field_count = 0; + let mut field_mapping = BTreeMap::new(); for (i, field) in o.fields.iter().enumerate() { + GraphQLSchemaValidator::id_field_is_type_id(&field.node, &obj_name); + let field_name = field.node.name.to_string(); let field_typ_name = field.node.ty.to_string(); let fid = field_id(&obj_name, &field_name); + GraphQLSchemaValidator::ensure_fielddef_is_not_nested_list(&field.node); + self.parsed_graphql_schema .object_ordered_fields .entry(obj_name.clone()) .or_default() .push(OrderedField(field.node.clone(), i)); + // We need to add these field type names to `GraphQLSchemaValidator::list_field_types` prior to + // doing the foreign key check below, (since we need to know whether a field is a FK type) if is_list_type(&field.node) { self.parsed_graphql_schema .list_field_types @@ -786,19 +829,7 @@ impl SchemaDecoder { .insert(obj_name.clone(), node.clone()); } - let is_virtual = node - .directives - .iter() - .flat_map(|d| d.node.arguments.clone()) - .any(|t| t.0.node == "virtual"); - - if is_virtual { - self.parsed_graphql_schema - .virtual_type_names - .insert(obj_name.clone()); - } - - // Manual version of `ParsedGraphQLSchema::is_possible_foreign_key` + // Manual foreign key check let ftype = field_type_name(&field.node); if self .parsed_graphql_schema @@ -811,12 +842,31 @@ impl SchemaDecoder { .virtual_type_names .contains(&ftype) { - let (_ref_coltype, ref_colname, ref_tablename) = extract_foreign_key_info( + GraphQLSchemaValidator::foreign_key_field_contains_no_unique_directive( + &field.node, + &obj_name, + ); + + let (ref_coltype, ref_colname, ref_tablename) = extract_foreign_key_info( &field.node, &self.parsed_graphql_schema.field_type_mappings, ); if is_list_type(&field.node) { + GraphQLSchemaValidator::m2m_fk_field_ref_col_is_id( + &field.node, + &obj_name, + &ref_coltype, + &ref_colname, + ); + + m2m_field_count += 1; + + GraphQLSchemaValidator::verify_m2m_relationship_count( + &obj_name, + m2m_field_count, + ); + self.parsed_graphql_schema .join_table_meta .entry(obj_name.clone()) @@ -997,4 +1047,296 @@ union Storage = Safe | Vault JoinTableMeta::new("storage", "id", "user", "id", Some(3)) ); } + + /* Schema validation tests */ + #[test] + #[should_panic(expected = "TypeDefinition name 'TransactionData' is reserved.")] + fn test_schema_validator_check_disallowed_graphql_typedef_name() { + let schema = r#" +type Foo @entity { + id: ID! +} + +type TransactionData @entity { + id: ID! +} +"#; + + let _ = ParsedGraphQLSchema::new( + "test", + "test", + ExecutionSource::Wasm, + Some(&GraphQLSchema::new(schema.to_string())), + ) + .unwrap(); + } + + #[test] + #[should_panic( + expected = "TypeDefinition(Union(Baz)) does not have consistent virtual/non-virtual members." + )] + fn test_schema_validator_check_derived_union_virtuality_is_well_formed() { + let schema = r#" +type Foo @entity { + id: ID! + name: Charfield! +} + +type Bar @entity { + id: ID! + age: UInt8! +} + +type Zoo @entity(virtual: true) { + height: UInt8! +} + +union Baz = Foo | Bar | Zoo +"#; + + let _ = ParsedGraphQLSchema::new( + "test", + "test", + ExecutionSource::Wasm, + Some(&GraphQLSchema::new(schema.to_string())), + ) + .unwrap(); + } + + #[test] + #[should_panic( + expected = "Derived type from Union(Baz) contains Field(name) which does not have a consistent type across all members." + )] + fn test_schema_validator_derived_field_type_is_consistent() { + let schema = r#" +type Foo @entity { + id: ID! + name: Charfield! +} + +type Bar @entity { + id: ID! + age: UInt8! +} + +type Zoo @entity { + id: ID! + name: UInt8! +} + +union Baz = Foo | Bar | Zoo +"#; + + let _ = ParsedGraphQLSchema::new( + "test", + "test", + ExecutionSource::Wasm, + Some(&GraphQLSchema::new(schema.to_string())), + ) + .unwrap(); + } + + #[test] + #[should_panic( + expected = "FieldDefinition(nested) is a nested list, which is not supported." + )] + fn test_schema_validator_ensure_fielddef_is_not_nested_list() { + let schema = r#" +type Foo @entity { + id: ID! + name: Charfield! +} + +type Zoo @entity { + id: ID! + nested: [[Foo!]!]! +} +"#; + + let _ = ParsedGraphQLSchema::new( + "test", + "test", + ExecutionSource::Wasm, + Some(&GraphQLSchema::new(schema.to_string())), + ) + .unwrap(); + } + + #[test] + #[should_panic( + expected = "FieldDefinition(id) on TypeDefinition(Foo) must be of type `ID!`. Found type `Charfield!`." + )] + fn test_schema_validator_id_field_is_type_id() { + let schema = r#" +type Foo @entity { + id: Charfield! + name: Charfield! +}"#; + + let _ = ParsedGraphQLSchema::new( + "test", + "test", + ExecutionSource::Wasm, + Some(&GraphQLSchema::new(schema.to_string())), + ) + .unwrap(); + } + + #[test] + #[should_panic( + expected = "Virtual TypeDefinition(Foo) cannot contain an `id: ID!` FieldDefinition." + )] + fn test_schema_validator_virtual_type_has_no_id_field() { + let schema = r#" +type Foo @entity(virtual: true) { + id: ID! + name: Charfield! +}"#; + + let _ = ParsedGraphQLSchema::new( + "test", + "test", + ExecutionSource::Wasm, + Some(&GraphQLSchema::new(schema.to_string())), + ) + .unwrap(); + } + + #[test] + #[should_panic( + expected = "FieldDefinition(id) on TypeDefinition(Bar) must be of type `ID!`. Found type `ID`." + )] + fn test_schema_validator_foreign_key_field_contains_no_unique_directive() { + let schema = r#" +type Foo @entity { + id: ID! + name: Charfield! +} + +type Bar @entity { + id: ID + foo: Foo! @unique +} +"#; + + let _ = ParsedGraphQLSchema::new( + "test", + "test", + ExecutionSource::Wasm, + Some(&GraphQLSchema::new(schema.to_string())), + ) + .unwrap(); + } + + #[test] + #[should_panic( + expected = "TypeDefinition(Bar) exceeds the allowed number of many-to-many` relationships. The maximum allowed is 10." + )] + fn test_schema_validator_verify_m2m_relationship_count() { + let schema = r#" +type Type1 @entity { + id: ID! + name: Charfield! +} + +type Type2 @entity { + id: ID! + name: Charfield! +} + +type Type3 @entity { + id: ID! + name: Charfield! +} + +type Type4 @entity { + id: ID! + name: Charfield! +} + +type Type5 @entity { + id: ID! + name: Charfield! +} + +type Type6 @entity { + id: ID! + name: Charfield! +} + +type Type7 @entity { + id: ID! + name: Charfield! +} + +type Type8 @entity { + id: ID! + name: Charfield! +} + +type Type9 @entity { + id: ID! + name: Charfield! +} + +type Type10 @entity { + id: ID! + name: Charfield! +} + +type Type11 @entity { + id: ID! + name: Charfield! +} + +type Bar @entity { + id: ID! + type1: [Type1!]! + type2: [Type2!]! + type3: [Type3!]! + type4: [Type4!]! + type5: [Type5!]! + type6: [Type6!]! + type7: [Type7!]! + type8: [Type8!]! + type9: [Type9!]! + type10: [Type10!]! + type11: [Type11!]! +} +"#; + + let _ = ParsedGraphQLSchema::new( + "test", + "test", + ExecutionSource::Wasm, + Some(&GraphQLSchema::new(schema.to_string())), + ) + .unwrap(); + } + + #[test] + #[should_panic( + expected = "FieldDefinition(foo) on TypeDefinition(Bar) is a many-to-many relationship where the inner scalar is of type `name: Charfield!`. However, only inner scalars of type `id: ID!` are allowed." + )] + fn test_schema_validator_m2m_fk_field_ref_col_is_id() { + let schema = r#" +type Foo @entity { + id: ID! + name: Charfield! +} + +type Bar @entity { + id: ID! + foo: [Foo!]! @join(on:name) +} +"#; + + let _ = ParsedGraphQLSchema::new( + "test", + "test", + ExecutionSource::Wasm, + Some(&GraphQLSchema::new(schema.to_string())), + ) + .unwrap(); + } } diff --git a/packages/fuel-indexer-lib/src/graphql/validator.rs b/packages/fuel-indexer-lib/src/graphql/validator.rs index 58874581b..97eb81511 100644 --- a/packages/fuel-indexer-lib/src/graphql/validator.rs +++ b/packages/fuel-indexer-lib/src/graphql/validator.rs @@ -1,5 +1,7 @@ -use crate::constants::*; -use async_graphql_parser::types::{FieldDefinition, TypeDefinition, TypeKind}; +use crate::{constants::*, graphql::MAX_FOREIGN_KEY_LIST_FIELDS}; +use async_graphql_parser::types::{ + FieldDefinition, ObjectType, TypeDefinition, TypeKind, +}; use std::collections::HashSet; /// General container used to store a set of GraphQL schema validation functions. @@ -13,15 +15,8 @@ impl GraphQLSchemaValidator { } } - /// Check the given `TypeDefinition` name is not a disallowed Sway ABI name. - pub fn check_disallowed_abi_typedef_name(name: &str) { - if FUEL_PRIMITIVES.contains(name) { - panic!("TypeDefinition name '{name}' is reserved."); - } - } - /// Check that a `TypeKind::Union(UnionType)`'s members are either all virtual, or all regular/non-virtual - pub fn check_derived_union_is_well_formed( + pub fn check_derived_union_virtuality_is_well_formed( typ: &TypeDefinition, virtual_type_names: &mut HashSet, ) { @@ -69,9 +64,9 @@ impl GraphQLSchemaValidator { pub fn derived_field_type_is_consistent( union_name: &str, field_name: &str, - fields: &HashSet, + types: &HashSet, ) { - if fields.contains(field_name) { + if types.len() > 1 { panic!("Derived type from Union({union_name}) contains Field({field_name}) which does not have a consistent type across all members."); } } @@ -83,4 +78,67 @@ impl GraphQLSchemaValidator { panic!("FieldDefinition({name}) is a nested list, which is not supported."); } } + + /// Ensure a `FieldDefinition` with name `id` is of type `ID`. + pub fn id_field_is_type_id(f: &FieldDefinition, obj_name: &str) { + let name = f.name.to_string(); + let field_type = f.ty.node.to_string(); + // FIXME: Find some way to use IdCol here? + if name == "id" && field_type != "ID!" { + panic!("FieldDefinition({name}) on TypeDefinition({obj_name}) must be of type `ID!`. Found type `{field_type}`."); + } + } + + /// Ensure `TypeDefinition`s that are marked as virtual, don't contain an `id: ID` `FieldDefinition`. + /// + /// `id: ID!` fields are reserved for non-virtual entities only. + pub fn virtual_type_has_no_id_field(o: &ObjectType, obj_name: &str) { + let has_id_field = o.fields.iter().any(|f| { + f.node.name.to_string() == "id" && f.node.ty.node.to_string() == "ID!" + }); + if has_id_field { + panic!("Virtual TypeDefinition({obj_name}) cannot contain an `id: ID!` FieldDefinition."); + } + } + + /// Ensure that any `FieldDefinition` that itself is a foreign relationship, does not contain + /// a `@unique` directive. + pub fn foreign_key_field_contains_no_unique_directive( + f: &FieldDefinition, + obj_name: &str, + ) { + let name = f.name.to_string(); + let has_unique_directive = f + .directives + .iter() + .any(|d| d.node.name.to_string() == "unique"); + if has_unique_directive { + panic!("FieldDefinition({name}) on TypeDefinition({obj_name}) cannot contain a `@unique` directive."); + } + } + + /// Ensure that a given `TypeDefiniton` does not contain more than `MAX_FOREIGN_KEY_LIST_FIELDS` many-to-many relationships. + pub fn verify_m2m_relationship_count(obj_name: &str, m2m_field_count: usize) { + if m2m_field_count > MAX_FOREIGN_KEY_LIST_FIELDS { + panic!( + "TypeDefinition({obj_name}) exceeds the allowed number of many-to-many` relationships. The maximum allowed is {MAX_FOREIGN_KEY_LIST_FIELDS}.", + ); + } + } + + /// Ensure that a `FieldDefinition` that is a many-to-many relationship only references parent `FieldDefinitions` that + /// are of type `id: ID!`. + pub fn m2m_fk_field_ref_col_is_id( + f: &FieldDefinition, + obj_name: &str, + ref_coltype: &str, + ref_colname: &str, + ) { + let name = f.name.to_string(); + if ref_coltype != "UID" || ref_colname != "id" { + panic!( + "FieldDefinition({name}) on TypeDefinition({obj_name}) is a many-to-many relationship where the inner scalar is of type `{ref_colname}: {ref_coltype}!`. However, only inner scalars of type `id: ID!` are allowed.", + ); + } + } } diff --git a/packages/fuel-indexer-macros/src/decoder.rs b/packages/fuel-indexer-macros/src/decoder.rs index 7b1b14dc3..f360976be 100644 --- a/packages/fuel-indexer-macros/src/decoder.rs +++ b/packages/fuel-indexer-macros/src/decoder.rs @@ -5,10 +5,7 @@ use async_graphql_parser::types::{ use async_graphql_parser::{Pos, Positioned}; use async_graphql_value::Name; use fuel_indexer_lib::{ - graphql::{ - field_id, types::IdCol, GraphQLSchemaValidator, ParsedGraphQLSchema, - MAX_FOREIGN_KEY_LIST_FIELDS, - }, + graphql::{field_id, types::IdCol, ParsedGraphQLSchema, MAX_FOREIGN_KEY_LIST_FIELDS}, ExecutionSource, }; use fuel_indexer_types::type_id; @@ -511,8 +508,6 @@ impl Decoder for ObjectDecoder { TypeKind::Object(o) => { let obj_name = typ.name.to_string(); - GraphQLSchemaValidator::check_disallowed_graphql_typedef_name(&obj_name); - let ident = format_ident!("{obj_name}"); let type_id = type_id(&parsed.fully_qualified_namespace(), &obj_name); diff --git a/packages/fuel-indexer-macros/src/helpers.rs b/packages/fuel-indexer-macros/src/helpers.rs index 57616d90b..57289a9ab 100644 --- a/packages/fuel-indexer-macros/src/helpers.rs +++ b/packages/fuel-indexer-macros/src/helpers.rs @@ -96,19 +96,9 @@ pub fn is_ignored_type(typ: &TypeDeclaration) -> bool { /// Whether the TypeDeclaration should be used to build struct fields and decoders pub fn is_non_decodable_type(typ: &TypeDeclaration) -> bool { - if GENERIC_TYPES.contains(typ.type_field.as_str()) { - return true; - } - - if is_ignored_type(typ) { - return true; - } - - if is_unit_type(typ) { - return true; - } - - false + is_ignored_type(typ) + || is_unit_type(typ) + || GENERIC_TYPES.contains(typ.type_field.as_str()) } /// Derive Ident for decoded type diff --git a/packages/fuel-indexer-macros/src/indexer.rs b/packages/fuel-indexer-macros/src/indexer.rs index 780e1362e..000065fad 100644 --- a/packages/fuel-indexer-macros/src/indexer.rs +++ b/packages/fuel-indexer-macros/src/indexer.rs @@ -4,8 +4,8 @@ use crate::{ }; use fuel_abi_types::abi::program::TypeDeclaration; use fuel_indexer_lib::{ - constants::*, graphql::GraphQLSchemaValidator, manifest::ContractIds, - manifest::Manifest, utils::workspace_manifest_prefix, ExecutionSource, + constants::*, manifest::ContractIds, manifest::Manifest, + utils::workspace_manifest_prefix, ExecutionSource, }; use fuel_indexer_types::{type_id, FUEL_TYPES_NAMESPACE}; use fuels::{core::codec::resolve_fn_selector, types::param_types::ParamType}; @@ -122,8 +122,6 @@ fn process_fn_items( proc_macro_error::abort_call_site!("'{}' is a reserved Fuel type.", ty) } - GraphQLSchemaValidator::check_disallowed_abi_typedef_name(&ty.to_string()); - type_ids.insert(ty.to_string(), typ.type_id); decoded_abi_types.insert(typ.type_id); @@ -314,33 +312,34 @@ fn process_fn_items( } FnArg::Typed(PatType { ty, .. }) => { if let Type::Path(path) = &**ty { - let path = path + let typ = path .path .segments .last() .expect("Could not get last path segment."); - let path_ident = path.ident.to_string(); - let name = decoded_ident(&path_ident); + let typ_name = typ.ident.to_string(); + let dispatcher_name = decoded_ident(&typ_name); - if !type_ids.contains_key(&path_ident) { + if !type_ids.contains_key(&typ_name) { proc_macro_error::abort_call_site!( "Type with ident '{:?}' not defined in the ABI.", - path.ident + typ.ident ); }; - if DISALLOWED_ABI_JSON_TYPES.contains(path_ident.as_str()) - { + if DISALLOWED_ABI_JSON_TYPES.contains(typ_name.as_str()) { proc_macro_error::abort_call_site!( "Type with ident '{:?}' is not currently supported.", - path.ident + typ.ident ) } - input_checks.push(quote! { self.#name.len() > 0 }); + input_checks + .push(quote! { self.#dispatcher_name.len() > 0 }); - arg_list.push(quote! { self.#name[0].clone() }); + arg_list + .push(quote! { self.#dispatcher_name[0].clone() }); } else { proc_macro_error::abort_call_site!( "Arguments must be types defined in the ABI."