diff --git a/.changeset/big-hornets-relate.md b/.changeset/big-hornets-relate.md new file mode 100644 index 000000000..39cfaf269 --- /dev/null +++ b/.changeset/big-hornets-relate.md @@ -0,0 +1,5 @@ +--- +"@graphprotocol/graph-ts": minor +--- + +export `loadRelated` host function diff --git a/.changeset/rotten-lemons-pull.md b/.changeset/rotten-lemons-pull.md new file mode 100644 index 000000000..5ebd6be2f --- /dev/null +++ b/.changeset/rotten-lemons-pull.md @@ -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. diff --git a/packages/cli/src/codegen/schema.test.ts b/packages/cli/src/codegen/schema.test.ts index 23542864c..e68034761 100644 --- a/packages/cli/src/codegen/schema.test.ts +++ b/packages/cli/src/codegen/schema.test.ts @@ -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 }); @@ -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") `, }, ], diff --git a/packages/cli/src/codegen/schema.ts b/packages/cli/src/codegen/schema.ts index 5fe7f5bd9..d7368d058 100644 --- a/packages/cli/src/codegen/schema.ts +++ b/packages/cli/src/codegen/schema.ts @@ -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'; @@ -104,6 +105,23 @@ export default class SchemaCodeGenerator { .filter(Boolean) as Array; } + 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' && @@ -111,6 +129,12 @@ export default class SchemaCodeGenerator { ); } + _isDerivedField(field: FieldDefinitionNode | undefined): boolean { + return ( + field?.directives?.find((directive: any) => directive.name.value === 'derivedFrom') !== + undefined + ); + } _isInterfaceDefinition(def: DefinitionNode): def is InterfaceTypeDefinitionNode { return def.kind === 'InterfaceTypeDefinition'; } @@ -138,6 +162,61 @@ export default class SchemaCodeGenerator { return klass; } + _generateDerivedLoader(typeName: string): any { + // 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( @@ -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); @@ -240,7 +326,59 @@ export default class SchemaCodeGenerator { `, ); } + _generateDerivedFieldGetter(entityDef: ObjectTypeDefinitionNode, 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: TypeNode): tsCodegen.NamedType { + 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: ObjectTypeDefinitionNode, + 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( diff --git a/packages/cli/src/type-generator.ts b/packages/cli/src/type-generator.ts index 0f0335873..50ec0d5af 100644 --- a/packages/cli/src/type-generator.ts +++ b/packages/cli/src/type-generator.ts @@ -161,6 +161,7 @@ export default class TypeGenerator { GENERATED_FILE_NOTE, ...codeGenerator.generateModuleImports(), ...codeGenerator.generateTypes(), + ...codeGenerator.generateDerivedLoaders(), ].join('\n'), { parser: 'typescript', diff --git a/packages/ts/index.ts b/packages/ts/index.ts index e393717e4..0e9507b6d 100644 --- a/packages/ts/index.ts +++ b/packages/ts/index.ts @@ -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; function set(entity: string, id: string, data: Entity): void; function remove(entity: string, id: string): void; }