Skip to content
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/tangy-pants-buy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphprotocol/graph-cli': minor
---

require immutable flag on entities
8 changes: 4 additions & 4 deletions examples/arweave-blocks-transactions/schema.graphql
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
type Block @entity {
type Block @entity(immutable: true) {
id: ID!

timestamp: BigInt!
Expand All @@ -22,7 +22,7 @@ type Block @entity {
poa: Poa
}

type Transaction @entity {
type Transaction @entity(immutable: true) {
id: ID!

block: Block!
Expand All @@ -39,7 +39,7 @@ type Transaction @entity {
reward: Bytes!
}

type Poa @entity {
type Poa @entity(immutable: true) {
id: ID!

option: String!
Expand All @@ -48,7 +48,7 @@ type Poa @entity {
chunk: Bytes!
}

type Tag @entity {
type Tag @entity(immutable: true) {
id: ID!

name: Bytes!
Expand Down
2 changes: 1 addition & 1 deletion examples/cosmos-block-filtering/schema.graphql
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
type Block @entity {
type Block @entity(immutable: true) {
id: ID!
number: BigInt
timestamp: BigInt
Expand Down
4 changes: 2 additions & 2 deletions examples/cosmos-osmosis-token-swaps/schema.graphql
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
type Token @entity {
type Token @entity(immutable: true) {
id: ID!
amount: String
denom: String
}

type TokenSwap @entity {
type TokenSwap @entity(immutable: true) {
id: ID!
sender: String
poolId: String
Expand Down
4 changes: 2 additions & 2 deletions examples/cosmos-validator-delegations/schema.graphql
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
type Delegation @entity {
type Delegation @entity(immutable: true) {
id: ID!
delegatorAddress: String
validatorAddress: String
amount: Coin
}

type Coin @entity {
type Coin @entity(immutable: true) {
id: ID!
denom: String
amount: String
Expand Down
2 changes: 1 addition & 1 deletion examples/cosmos-validator-rewards/schema.graphql
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
type Reward @entity {
type Reward @entity(immutable: true) {
id: ID!
amount: String
validator: String
Expand Down
4 changes: 2 additions & 2 deletions examples/ethereum-basic-event-handlers/schema.graphql
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
type NewGravatar @entity {
type NewGravatar @entity(immutable: false) {
id: ID!
owner: Bytes!
displayName: String!
imageUrl: String!
}

type UpdatedGravatar @entity {
type UpdatedGravatar @entity(immutable: false) {
id: ID!
owner: Bytes!
displayName: String!
Expand Down
2 changes: 1 addition & 1 deletion examples/ethereum-gravatar/schema.graphql
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
type Gravatar @entity {
type Gravatar @entity(immutable: false) {
id: ID!
owner: Bytes!
displayName: String!
Expand Down
4 changes: 2 additions & 2 deletions examples/example-subgraph/schema.graphql
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
type ExampleEntity @entity {
type ExampleEntity @entity(immutable: false) {
id: ID!

optionalBoolean: Boolean
Expand Down Expand Up @@ -32,6 +32,6 @@ type ExampleEntity @entity {
requiredReferenceList: [OtherEntity!]!
}

type OtherEntity @entity {
type OtherEntity @entity(immutable: true) {
id: ID!
}
2 changes: 1 addition & 1 deletion examples/near-blocks/schema.graphql
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
type BlockEvent @entity {
type BlockEvent @entity(immutable: true) {
id: ID!
number: BigInt
hash: Bytes
Expand Down
4 changes: 2 additions & 2 deletions examples/near-receipts/schema.graphql
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
type Greeter @entity {
type Greeter @entity(immutable: true) {
id: ID!
name: String!
greetings: [Greeting!] @derivedFrom(field: "greeter")
}

type Greeting @entity {
type Greeting @entity(immutable: true) {
id: ID!
greeter: Greeter!
timestamp: BigInt!
Expand Down
2 changes: 1 addition & 1 deletion examples/substreams-powered-subgraph/schema.graphql
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
type Contract @entity {
type Contract @entity(immutable: true) {
id: ID!

"The timestamp when the contract was deployed"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export function handleBlock(block: cosmos.Block): void {
`;

exports[`Cosmos subgraph scaffolding > Schema (default) 1`] = `
"type ExampleEntity @entity {
"type ExampleEntity @entity(immutable: true) {
id: ID!
block: Bytes!
count: BigInt!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ export function handleTupleArrayEvent(event: TupleArrayEvent): void {}
`;

exports[`Ethereum subgraph scaffolding > Schema (default) 1`] = `
"type ExampleEntity @entity {
"type ExampleEntity @entity(immutable: true) {
id: Bytes!
count: BigInt!
a: BigInt! # uint256
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/scaffold/__snapshots__/near.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export function handleReceipt(
`;

exports[`NEAR subgraph scaffolding > Schema (default) 1`] = `
"type ExampleEntity @entity {
"type ExampleEntity @entity(immutable: true) {
id: ID!
block: Bytes!
count: BigInt!
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/scaffold/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export const generateEventType = (

export const generateExampleEntityType = (protocol: Protocol, events: any[]) => {
if (protocol.hasABIs() && events.length > 0) {
return `type ExampleEntity @entity {
return `type ExampleEntity @entity(immutable: true) {
id: Bytes!
count: BigInt!
${events[0].inputs
Expand All @@ -97,7 +97,7 @@ export const generateExampleEntityType = (protocol: Protocol, events: any[]) =>
.join('\n')}
}`;
}
return `type ExampleEntity @entity {
return `type ExampleEntity @entity(immutable: true) {
id: ID!
block: Bytes!
count: BigInt!
Expand Down
27 changes: 27 additions & 0 deletions packages/cli/src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,31 @@ export default class Schema {
)
.map(entity => (entity as graphql.ObjectTypeDefinitionNode).name.value);
}

immutableEntitiesCount(): number {
const isImmutable = (entity: graphql.ConstDirectiveNode) => {
return (
entity.arguments?.find(arg => {
return (
(arg.name.value === 'immutable' || arg.name.value === 'timeseries') &&
arg.value.kind === 'BooleanValue' &&
arg.value.value
);
}) !== undefined
);
};

return this.ast.definitions.filter(def => {
if (def.kind !== 'ObjectTypeDefinition') {
return false;
}

const entity = def.directives?.find(directive => directive.name.value === 'entity');
if (entity === undefined) {
return false;
}

return isImmutable(entity);
}).length;
}
}
8 changes: 8 additions & 0 deletions packages/cli/src/type-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,14 @@ export default class TypeGenerator {
typeGenDebug.extend('generateTypes')('Generating types for schema');
await this.generateTypesForSchema({ schema });

if (schema.immutableEntitiesCount() === 0) {
toolbox.print.warning(
'\nThe GraphQL schema does not take advantage of using immutable entities. ' +
'Immutable entities can significantly improve subgraph performance.\n' +
'Learn more about using immutable entities: https://thegraph.com/docs/en/subgraphs/best-practices/immutable-entities-bytes-as-ids/#immutable-entities',
);
}

if (this.options.subgraphSources.length > 0) {
const ipfsClient = createIpfsClient({
url: appendApiVersionForGraph(this.options.ipfsUrl.toString()),
Expand Down
32 changes: 32 additions & 0 deletions packages/cli/src/validation/schema.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import fs from 'node:fs';
import { print } from 'gluegun';
import * as graphql from 'graphql/language/index.js';
import immutable from 'immutable';
import debugFactory from '../debug.js';
Expand Down Expand Up @@ -99,6 +100,36 @@ const validateEntityDirective = (def: any) => {
]);
};

const validateEntityDirectiveImmutableArgument = (def: graphql.ObjectTypeDefinitionNode) => {
validateDebugger('Validating immutable argument on entity directive for %s', def?.name?.value);

const entity = def.directives?.find(directive => directive.name.value === 'entity');
if (entity === undefined) {
return List();
}

const hasImmutableArg =
entity.arguments?.find(arg => arg.name.value === 'immutable') !== undefined;
const hasTimeseriesArg =
entity.arguments?.find(arg => arg.name.value === 'timeseries') !== undefined;

if (hasImmutableArg || hasTimeseriesArg) {
return List();
}

return immutable.fromJS([
{
loc: def.loc,
entity: def.name?.value,
message:
`@entity directive requires \`immutable\` argument\n ` +
print.colors.yellow(
`Hint: Try updating the entity definition with: @entity(immutable: true)`,
),
},
]);
};

const validateEntityID = (def: any) => {
validateDebugger('Validating entity ID for %s', def?.name?.value);
const idField = def.fields.find((field: any) => field.name.value === 'id');
Expand Down Expand Up @@ -986,6 +1017,7 @@ const typeDefinitionValidators = {
? List.of(...validateSubgraphSchemaDirectives(def), ...validateTypeHasNoFields(def))
: List.of(
...validateEntityDirective(def),
...validateEntityDirectiveImmutableArgument(def),
...validateEntityID(def),
...validateEntityFields(defs, def),
...validateNoImportDirective(def),
Expand Down
14 changes: 14 additions & 0 deletions packages/cli/tests/cli/__snapshots__/validation.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -932,6 +932,20 @@ Types generated successfully
"
`;

exports[`Validation > Should require immutable argument on entity directive 1`] = `
"- Load subgraph from subgraph.yaml
✖ Failed to load subgraph from subgraph.yaml: Error in schema.graphql:

EntityA:
- @entity directive requires \`immutable\` argument
Hint: Try updating the entity definition with: @entity(immutable: true)
"
`;

exports[`Validation > Should require immutable argument on entity directive 2`] = `1`;

exports[`Validation > Should require immutable argument on entity directive 3`] = `""`;

exports[`Validation > Source without address is valid 1`] = `
"- Load subgraph from subgraph.yaml
✔ Load subgraph from subgraph.yaml
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/tests/cli/add/subgraph/schema.graphql
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
type NewGravatar @entity {
type NewGravatar @entity(immutable: true) {
id: ID!
owner: Bytes!
displayName: String!
imageUrl: String!
}

type UpdatedGravatar @entity {
type UpdatedGravatar @entity(immutable: true) {
id: ID!
owner: Bytes!
displayName: String!
Expand Down
9 changes: 9 additions & 0 deletions packages/cli/tests/cli/validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,4 +247,13 @@ describe('Validation', { concurrent: true, timeout: 60_000 }, () => {
exitCode: 0,
},
);

cliTest(
'Should require immutable argument on entity directive',
['codegen', '--skip-migrations'],
'validation/require-immutable-argument',
{
exitCode: 1,
},
);
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
type MyEntity @entity {
type MyEntity @entity(immutable: true) {
id: ID!
childTokenURIs: [[String!]!]! # string[][]
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
type MyEntity @entity {
type MyEntity @entity(immutable: true) {
id: ID!
childTokenURIs: [[[String!]!]!]! # string[][][]
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
type MyEntity @entity {
type MyEntity @entity(immutable: true) {
id: ID!
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
type MyEntity @entity {
type MyEntity @entity(immutable: true) {
id: ID!
x: BigDecimal!
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
type A @entity {
type A @entity(immutable: true) {
id: ID!
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
type MyEntity @entity {
type MyEntity @entity(immutable: true) {
id: ID!
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
type MyEntity @entity {
type MyEntity @entity(immutable: true) {
id: ID!
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
type MyEntity @entity {
type MyEntity @entity(immutable: true) {
id: ID!
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
type MyEntity @entity {
type MyEntity @entity(immutable: true) {
id: ID!
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
type Foo @entity {
type Foo @entity(immutable: true) {
id: ID!
bars: [Bars!]! @derivedFrom(field: "foo")
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
type Gravatar @entity {
type Gravatar @entity(immutable: false) {
id: ID!
# This field points at an interface; we allow that a derived field
# in a type that implements the interface derives from this field
Expand All @@ -12,7 +12,7 @@ interface Account {
gravatars: [Gravatar!]! @derivedFrom(field: "owner")
}

type UserAccount implements Account @entity {
type UserAccount implements Account @entity(immutable: true) {
id: ID!
name: String!
gravatars: [Gravatar!]! @derivedFrom(field: "owner")
Expand Down
Loading