Skip to content
5 changes: 5 additions & 0 deletions .changeset/@graphprotocol_graph-cli-1340-dependencies.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@graphprotocol/graph-cli": patch
---
dependencies updates:
- Updated dependency [`@oclif/[email protected]` ↗︎](https://www.npmjs.com/package/@oclif/core/v/2.8.4) (from `2.8.2`, in `dependencies`)
6 changes: 6 additions & 0 deletions .changeset/rotten-lemons-pull.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@graphprotocol/graph-cli': minor
---

Add support for codegen for derived field loaders, This adds getters for derived fields defined in
the schema for entities.
19 changes: 3 additions & 16 deletions packages/cli/src/codegen/schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,7 @@ import * as graphql from 'graphql/language';
import prettier from 'prettier';
import Schema from '../schema';
import SchemaCodeGenerator from './schema';
import {
ArrayType,
Class,
Method,
NamedType,
NullableType,
Param,
StaticMethod,
} from './typescript';
import { Class, Method, NamedType, NullableType, Param, StaticMethod } from './typescript';

const formatTS = (code: string) => prettier.format(code, { parser: 'typescript', semi: false });

Expand Down Expand Up @@ -273,14 +265,9 @@ describe('Schema code generator', () => {
{
name: 'get wallets',
params: [],
returnType: new NullableType(new ArrayType(new NamedType('string'))),
returnType: new NamedType('WalletLoader'),
body: `
let value = this.get('wallets')
if (!value || value.kind == ValueKind.NULL) {
return null
} else {
return value.toStringArray()
}
return new WalletLoader("Account", this.get('id')!.toString(), "wallets")
`,
},
],
Expand Down
135 changes: 135 additions & 0 deletions packages/cli/src/codegen/schema.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/* eslint-disable unicorn/no-array-for-each */
import debug from 'debug';
import Schema from '../schema';
import * as typesCodegen from './types';
import * as tsCodegen from './typescript';
Expand Down Expand Up @@ -104,13 +105,36 @@ export default class SchemaCodeGenerator {
.filter(Boolean) as Array<tsCodegen.Class>;
}

generateDerivedLoaders() {
const fields = (
(
this.schema.ast.definitions.filter(def =>
this._isEntityTypeDefinition(def),
) as ObjectTypeDefinitionNode[]
)
.flatMap((def: ObjectTypeDefinitionNode) => def.fields)
.filter(def => this._isDerivedField(def))
.filter(def => def?.type !== undefined) as FieldDefinitionNode[]
).map(def => this._getTypeNameForField(def.type));

return [...new Set(fields)].map(typeName => {
return this._generateDerivedLoader(typeName);
});
}

_isEntityTypeDefinition(def: DefinitionNode): def is ObjectTypeDefinitionNode {
return (
def.kind === 'ObjectTypeDefinition' &&
def.directives?.find(directive => directive.name.value === 'entity') !== undefined
);
}

_isDerivedField(field: any): boolean {
return (
field.directives?.find((directive: any) => directive.name.value === 'derivedFrom') !==
undefined
);
}
_isInterfaceDefinition(def: DefinitionNode): def is InterfaceTypeDefinitionNode {
return def.kind === 'InterfaceTypeDefinition';
}
Expand Down Expand Up @@ -138,6 +162,61 @@ export default class SchemaCodeGenerator {
return klass;
}

_generateDerivedLoader(typeName: string): any {
// <field>Loader
const klass = tsCodegen.klass(`${typeName}Loader`, { export: true, extends: 'Entity' });

klass.addMember(tsCodegen.klassMember('_entity', 'string'));
klass.addMember(tsCodegen.klassMember('_field', 'string'));
klass.addMember(tsCodegen.klassMember('_id', 'string'));
// Generate and add a constructor
klass.addMethod(
tsCodegen.method(
'constructor',
[
tsCodegen.param('entity', 'string'),
tsCodegen.param('id', 'string'),
tsCodegen.param('field', 'string'),
],
undefined,
`
super();
this._entity = entity;
this._id = id;
this._field = field;
`,
),
);

// Generate load() method for the Loader
klass.addMethod(
tsCodegen.method(
'load',
[],
`${typeName}[]`,
`
let value = store.loadRelated(this._entity, this._id, this._field);
return changetype<${typeName}[]>(value);
`,
),
);

return klass;
}

_getTypeNameForField(gqlType: TypeNode): string {
if (gqlType.kind === 'NonNullType') {
return this._getTypeNameForField(gqlType.type);
}
if (gqlType.kind === 'ListType') {
return this._getTypeNameForField(gqlType.type);
}
if (gqlType.kind === 'NamedType') {
return (gqlType as NamedTypeNode).name.value;
}

throw new Error(`Unknown type kind: ${gqlType}`);
}
_generateConstructor(_entityName: string, fields: readonly FieldDefinitionNode[] | undefined) {
const idField = IdField.fromFields(fields);
return tsCodegen.method(
Expand Down Expand Up @@ -207,6 +286,13 @@ export default class SchemaCodeGenerator {
}

_generateEntityFieldGetter(_entityDef: ObjectTypeDefinitionNode, fieldDef: FieldDefinitionNode) {
const isDerivedField = this._isDerivedField(fieldDef);
const codegenDebug = debug('codegen');
if (isDerivedField) {
codegenDebug(`Generating derived field getter for ${fieldDef.name.value}`);
return this._generateDerivedFieldGetter(_entityDef, fieldDef);
}

const name = fieldDef.name.value;
const gqlType = fieldDef.type;
const fieldValueType = this._valueTypeFromGraphQl(gqlType);
Expand Down Expand Up @@ -240,7 +326,56 @@ export default class SchemaCodeGenerator {
`,
);
}
_generateDerivedFieldGetter(entityDef: any, fieldDef: FieldDefinitionNode) {
const entityName = entityDef.name.value;
const name = fieldDef.name.value;
const gqlType = fieldDef.type;
const returnType = this._returnTypeForDervied(gqlType);
return tsCodegen.method(
`get ${name}`,
[],
returnType,
`
return new ${returnType}('${entityName}', this.get('id')!.toString(), '${name}')
`,
);
}

_returnTypeForDervied(gqlType: any): any {
if (gqlType.kind === 'NonNullType') {
return this._returnTypeForDervied(gqlType.type);
}
if (gqlType.kind === 'ListType') {
return this._returnTypeForDervied(gqlType.type);
}
const type = tsCodegen.namedType(gqlType.name.value + 'Loader');
return type;
}

_generatedEntityDerivedFieldGetter(_entityDef: any, fieldDef: FieldDefinitionNode) {
const name = fieldDef.name.value;
const gqlType = fieldDef.type;
const fieldValueType = this._valueTypeFromGraphQl(gqlType);
const returnType = this._typeFromGraphQl(gqlType);
const isNullable = returnType instanceof tsCodegen.NullableType;

const getNonNullable = `return ${typesCodegen.valueToAsc('value!', fieldValueType)}`;
const getNullable = `if (!value || value.kind == ValueKind.NULL) {
return null
} else {
return ${typesCodegen.valueToAsc('value', fieldValueType)}
}`;

return tsCodegen.method(
`get ${name}`,
[],
returnType,
`
let value = this.get('${name}')
${isNullable ? getNullable : getNonNullable}
`,
);
}
_generateEntityFieldSetter(_entityDef: ObjectTypeDefinitionNode, fieldDef: FieldDefinitionNode) {
const name = fieldDef.name.value;
const isDerivedField = !!fieldDef.directives?.find(
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/type-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ export default class TypeGenerator {
GENERATED_FILE_NOTE,
...codeGenerator.generateModuleImports(),
...codeGenerator.generateTypes(),
...codeGenerator.generateDerivedLoaders(),
].join('\n'),
{
parser: 'typescript',
Expand Down
1 change: 1 addition & 0 deletions packages/ts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export declare namespace store {
/** If the entity was not created in the block, this function will return null. */
// Matches the host function https://github.com/graphprotocol/graph-node/blob/9f4a1821146b18f6f49165305e9a8c0795120fad/runtime/wasm/src/module/mod.rs#L1091-L1099
function get_in_block(entity: string, id: string): Entity | null;
function loadRelated(entity: string, id: string, field: string): Array<Entity>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you add a changeset for explaining the upgrade to graph-ts

function set(entity: string, id: string, data: Entity): void;
function remove(entity: string, id: string): void;
}
Expand Down