Skip to content

Commit e12c79c

Browse files
Add new API Version to validate when setting fields not defined in the schema (#4894)
* build(deps): bump chrono from 0.4.26 to 0.4.31 (#4876) Bumps [chrono](https://github.com/chronotope/chrono) from 0.4.26 to 0.4.31. - [Release notes](https://github.com/chronotope/chrono/releases) - [Changelog](https://github.com/chronotope/chrono/blob/main/CHANGELOG.md) - [Commits](chronotope/chrono@v0.4.26...v0.4.31) --- updated-dependencies: - dependency-name: chrono dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump webpki from 0.22.0 to 0.22.1 (#4857) Bumps [webpki](https://github.com/briansmith/webpki) from 0.22.0 to 0.22.1. - [Commits](https://github.com/briansmith/webpki/commits) --- updated-dependencies: - dependency-name: webpki dependency-type: indirect ... Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * runtime: only include valid fields in entity for store_set * graph, runtime: add new apiVersion to validate fields not defined in the schema * graph: update tests for setting invalid field * tests: add runner tests for undefined field setting validation in apiVersion 0.0.8 * graph: add check_invalid_fields method to HostExports --------- Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1 parent bad3223 commit e12c79c

File tree

18 files changed

+336
-47
lines changed

18 files changed

+336
-47
lines changed

Cargo.lock

Lines changed: 6 additions & 18 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

NEWS.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
- **Initialization handler** - A new block handler filter `once` for `ethereum` data sources which enables subgraph developers to create a handle which will be called only once before all other handlers run. This configuration allows the subgraph to use the handler as an initialization handler, performing specific tasks at the start of indexing. [(#4725)](https://github.com/graphprotocol/graph-node/pull/4725)
1313
- **DataSourceContext in manifest** - `DataSourceContext` in Manifest - DataSourceContext can now be defined in the subgraph manifest. It's a free-form map accessible from the mapping. This feature is useful for templating chain-specific data in subgraphs that use the same codebase across multiple chains.[(#4848)](https://github.com/graphprotocol/graph-node/pull/4848)
1414
- `graph-node` version in index node API - The Index Node API now features a new query, Version, which can be used to query the current graph-node version and commit. [(#4852)](https://github.com/graphprotocol/graph-node/pull/4852)
15-
- Added subgraph status to index node API [(#4779)](https://github.com/graphprotocol/graph-node/pull/4779)
15+
- Added a '`paused`' field to Index Node API, a boolean indicating the subgraph’s pause status. [(#4779)](https://github.com/graphprotocol/graph-node/pull/4779)
1616
- Proof of Indexing logs now include block number [(#4798)](https://github.com/graphprotocol/graph-node/pull/4798)
1717
- `subgraph_features` table now tracks details about handlers used in a subgraph [(#4820)](https://github.com/graphprotocol/graph-node/pull/4820)
1818
- Configurable SSL for Postgres in Dockerfile - ssl-mode for Postgres can now be configured via the connection string when deploying through Docker, offering enhanced flexibility in database security settings.[(#4840)](https://github.com/graphprotocol/graph-node/pull/4840)
@@ -43,7 +43,7 @@ Not Relevant
4343
* Update docker-compose.yml by @computeronix in https://github.com/graphprotocol/graph-node/pull/4844
4444
-->
4545

46-
**Full Changelog**: https://github.com/graphprotocol/graph-node/compare/v0.32.0...e253ee14cda2d8456a86ae8f4e3f74a1a7979953
46+
**Full Changelog**: https://github.com/graphprotocol/graph-node/compare/v0.33.0...e253ee14cda2d8456a86ae8f4e3f74a1a7979953
4747

4848
## v0.32.0
4949

graph/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ bytes = "1.0.1"
1313
cid = "0.10.1"
1414
diesel = { version = "1.4.8", features = ["postgres", "serde_json", "numeric", "r2d2", "chrono"] }
1515
diesel_derives = "1.4"
16-
chrono = "0.4.25"
16+
chrono = "0.4.31"
1717
envconfig = "0.10.0"
1818
Inflector = "0.11.3"
1919
isatty = "0.1.9"

graph/src/data/store/mod.rs

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -663,9 +663,6 @@ impl<E, T: IntoIterator<Item = Result<(Word, Value), E>>> TryIntoEntityIterator<
663663

664664
#[derive(Debug, Error, PartialEq, Eq, Clone)]
665665
pub enum EntityValidationError {
666-
#[error("The provided entity has fields not defined in the schema for entity `{entity}`")]
667-
FieldsNotDefined { entity: String },
668-
669666
#[error("Entity {entity}[{id}]: unknown entity type `{entity}`")]
670667
UnknownEntityType { entity: String, id: String },
671668

@@ -928,14 +925,6 @@ impl Entity {
928925
}
929926
})?;
930927

931-
for field in self.0.atoms() {
932-
if !schema.has_field(&key.entity_type, field) {
933-
return Err(EntityValidationError::FieldsNotDefined {
934-
entity: key.entity_type.clone().into_string(),
935-
});
936-
}
937-
}
938-
939928
for field in &object_type.fields {
940929
let is_derived = field.is_derived();
941930
match (self.get(&field.name), is_derived) {

graph/src/data/subgraph/api_version.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ pub const API_VERSION_0_0_6: Version = Version::new(0, 0, 6);
1515
/// Enables event handlers to require transaction receipts in the runtime.
1616
pub const API_VERSION_0_0_7: Version = Version::new(0, 0, 7);
1717

18+
/// Enables validation for fields that doesnt exist in the schema for an entity.
19+
pub const API_VERSION_0_0_8: Version = Version::new(0, 0, 8);
20+
1821
/// Before this check was introduced, there were already subgraphs in the wild with spec version
1922
/// 0.0.3, due to confusion with the api version. To avoid breaking those, we accept 0.0.3 though it
2023
/// doesn't exist.

graph/src/env/mappings.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ pub struct EnvVarsMapping {
1616
/// kilobytes). The default value is 10 megabytes.
1717
pub entity_cache_size: usize,
1818
/// Set by the environment variable `GRAPH_MAX_API_VERSION`. The default
19-
/// value is `0.0.7`.
19+
/// value is `0.0.8`.
2020
pub max_api_version: Version,
2121
/// Set by the environment variable `GRAPH_MAPPING_HANDLER_TIMEOUT`
2222
/// (expressed in seconds). No default is provided.
@@ -93,7 +93,7 @@ pub struct InnerMappingHandlers {
9393
entity_cache_dead_weight: EnvVarBoolean,
9494
#[envconfig(from = "GRAPH_ENTITY_CACHE_SIZE", default = "10000")]
9595
entity_cache_size_in_kb: usize,
96-
#[envconfig(from = "GRAPH_MAX_API_VERSION", default = "0.0.7")]
96+
#[envconfig(from = "GRAPH_MAX_API_VERSION", default = "0.0.8")]
9797
max_api_version: Version,
9898
#[envconfig(from = "GRAPH_MAPPING_HANDLER_TIMEOUT")]
9999
mapping_handler_timeout_in_secs: Option<u64>,

graph/src/schema/input_schema.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,15 @@ impl InputSchema {
379379
.map(|fields| fields.contains(&field))
380380
.unwrap_or(false)
381381
}
382+
383+
pub fn has_field_with_name(&self, entity_type: &EntityType, field: &str) -> bool {
384+
let field = self.inner.pool.lookup(field);
385+
386+
match field {
387+
Some(field_atom) => self.has_field(entity_type, field_atom),
388+
None => false,
389+
}
390+
}
382391
}
383392

384393
/// Create a new pool that contains the names of all the types defined

runtime/test/src/test.rs

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1225,8 +1225,13 @@ struct Host {
12251225
}
12261226

12271227
impl Host {
1228-
async fn new(schema: &str, deployment_hash: &str, wasm_file: &str) -> Host {
1229-
let version = ENV_VARS.mappings.max_api_version.clone();
1228+
async fn new(
1229+
schema: &str,
1230+
deployment_hash: &str,
1231+
wasm_file: &str,
1232+
api_version: Option<Version>,
1233+
) -> Host {
1234+
let version = api_version.unwrap_or(ENV_VARS.mappings.max_api_version.clone());
12301235
let wasm_file = wasm_file_path(wasm_file, API_VERSION_0_0_5);
12311236

12321237
let ds = mock_data_source(&wasm_file, version.clone());
@@ -1324,7 +1329,7 @@ async fn test_store_set_id() {
13241329
name: String,
13251330
}";
13261331

1327-
let mut host = Host::new(schema, "hostStoreSetId", "boolean.wasm").await;
1332+
let mut host = Host::new(schema, "hostStoreSetId", "boolean.wasm", None).await;
13281333

13291334
host.store_set(USER, UID, vec![("id", "u1"), ("name", "user1")])
13301335
.expect("setting with same id works");
@@ -1414,7 +1419,13 @@ async fn test_store_set_invalid_fields() {
14141419
test2: String
14151420
}";
14161421

1417-
let mut host = Host::new(schema, "hostStoreSetInvalidFields", "boolean.wasm").await;
1422+
let mut host = Host::new(
1423+
schema,
1424+
"hostStoreSetInvalidFields",
1425+
"boolean.wasm",
1426+
Some(API_VERSION_0_0_8),
1427+
)
1428+
.await;
14181429

14191430
host.store_set(USER, UID, vec![("id", "u1"), ("name", "user1")])
14201431
.unwrap();
@@ -1437,8 +1448,7 @@ async fn test_store_set_invalid_fields() {
14371448
// So we just check the string contains them
14381449
let err_string = err.to_string();
14391450
dbg!(err_string.as_str());
1440-
assert!(err_string
1441-
.contains("The provided entity has fields not defined in the schema for entity `User`"));
1451+
assert!(err_string.contains("Attempted to set undefined fields [test, test2] for the entity type `User`. Make sure those fields are defined in the schema."));
14421452

14431453
let err = host
14441454
.store_set(
@@ -1449,8 +1459,30 @@ async fn test_store_set_invalid_fields() {
14491459
.err()
14501460
.unwrap();
14511461

1452-
err_says(
1453-
err,
1454-
"Unknown key `test3`. It probably is not part of the schema",
1462+
err_says(err, "Attempted to set undefined fields [test3] for the entity type `User`. Make sure those fields are defined in the schema.");
1463+
1464+
// For apiVersion below 0.0.8, we should not error out
1465+
let mut host2 = Host::new(
1466+
schema,
1467+
"hostStoreSetInvalidFields",
1468+
"boolean.wasm",
1469+
Some(API_VERSION_0_0_7),
14551470
)
1471+
.await;
1472+
1473+
let err_is_none = host2
1474+
.store_set(
1475+
USER,
1476+
UID,
1477+
vec![
1478+
("id", "u1"),
1479+
("name", "user1"),
1480+
("test", "invalid_field"),
1481+
("test2", "invalid_field"),
1482+
],
1483+
)
1484+
.err()
1485+
.is_none();
1486+
1487+
assert!(err_is_none);
14561488
}

runtime/wasm/src/host_exports.rs

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use std::ops::Deref;
44
use std::str::FromStr;
55
use std::time::{Duration, Instant};
66

7+
use graph::data::subgraph::API_VERSION_0_0_8;
78
use graph::data::value::Word;
89

910
use never::Never;
@@ -151,6 +152,54 @@ impl<C: Blockchain> HostExports<C> {
151152
)))
152153
}
153154

155+
fn check_invalid_fields(
156+
&self,
157+
api_version: Version,
158+
data: &HashMap<Word, Value>,
159+
state: &BlockState<C>,
160+
entity_type: &EntityType,
161+
) -> Result<(), HostExportError> {
162+
if api_version >= API_VERSION_0_0_8 {
163+
let has_invalid_fields = data.iter().any(|(field_name, _)| {
164+
!state
165+
.entity_cache
166+
.schema
167+
.has_field_with_name(entity_type, &field_name)
168+
});
169+
170+
if has_invalid_fields {
171+
let mut invalid_fields: Vec<Word> = data
172+
.iter()
173+
.filter_map(|(field_name, _)| {
174+
if !state
175+
.entity_cache
176+
.schema
177+
.has_field_with_name(entity_type, &field_name)
178+
{
179+
Some(field_name.clone())
180+
} else {
181+
None
182+
}
183+
})
184+
.collect();
185+
186+
invalid_fields.sort();
187+
188+
return Err(HostExportError::Deterministic(anyhow!(
189+
"Attempted to set undefined fields [{}] for the entity type `{}`. Make sure those fields are defined in the schema.",
190+
invalid_fields
191+
.iter()
192+
.map(|f| f.as_str())
193+
.collect::<Vec<_>>()
194+
.join(", "),
195+
entity_type
196+
)));
197+
}
198+
}
199+
200+
Ok(())
201+
}
202+
154203
pub(crate) fn store_set(
155204
&self,
156205
logger: &Logger,
@@ -199,9 +248,19 @@ impl<C: Blockchain> HostExports<C> {
199248
}
200249
}
201250

251+
self.check_invalid_fields(self.api_version.clone(), &data, state, &key.entity_type)?;
252+
253+
// Filter out fields that are not in the schema
254+
let filtered_entity_data = data.into_iter().filter(|(field_name, _)| {
255+
state
256+
.entity_cache
257+
.schema
258+
.has_field_with_name(&key.entity_type, field_name)
259+
});
260+
202261
let entity = state
203262
.entity_cache
204-
.make_entity(data.into_iter().map(|(key, value)| (key, value)))
263+
.make_entity(filtered_entity_data)
205264
.map_err(|e| HostExportError::Deterministic(anyhow!(e)))?;
206265

207266
let poi_section = stopwatch.start_section("host_export_store_set__proof_of_indexing");
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[
2+
{
3+
"anonymous": false,
4+
"inputs": [
5+
{
6+
"indexed": false,
7+
"internalType": "string",
8+
"name": "testCommand",
9+
"type": "string"
10+
}
11+
],
12+
"name": "TestEvent",
13+
"type": "event"
14+
}
15+
]

0 commit comments

Comments
 (0)