Repository: graphprotocol/graph-tooling
Pull request #1993 (2025-03-27 15:21:24)

graph init should only generate entity handlers for immutable enitites
We recently changed composed subgraphs to only work with immutable entities
This change in cli is focused on `graph init` to stop generating handlers for mutable entities

cloudflare-workers-and-pages[bot]: ## Deploying graph-tooling with &nbsp;<a href="https://pages.dev"><img alt="Cloudflare Pages" src="https://user-images.githubusercontent.com/23264/106598434-9e719e00-654f-11eb-9e59-6167043cfa01.png" width="16"></a> &nbsp;Cloudflare Pages

<table><tr><td><strong>Latest commit:</strong> </td><td>
<code>70423ca</code>
</td></tr>
<tr><td><strong>Status:</strong></td><td>&nbsp;✅&nbsp; Deploy successful!</td></tr>
<tr><td><strong>Preview URL:</strong></td><td>
<a href='https://cf39d472.graph-tooling.pages.dev'>https://cf39d472.graph-tooling.pages.dev</a>
</td></tr>
<tr><td><strong>Branch Preview URL:</strong></td><td>
<a href='https://krishna-sc-only-immutable.graph-tooling.pages.dev'>https://krishna-sc-only-immutable.graph-tooling.pages.dev</a>
</td></tr>
</table>

