diff --git a/.changeset/short-coins-deny.md b/.changeset/short-coins-deny.md new file mode 100644 index 000000000..20e8f440c --- /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 78f9c305e..8a5e23ab0 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 a2241259a..cff158d30 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.test.ts b/packages/cli/src/schema.test.ts new file mode 100644 index 000000000..668267510 --- /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'); + }); +}); diff --git a/packages/cli/src/schema.ts b/packages/cli/src/schema.ts index 9f935fe05..12ae99bed 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 f12f98ba0..706b92ec8 100644 --- a/packages/ts/common/collections.ts +++ b/packages/ts/common/collections.ts @@ -457,28 +457,6 @@ export class Entity extends TypedMap { } } -/** - * Common representation for entity triggers, this wraps the entity - * and has fields for the operation type and the entity type. - */ -export class EntityTrigger { - 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. */