Skip to content

graph init should only generate entity handlers for immutable enitites #1993

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Apr 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/short-coins-deny.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphprotocol/graph-cli': minor
---

Composed subgraphs are modified to only accept immutable entites as triggers from a source subgraph
13 changes: 10 additions & 3 deletions packages/cli/src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1253,7 +1253,7 @@ async function initSubgraphFromContract(
}
}

let entities: string[] | undefined;
let immutableEntities: string[] | undefined;

if (isComposedSubgraph) {
try {
Expand All @@ -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 });
}
Expand Down Expand Up @@ -1316,7 +1323,7 @@ async function initSubgraphFromContract(
startBlock,
node,
spkgPath,
entities,
entities: immutableEntities,
},
spinner,
);
Expand Down
3 changes: 1 addition & 2 deletions packages/cli/src/protocols/subgraph/scaffold/mapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}
}`,
)
Expand Down
44 changes: 44 additions & 0 deletions packages/cli/src/schema.test.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
20 changes: 20 additions & 0 deletions packages/cli/src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
22 changes: 0 additions & 22 deletions packages/ts/common/collections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down