[View logs](https://dash.cloudflare.com/a3eea03329b242c22dbbf5bfc4ff19a3/pages/view/graph-tooling/cf39d472-b7b2-4376-ae9a-a7d88a3ab778)

github-actions[bot]: <!-- changesetsSnapshotPrCommentKey -->
### 🚀 Snapshot Release (`alpha`)

The latest changes of this PR are available as `alpha` on npm (based on the declared `changesets`):
| Package | Version | Info |
|------|---------|----|
| `@graphprotocol/graph-cli` | `0.96.1-alpha-20250327152200-70423caa604605c5840d8cdf34e538ed398840e9` | [npm ↗︎](https://www.npmjs.com/package/@graphprotocol/graph-cli/v/0.96.1-alpha-20250327152200-70423caa604605c5840d8cdf34e538ed398840e9) [unpkg ↗︎](https://unpkg.com/browse/@graphprotocol/graph-cli@0.96.1-alpha-20250327152200-70423caa604605c5840d8cdf34e538ed398840e9/) |

Patch:
diff --git a/.changeset/short-coins-deny.md b/.changeset/short-coins-deny.md
new file mode 100644
index 00000000..20e8f440
--- /dev/null
+++ b/.changeset/short-coins-deny.md
@@ -0,0 +1,5 @@
+---
+'@graphprotocol/graph-cli': minor
+---
+
+Composed subgraphs are modified to only accept immutable entites as triggers from a source subgraph
diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts
index 78f9c305..8a5e23ab 100644
--- a/packages/cli/src/commands/init.ts
+++ b/packages/cli/src/commands/init.ts
@@ -1253,7 +1253,7 @@ async function initSubgraphFromContract(
     }
   }
 
-  let entities: string[] | undefined;
+  let immutableEntities: string[] | undefined;
 
   if (isComposedSubgraph) {
     try {
@@ -1274,7 +1274,14 @@ async function initSubgraphFromContract(
       startBlock ||= getMinStartBlock(manifestYaml)?.toString();
       const schemaString = await loadSubgraphSchemaFromIPFS(ipfsClient, source);
       const schema = await Schema.loadFromString(schemaString);
-      entities = schema.getEntityNames();
+      immutableEntities = schema.getImmutableEntityNames();
+
+      if (immutableEntities.length === 0) {
+        this.error(
+          'Source subgraph must have at least one immutable entity. This subgraph cannot be used as a source subgraph since it has no immutable entities.',
+          { exit: 1 },
+        );
+      }
     } catch (e) {
       this.error(`Failed to load and parse subgraph schema: ${e.message}`, { exit: 1 });
     }
@@ -1316,7 +1323,7 @@ async function initSubgraphFromContract(
           startBlock,
           node,
           spkgPath,
-          entities,
+          entities: immutableEntities,
         },
         spinner,
       );
diff --git a/packages/cli/src/protocols/subgraph/scaffold/mapping.ts b/packages/cli/src/protocols/subgraph/scaffold/mapping.ts
index a2241259..cff158d3 100644
--- a/packages/cli/src/protocols/subgraph/scaffold/mapping.ts
+++ b/packages/cli/src/protocols/subgraph/scaffold/mapping.ts
@@ -7,12 +7,11 @@ export const generatePlaceholderHandlers = ({
 }) => `
 import { ExampleEntity } from '../generated/schema'
 import {${entities.join(', ')}} from '../generated/subgraph-${contract}'
-import { EntityTrigger } from '@graphprotocol/graph-ts'
 
 ${entities
   .map(
     entityName => `
-export function handle${entityName}(entity: EntityTrigger<${entityName}>): void {
+export function handle${entityName}(entity: ${entityName}): void {
   // Empty handler for ${entityName}
 }`,
   )
diff --git a/packages/cli/src/schema.ts b/packages/cli/src/schema.ts
index 9f935fe0..12ae99be 100644
--- a/packages/cli/src/schema.ts
+++ b/packages/cli/src/schema.ts
@@ -69,4 +69,24 @@ export default class Schema {
       return isImmutable(entity);
     }).length;
   }
+
+  getImmutableEntityNames(): string[] {
+    return this.ast.definitions
+      .filter(
+        def =>
+          def.kind === 'ObjectTypeDefinition' &&
+          def.directives?.find(
+            directive =>
+              directive.name.value === 'entity' &&
+              directive.arguments?.find(arg => {
+                return (
+                  arg.name.value === 'immutable' &&
+                  arg.value.kind === 'BooleanValue' &&
+                  arg.value.value === true
+                );
+              }),
+          ) !== undefined,
+      )
+      .map(entity => (entity as graphql.ObjectTypeDefinitionNode).name.value);
+  }
 }
diff --git a/packages/ts/common/collections.ts b/packages/ts/common/collections.ts
index f12f98ba..706b92ec 100644
--- a/packages/ts/common/collections.ts
+++ b/packages/ts/common/collections.ts
@@ -457,28 +457,6 @@ export class Entity extends TypedMap<string, Value> {
   }
 }
 
-/**
- * Common representation for entity triggers, this wraps the entity
- * and has fields for the operation type and the entity type.
- */
-export class EntityTrigger<T extends Entity> {
-  constructor(
-    public operation: EntityOp,
-    public type: string,
-    public data: T, // T is a specific type that extends Entity
-  ) {}
-}
-
-/**
- * Enum for entity operations.
- * Create, Modify, Remove
- */
-export enum EntityOp {
-  Create,
-  Modify,
-  Remove,
-}
-
 /**
  * The result of an operation, with a corresponding value and error type.
  */


Test patch:
diff --git a/packages/cli/src/schema.test.ts b/packages/cli/src/schema.test.ts
new file mode 100644
index 00000000..66826751
--- /dev/null
+++ b/packages/cli/src/schema.test.ts
@@ -0,0 +1,44 @@
+import { beforeEach, describe, expect, test } from 'vitest';
+import Schema from './schema.js';
+
+describe('Schema', () => {
+  const schemaDocument = `
+    type Entity1 @entity {
+      id: ID!
+    }
+
+    type Entity2 @entity(immutable: true) {
+      id: ID!
+    }
+
+    type Entity3 @entity(immutable: false) {
+      id: ID!
+    }
+  `;
+
+  let schema: Schema;
+
+  beforeEach(async () => {
+    schema = await Schema.loadFromString(schemaDocument);
+  });
+
+  test('getEntityNames returns all entity types', () => {
+    const entityNames = schema.getEntityNames();
+    expect(entityNames).toEqual(['Entity1', 'Entity2', 'Entity3']);
+  });
+
+  test('getImmutableEntityNames returns only immutable entity types', () => {
+    const immutableEntityNames = schema.getImmutableEntityNames();
+    expect(immutableEntityNames).toEqual(['Entity2']);
+  });
+
+  test('getImmutableEntityNames handles entities without immutable flag', () => {
+    const immutableEntityNames = schema.getImmutableEntityNames();
+    expect(immutableEntityNames).not.toContain('Entity1');
+  });
+
+  test('getImmutableEntityNames handles explicitly non-immutable entities', () => {
+    const immutableEntityNames = schema.getImmutableEntityNames();
+    expect(immutableEntityNames).not.toContain('Entity3');
+  });
+});