@@ -41,6 +41,10 @@ contract SubgraphManifestTest is Test {
4141 /// The subgraph manifest, as a template: no deployment fact in it.
4242 string constant SUBGRAPH_YAML = "subgraph/subgraph.yaml " ;
4343
44+ /// The schema the mapping stores into, and the other side of the
45+ /// `mapping.entities` check.
46+ string constant SCHEMA_GRAPHQL = "subgraph/schema.graphql " ;
47+
4448 /// The interface whose ABI the manifest decodes with. Named rather than
4549 /// read out of the manifest, so that the manifest is compared to an
4650 /// expectation instead of to itself.
@@ -277,6 +281,107 @@ contract SubgraphManifestTest is Test {
277281 );
278282 }
279283
284+ /// The schema's entity declarations, as JSON: `types` are the entity type
285+ /// names, `declarations` is how many lines declare one.
286+ ///
287+ /// A LINE read, which every manifest check here is forbidden from being,
288+ /// because there is no schema parser to reach for: this lane deliberately
289+ /// has no node, and `yq` does not speak GraphQL. `declarations` is what
290+ /// makes that safe. It counts every non-comment line carrying the entity
291+ /// directive at all, and `schemaEntityTypes` holds `types` to that count,
292+ /// so a schema written in a shape this cannot read — the directive on a
293+ /// line of its own, a description carrying the word — fails loudly rather
294+ /// than silently reporting fewer entities than the file declares. Comment
295+ /// lines are dropped first, since a commented-out declaration is not one,
296+ /// which is the same distinction reading the manifest as YAML gets for
297+ /// free.
298+ /// @return The `{declarations, types}` object as JSON.
299+ function schemaEntityJson () internal returns (string memory ) {
300+ string [] memory cmd = new string [](4 );
301+ cmd[0 ] = "jq " ;
302+ cmd[1 ] = "-Rn " ;
303+ cmd[2 ] = "[inputs | select(test( \" ^[[:space:]]*# \" ) | not) | select(test( \" @entity \" ))] "
304+ " | {declarations: length, types: [.[] "
305+ " | scan( \" ^type[[:space:]]+([A-Za-z_][A-Za-z0-9_]*)[[:space:]]+@entity \" ) | .[0]]} " ;
306+ cmd[3 ] = SCHEMA_GRAPHQL;
307+ return string (vm.ffi (cmd));
308+ }
309+
310+ /// Every entity type the schema declares.
311+ /// @return The entity type names.
312+ function schemaEntityTypes () internal returns (string [] memory ) {
313+ string memory json = schemaEntityJson ();
314+ string [] memory types = vm.parseJsonStringArray (json, ".types " );
315+ assertEq (
316+ types.length ,
317+ vm.parseJsonUint (json, ".declarations " ),
318+ "schema.graphql declares @entity on a line that is not a type declaration this can read "
319+ );
320+ return types;
321+ }
322+
323+ /// The manifest MUST declare the entities the mapping stores.
324+ ///
325+ /// `mapping.entities` is the manifest's claim about which schema entities
326+ /// the handler writes. Nothing downstream contradicts a stale one —
327+ /// graph-node does not enforce the list at runtime and `graph codegen` does
328+ /// not read it — which is how `Transaction`, stored by
329+ /// `createTransactionEntity` on every event, sat unlisted
330+ /// (rainlanguage/rain.metadata#151).
331+ ///
332+ /// The schema is the other side, rather than a list restated here, because
333+ /// it is a separate file that codegen does read, and because
334+ /// `dataSources` is pinned to one entry: there is exactly one mapping for
335+ /// its entities to belong to, so an entity the schema declares is one that
336+ /// mapping stores or dead schema. Both directions are asserted and neither
337+ /// implies the other: a schema entity the manifest omits is #151 again, and
338+ /// a manifest entity the schema does not declare is a name `graph build`
339+ /// rejects.
340+ function testManifestDeclaresTheEntitiesTheMappingStores () external {
341+ string memory json = manifestJson ();
342+ string memory entitiesPath = string .concat (THE_DATA_SOURCE, ".mapping.entities " );
343+ assertTrue (vm.keyExistsJson (json, entitiesPath), "subgraph.yaml declares no mapping.entities " );
344+
345+ string [] memory declared = vm.parseJsonStringArray (json, entitiesPath);
346+ string [] memory schema = schemaEntityTypes ();
347+
348+ for (uint256 i = 0 ; i < schema.length ; i++ ) {
349+ assertTrue (
350+ _holds (declared, schema[i]),
351+ string .concat (
352+ "subgraph.yaml's mapping.entities omits " , schema[i], ", which schema.graphql declares @entity "
353+ )
354+ );
355+ }
356+ for (uint256 i = 0 ; i < declared.length ; i++ ) {
357+ assertTrue (
358+ _holds (schema, declared[i]),
359+ string .concat (
360+ "subgraph.yaml's mapping.entities declares " ,
361+ declared[i],
362+ ", which schema.graphql declares no @entity type for "
363+ )
364+ );
365+ }
366+ assertEq (
367+ declared.length ,
368+ schema.length ,
369+ "subgraph.yaml's mapping.entities and schema.graphql name the same entities, one of them twice "
370+ );
371+ }
372+
373+ /// @param names The names to search.
374+ /// @param name The name to find.
375+ /// @return Whether the list holds the name.
376+ function _holds (string [] memory names , string memory name ) internal pure returns (bool ) {
377+ for (uint256 i = 0 ; i < names.length ; i++ ) {
378+ if (keccak256 (bytes (names[i])) == keccak256 (bytes (name))) {
379+ return true ;
380+ }
381+ }
382+ return false ;
383+ }
384+
280385 /// Every node in the document keyed `network` that is not the placeholder,
281386 /// by path.
282387 ///
0 commit comments