Skip to content

Commit 7316fd8

Browse files
committed
graph, runtime: Move entity validation into the graph crate
1 parent d874174 commit 7316fd8

File tree

8 files changed

+267
-247
lines changed

8 files changed

+267
-247
lines changed

core/src/subgraph/instance_manager.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1097,9 +1097,8 @@ async fn process_block<T: RuntimeHostBuilder<C>, C: Blockchain>(
10971097

10981098
// If a deterministic error has happened, make the PoI to be the only entity that'll be stored.
10991099
if has_errors && !is_non_fatal_errors_active {
1100-
let is_poi_entity = |entity_mod: &EntityModification| {
1101-
entity_mod.entity_key().entity_type.as_str() == "Poi$"
1102-
};
1100+
let is_poi_entity =
1101+
|entity_mod: &EntityModification| entity_mod.entity_key().entity_type.is_poi();
11031102
mods.retain(is_poi_entity);
11041103
// Confidence check
11051104
assert!(

graph/src/components/store.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ impl EntityType {
6161
pub fn into_string(self) -> String {
6262
self.0
6363
}
64+
65+
pub fn is_poi(&self) -> bool {
66+
&self.0 == "Poi$"
67+
}
6468
}
6569

6670
impl fmt::Display for EntityType {

graph/src/data/graphql/ext.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ use crate::prelude::s::{
44
Definition, Directive, Document, EnumType, Field, InterfaceType, ObjectType, Type,
55
TypeDefinition, Value,
66
};
7+
use crate::prelude::ValueType;
78
use lazy_static::lazy_static;
89
use std::collections::{BTreeMap, HashMap};
10+
use std::str::FromStr;
911

1012
lazy_static! {
1113
static ref ALLOW_NON_DETERMINISTIC_FULLTEXT_SEARCH: bool = if cfg!(debug_assertions) {
@@ -62,6 +64,8 @@ pub trait DocumentExt {
6264
fn object_or_interface(&self, name: &str) -> Option<ObjectOrInterface<'_>>;
6365

6466
fn get_named_type(&self, name: &str) -> Option<&TypeDefinition>;
67+
68+
fn scalar_value_type(&self, field_type: &Type) -> ValueType;
6569
}
6670

6771
impl DocumentExt for Document {
@@ -196,10 +200,31 @@ impl DocumentExt for Document {
196200
TypeDefinition::Union(t) => &t.name == name,
197201
})
198202
}
203+
204+
fn scalar_value_type(&self, field_type: &Type) -> ValueType {
205+
use TypeDefinition as t;
206+
match field_type {
207+
Type::NamedType(name) => {
208+
ValueType::from_str(&name).unwrap_or_else(|_| match self.get_named_type(name) {
209+
Some(t::Object(_)) | Some(t::Interface(_)) | Some(t::Enum(_)) => {
210+
ValueType::String
211+
}
212+
Some(t::Scalar(_)) => unreachable!("user-defined scalars are not used"),
213+
Some(t::Union(_)) => unreachable!("unions are not used"),
214+
Some(t::InputObject(_)) => unreachable!("inputObjects are not used"),
215+
None => unreachable!("names of field types have been validated"),
216+
})
217+
}
218+
Type::NonNullType(inner) => self.scalar_value_type(inner),
219+
Type::ListType(inner) => self.scalar_value_type(inner),
220+
}
221+
}
199222
}
200223

201224
pub trait TypeExt {
202225
fn get_base_type(&self) -> &str;
226+
fn is_list(&self) -> bool;
227+
fn is_non_null(&self) -> bool;
203228
}
204229

205230
impl TypeExt for Type {
@@ -210,6 +235,22 @@ impl TypeExt for Type {
210235
Type::ListType(inner) => Self::get_base_type(inner),
211236
}
212237
}
238+
239+
fn is_list(&self) -> bool {
240+
match self {
241+
Type::NamedType(_) => false,
242+
Type::NonNullType(inner) => inner.is_list(),
243+
Type::ListType(_) => true,
244+
}
245+
}
246+
247+
// Returns true if the given type is a non-null type.
248+
fn is_non_null(&self) -> bool {
249+
match self {
250+
Type::NonNullType(_) => true,
251+
_ => false,
252+
}
253+
}
213254
}
214255

215256
pub trait DirectiveExt {

graph/src/data/store/mod.rs

Lines changed: 215 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use crate::{
22
components::store::{DeploymentLocator, EntityType},
3-
prelude::{q, r, s, CacheWeight, EntityKey, QueryExecutionError},
3+
prelude::{anyhow::Context, q, r, s, CacheWeight, EntityKey, QueryExecutionError},
44
runtime::gas::{Gas, GasSizeOf},
55
};
66
use crate::{data::subgraph::DeploymentHash, prelude::EntityChange};
@@ -20,6 +20,8 @@ use std::{
2020
use strum::AsStaticRef as _;
2121
use strum_macros::AsStaticStr;
2222

23+
use super::graphql::{ext::DirectiveFinder, DocumentExt as _, TypeExt as _};
24+
2325
/// Custom scalars in GraphQL.
2426
pub mod scalar;
2527

@@ -343,6 +345,22 @@ impl Value {
343345
Value::String(_) => "String".to_owned(),
344346
}
345347
}
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+
}
346364
}
347365

348366
impl fmt::Display for Value {
@@ -583,6 +601,92 @@ impl Entity {
583601
};
584602
}
585603
}
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+
}
586690
}
587691

588692
impl From<Entity> for BTreeMap<String, q::Value> {
@@ -670,3 +774,113 @@ fn value_bigint() {
670774
);
671775
assert_eq!(r::Value::from(from_query), graphql_value);
672776
}
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+
}

graphql/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ pub mod prelude {
2929
pub use super::execution::{ExecutionContext, Query, Resolver};
3030
pub use super::introspection::{introspection_schema, IntrospectionResolver};
3131
pub use super::query::{execute_query, ext::BlockConstraint, QueryExecutionOptions};
32-
pub use super::schema::{api_schema, ast::is_list, ast::validate_entity, APISchemaError};
32+
pub use super::schema::{api_schema, APISchemaError};
3333
pub use super::store::{build_query, StoreResolver};
3434
pub use super::subscription::SubscriptionExecutionOptions;
3535
pub use super::values::MaybeCoercible;

0 commit comments

Comments
 (0)