1
1
use crate :: {
2
2
components:: store:: { DeploymentLocator , EntityType } ,
3
- prelude:: { q, r, s, CacheWeight , EntityKey , QueryExecutionError } ,
3
+ prelude:: { anyhow :: Context , q, r, s, CacheWeight , EntityKey , QueryExecutionError } ,
4
4
runtime:: gas:: { Gas , GasSizeOf } ,
5
5
} ;
6
6
use crate :: { data:: subgraph:: DeploymentHash , prelude:: EntityChange } ;
@@ -20,6 +20,8 @@ use std::{
20
20
use strum:: AsStaticRef as _;
21
21
use strum_macros:: AsStaticStr ;
22
22
23
+ use super :: graphql:: { ext:: DirectiveFinder , DocumentExt as _, TypeExt as _} ;
24
+
23
25
/// Custom scalars in GraphQL.
24
26
pub mod scalar;
25
27
@@ -343,6 +345,22 @@ impl Value {
343
345
Value :: String ( _) => "String" . to_owned ( ) ,
344
346
}
345
347
}
348
+
349
+ pub fn is_assignable ( & self , scalar_type : & ValueType , is_list : bool ) -> bool {
350
+ match ( self , scalar_type) {
351
+ ( Value :: String ( _) , ValueType :: String )
352
+ | ( Value :: BigDecimal ( _) , ValueType :: BigDecimal )
353
+ | ( Value :: BigInt ( _) , ValueType :: BigInt )
354
+ | ( Value :: Bool ( _) , ValueType :: Boolean )
355
+ | ( Value :: Bytes ( _) , ValueType :: Bytes )
356
+ | ( Value :: Int ( _) , ValueType :: Int )
357
+ | ( Value :: Null , _) => true ,
358
+ ( Value :: List ( values) , _) if is_list => values
359
+ . iter ( )
360
+ . all ( |value| value. is_assignable ( scalar_type, false ) ) ,
361
+ _ => false ,
362
+ }
363
+ }
346
364
}
347
365
348
366
impl fmt:: Display for Value {
@@ -583,6 +601,92 @@ impl Entity {
583
601
} ;
584
602
}
585
603
}
604
+
605
+ /// Validate that this entity matches the object type definition in the
606
+ /// schema. An entity that passes these checks can be stored
607
+ /// successfully in the subgraph's database schema
608
+ pub fn validate ( & self , schema : & s:: Document , key : & EntityKey ) -> Result < ( ) , anyhow:: Error > {
609
+ if key. entity_type . is_poi ( ) {
610
+ // Users can't modify Poi entities, and therefore they do not
611
+ // need to be validated. In addition, the schema has no object
612
+ // type for them, and validation would therefore fail
613
+ return Ok ( ( ) ) ;
614
+ }
615
+ let object_type_definitions = schema. get_object_type_definitions ( ) ;
616
+ let object_type = object_type_definitions
617
+ . iter ( )
618
+ . find ( |object_type| key. entity_type . as_str ( ) == & object_type. name )
619
+ . with_context ( || {
620
+ format ! (
621
+ "Entity {}[{}]: unknown entity type `{}`" ,
622
+ key. entity_type, key. entity_id, key. entity_type
623
+ )
624
+ } ) ?;
625
+
626
+ for field in & object_type. fields {
627
+ let is_derived = field. is_derived ( ) ;
628
+ match ( self . get ( & field. name ) , is_derived) {
629
+ ( Some ( value) , false ) => {
630
+ let scalar_type = schema. scalar_value_type ( & field. field_type ) ;
631
+ if field. field_type . is_list ( ) {
632
+ // Check for inhomgeneous lists to produce a better
633
+ // error message for them; other problems, like
634
+ // assigning a scalar to a list will be caught below
635
+ if let Value :: List ( elts) = value {
636
+ for ( index, elt) in elts. iter ( ) . enumerate ( ) {
637
+ if !elt. is_assignable ( & scalar_type, false ) {
638
+ anyhow:: bail!(
639
+ "Entity {}[{}]: field `{}` is of type {}, but the value `{}` \
640
+ contains a {} at index {}",
641
+ key. entity_type,
642
+ key. entity_id,
643
+ field. name,
644
+ & field. field_type,
645
+ value,
646
+ elt. type_name( ) ,
647
+ index
648
+ ) ;
649
+ }
650
+ }
651
+ }
652
+ }
653
+ if !value. is_assignable ( & scalar_type, field. field_type . is_list ( ) ) {
654
+ anyhow:: bail!(
655
+ "Entity {}[{}]: the value `{}` for field `{}` must have type {} but has type {}" ,
656
+ key. entity_type,
657
+ key. entity_id,
658
+ value,
659
+ field. name,
660
+ & field. field_type,
661
+ value. type_name( )
662
+ ) ;
663
+ }
664
+ }
665
+ ( None , false ) => {
666
+ if field. field_type . is_non_null ( ) {
667
+ anyhow:: bail!(
668
+ "Entity {}[{}]: missing value for non-nullable field `{}`" ,
669
+ key. entity_type,
670
+ key. entity_id,
671
+ field. name,
672
+ ) ;
673
+ }
674
+ }
675
+ ( Some ( _) , true ) => {
676
+ anyhow:: bail!(
677
+ "Entity {}[{}]: field `{}` is derived and can not be set" ,
678
+ key. entity_type,
679
+ key. entity_id,
680
+ field. name,
681
+ ) ;
682
+ }
683
+ ( None , true ) => {
684
+ // derived fields should not be set
685
+ }
686
+ }
687
+ }
688
+ Ok ( ( ) )
689
+ }
586
690
}
587
691
588
692
impl From < Entity > for BTreeMap < String , q:: Value > {
@@ -670,3 +774,113 @@ fn value_bigint() {
670
774
) ;
671
775
assert_eq ! ( r:: Value :: from( from_query) , graphql_value) ;
672
776
}
777
+
778
+ #[ test]
779
+ fn entity_validation ( ) {
780
+ fn make_thing ( name : & str ) -> Entity {
781
+ let mut thing = Entity :: new ( ) ;
782
+ thing. set ( "id" , name) ;
783
+ thing. set ( "name" , name) ;
784
+ thing. set ( "stuff" , "less" ) ;
785
+ thing. set ( "favorite_color" , "red" ) ;
786
+ thing. set ( "things" , Value :: List ( vec ! [ ] ) ) ;
787
+ thing
788
+ }
789
+
790
+ fn check ( thing : Entity , errmsg : & str ) {
791
+ const DOCUMENT : & str = "
792
+ enum Color { red, yellow, blue }
793
+ interface Stuff { id: ID!, name: String! }
794
+ type Cruft @entity {
795
+ id: ID!,
796
+ thing: Thing!
797
+ }
798
+ type Thing @entity {
799
+ id: ID!,
800
+ name: String!,
801
+ favorite_color: Color,
802
+ stuff: Stuff,
803
+ things: [Thing!]!
804
+ # Make sure we do not validate derived fields; it's ok
805
+ # to store a thing with a null Cruft
806
+ cruft: Cruft! @derivedFrom(field: \" thing\" )
807
+ }" ;
808
+ let subgraph = DeploymentHash :: new ( "doesntmatter" ) . unwrap ( ) ;
809
+ let schema =
810
+ crate :: prelude:: Schema :: parse ( DOCUMENT , subgraph) . expect ( "Failed to parse test schema" ) ;
811
+ let id = thing. id ( ) . unwrap_or ( "none" . to_owned ( ) ) ;
812
+ let key = EntityKey :: data (
813
+ DeploymentHash :: new ( "doesntmatter" ) . unwrap ( ) ,
814
+ "Thing" . to_owned ( ) ,
815
+ id. to_owned ( ) ,
816
+ ) ;
817
+
818
+ let err = thing. validate ( & schema. document , & key) ;
819
+ if errmsg == "" {
820
+ assert ! (
821
+ err. is_ok( ) ,
822
+ "checking entity {}: expected ok but got {}" ,
823
+ id,
824
+ err. unwrap_err( )
825
+ ) ;
826
+ } else {
827
+ if let Err ( e) = err {
828
+ assert_eq ! ( errmsg, e. to_string( ) , "checking entity {}" , id) ;
829
+ } else {
830
+ panic ! (
831
+ "Expected error `{}` but got ok when checking entity {}" ,
832
+ errmsg, id
833
+ ) ;
834
+ }
835
+ }
836
+ }
837
+
838
+ let mut thing = make_thing ( "t1" ) ;
839
+ thing. set ( "things" , Value :: from ( vec ! [ "thing1" , "thing2" ] ) ) ;
840
+ check ( thing, "" ) ;
841
+
842
+ let thing = make_thing ( "t2" ) ;
843
+ check ( thing, "" ) ;
844
+
845
+ let mut thing = make_thing ( "t3" ) ;
846
+ thing. remove ( "name" ) ;
847
+ check (
848
+ thing,
849
+ "Entity Thing[t3]: missing value for non-nullable field `name`" ,
850
+ ) ;
851
+
852
+ let mut thing = make_thing ( "t4" ) ;
853
+ thing. remove ( "things" ) ;
854
+ check (
855
+ thing,
856
+ "Entity Thing[t4]: missing value for non-nullable field `things`" ,
857
+ ) ;
858
+
859
+ let mut thing = make_thing ( "t5" ) ;
860
+ thing. set ( "name" , Value :: Int ( 32 ) ) ;
861
+ check (
862
+ thing,
863
+ "Entity Thing[t5]: the value `32` for field `name` must \
864
+ have type String! but has type Int",
865
+ ) ;
866
+
867
+ let mut thing = make_thing ( "t6" ) ;
868
+ thing. set ( "things" , Value :: List ( vec ! [ "thing1" . into( ) , 17 . into( ) ] ) ) ;
869
+ check (
870
+ thing,
871
+ "Entity Thing[t6]: field `things` is of type [Thing!]!, \
872
+ but the value `[thing1, 17]` contains a Int at index 1",
873
+ ) ;
874
+
875
+ let mut thing = make_thing ( "t7" ) ;
876
+ thing. remove ( "favorite_color" ) ;
877
+ thing. remove ( "stuff" ) ;
878
+ check ( thing, "" ) ;
879
+
880
+ let mut thing = make_thing ( "t8" ) ;
881
+ thing. set ( "cruft" , "wat" ) ;
882
+ check (
883
+ thing,
884
+ "Entity Thing[t8]: field `cruft` is derived and can not be set" ,
885
+ ) ;
886
+ }
0 commit comments