Skip to content

Commit 2375877

Browse files
incrypto32gusinaciogithub-actions[bot]saihaj
authored
Derived Loader Fixes (#1340)
* cli: add derived loader * Fix wrong generated type for derived loaders * Run prettier * Refactoring * Generate derived field getter * Remove usage of any, add changeset * chore(dependencies): updated changesets for modified dependencies * change any to concrete types * chore(dependencies): updated changesets for modified dependencies * Create big-hornets-relate.md --------- Co-authored-by: Gustavo Inacio <[email protected]> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Saihajpreet Singh <[email protected]>
1 parent 95c77fd commit 2375877

File tree

6 files changed

+154
-16
lines changed

6 files changed

+154
-16
lines changed

.changeset/big-hornets-relate.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@graphprotocol/graph-ts": minor
3+
---
4+
5+
export `loadRelated` host function

.changeset/rotten-lemons-pull.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@graphprotocol/graph-cli': minor
3+
---
4+
5+
Add support for codegen for derived field loaders, This adds getters for derived fields defined in
6+
the schema for entities.

packages/cli/src/codegen/schema.test.ts

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,7 @@ import * as graphql from 'graphql/language';
22
import prettier from 'prettier';
33
import Schema from '../schema';
44
import SchemaCodeGenerator from './schema';
5-
import {
6-
ArrayType,
7-
Class,
8-
Method,
9-
NamedType,
10-
NullableType,
11-
Param,
12-
StaticMethod,
13-
} from './typescript';
5+
import { Class, Method, NamedType, NullableType, Param, StaticMethod } from './typescript';
146

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

@@ -273,14 +265,9 @@ describe('Schema code generator', () => {
273265
{
274266
name: 'get wallets',
275267
params: [],
276-
returnType: new NullableType(new ArrayType(new NamedType('string'))),
268+
returnType: new NamedType('WalletLoader'),
277269
body: `
278-
let value = this.get('wallets')
279-
if (!value || value.kind == ValueKind.NULL) {
280-
return null
281-
} else {
282-
return value.toStringArray()
283-
}
270+
return new WalletLoader("Account", this.get('id')!.toString(), "wallets")
284271
`,
285272
},
286273
],

packages/cli/src/codegen/schema.ts

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
/* eslint-disable unicorn/no-array-for-each */
2+
import debug from 'debug';
23
import Schema from '../schema';
34
import * as typesCodegen from './types';
45
import * as tsCodegen from './typescript';
@@ -104,13 +105,36 @@ export default class SchemaCodeGenerator {
104105
.filter(Boolean) as Array<tsCodegen.Class>;
105106
}
106107

108+
generateDerivedLoaders() {
109+
const fields = (
110+
(
111+
this.schema.ast.definitions.filter(def =>
112+
this._isEntityTypeDefinition(def),
113+
) as ObjectTypeDefinitionNode[]
114+
)
115+
.flatMap((def: ObjectTypeDefinitionNode) => def.fields)
116+
.filter(def => this._isDerivedField(def))
117+
.filter(def => def?.type !== undefined) as FieldDefinitionNode[]
118+
).map(def => this._getTypeNameForField(def.type));
119+
120+
return [...new Set(fields)].map(typeName => {
121+
return this._generateDerivedLoader(typeName);
122+
});
123+
}
124+
107125
_isEntityTypeDefinition(def: DefinitionNode): def is ObjectTypeDefinitionNode {
108126
return (
109127
def.kind === 'ObjectTypeDefinition' &&
110128
def.directives?.find(directive => directive.name.value === 'entity') !== undefined
111129
);
112130
}
113131

132+
_isDerivedField(field: FieldDefinitionNode | undefined): boolean {
133+
return (
134+
field?.directives?.find((directive: any) => directive.name.value === 'derivedFrom') !==
135+
undefined
136+
);
137+
}
114138
_isInterfaceDefinition(def: DefinitionNode): def is InterfaceTypeDefinitionNode {
115139
return def.kind === 'InterfaceTypeDefinition';
116140
}
@@ -138,6 +162,61 @@ export default class SchemaCodeGenerator {
138162
return klass;
139163
}
140164

165+
_generateDerivedLoader(typeName: string): any {
166+
// <field>Loader
167+
const klass = tsCodegen.klass(`${typeName}Loader`, { export: true, extends: 'Entity' });
168+
169+
klass.addMember(tsCodegen.klassMember('_entity', 'string'));
170+
klass.addMember(tsCodegen.klassMember('_field', 'string'));
171+
klass.addMember(tsCodegen.klassMember('_id', 'string'));
172+
// Generate and add a constructor
173+
klass.addMethod(
174+
tsCodegen.method(
175+
'constructor',
176+
[
177+
tsCodegen.param('entity', 'string'),
178+
tsCodegen.param('id', 'string'),
179+
tsCodegen.param('field', 'string'),
180+
],
181+
undefined,
182+
`
183+
super();
184+
this._entity = entity;
185+
this._id = id;
186+
this._field = field;
187+
`,
188+
),
189+
);
190+
191+
// Generate load() method for the Loader
192+
klass.addMethod(
193+
tsCodegen.method(
194+
'load',
195+
[],
196+
`${typeName}[]`,
197+
`
198+
let value = store.loadRelated(this._entity, this._id, this._field);
199+
return changetype<${typeName}[]>(value);
200+
`,
201+
),
202+
);
203+
204+
return klass;
205+
}
206+
207+
_getTypeNameForField(gqlType: TypeNode): string {
208+
if (gqlType.kind === 'NonNullType') {
209+
return this._getTypeNameForField(gqlType.type);
210+
}
211+
if (gqlType.kind === 'ListType') {
212+
return this._getTypeNameForField(gqlType.type);
213+
}
214+
if (gqlType.kind === 'NamedType') {
215+
return (gqlType as NamedTypeNode).name.value;
216+
}
217+
218+
throw new Error(`Unknown type kind: ${gqlType}`);
219+
}
141220
_generateConstructor(_entityName: string, fields: readonly FieldDefinitionNode[] | undefined) {
142221
const idField = IdField.fromFields(fields);
143222
return tsCodegen.method(
@@ -207,6 +286,13 @@ export default class SchemaCodeGenerator {
207286
}
208287

209288
_generateEntityFieldGetter(_entityDef: ObjectTypeDefinitionNode, fieldDef: FieldDefinitionNode) {
289+
const isDerivedField = this._isDerivedField(fieldDef);
290+
const codegenDebug = debug('codegen');
291+
if (isDerivedField) {
292+
codegenDebug(`Generating derived field getter for ${fieldDef.name.value}`);
293+
return this._generateDerivedFieldGetter(_entityDef, fieldDef);
294+
}
295+
210296
const name = fieldDef.name.value;
211297
const gqlType = fieldDef.type;
212298
const fieldValueType = this._valueTypeFromGraphQl(gqlType);
@@ -240,7 +326,59 @@ export default class SchemaCodeGenerator {
240326
`,
241327
);
242328
}
329+
_generateDerivedFieldGetter(entityDef: ObjectTypeDefinitionNode, fieldDef: FieldDefinitionNode) {
330+
const entityName = entityDef.name.value;
331+
const name = fieldDef.name.value;
332+
const gqlType = fieldDef.type;
333+
const returnType = this._returnTypeForDervied(gqlType);
334+
return tsCodegen.method(
335+
`get ${name}`,
336+
[],
337+
returnType,
338+
`
339+
return new ${returnType}('${entityName}', this.get('id')!.toString(), '${name}')
340+
`,
341+
);
342+
}
343+
344+
_returnTypeForDervied(gqlType: TypeNode): tsCodegen.NamedType {
345+
if (gqlType.kind === 'NonNullType') {
346+
return this._returnTypeForDervied(gqlType.type);
347+
}
348+
if (gqlType.kind === 'ListType') {
349+
return this._returnTypeForDervied(gqlType.type);
350+
}
351+
const type = tsCodegen.namedType(gqlType.name.value + 'Loader');
352+
return type;
353+
}
354+
355+
_generatedEntityDerivedFieldGetter(
356+
_entityDef: ObjectTypeDefinitionNode,
357+
fieldDef: FieldDefinitionNode,
358+
) {
359+
const name = fieldDef.name.value;
360+
const gqlType = fieldDef.type;
361+
const fieldValueType = this._valueTypeFromGraphQl(gqlType);
362+
const returnType = this._typeFromGraphQl(gqlType);
363+
const isNullable = returnType instanceof tsCodegen.NullableType;
243364

365+
const getNonNullable = `return ${typesCodegen.valueToAsc('value!', fieldValueType)}`;
366+
const getNullable = `if (!value || value.kind == ValueKind.NULL) {
367+
return null
368+
} else {
369+
return ${typesCodegen.valueToAsc('value', fieldValueType)}
370+
}`;
371+
372+
return tsCodegen.method(
373+
`get ${name}`,
374+
[],
375+
returnType,
376+
`
377+
let value = this.get('${name}')
378+
${isNullable ? getNullable : getNonNullable}
379+
`,
380+
);
381+
}
244382
_generateEntityFieldSetter(_entityDef: ObjectTypeDefinitionNode, fieldDef: FieldDefinitionNode) {
245383
const name = fieldDef.name.value;
246384
const isDerivedField = !!fieldDef.directives?.find(

packages/cli/src/type-generator.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ export default class TypeGenerator {
161161
GENERATED_FILE_NOTE,
162162
...codeGenerator.generateModuleImports(),
163163
...codeGenerator.generateTypes(),
164+
...codeGenerator.generateDerivedLoaders(),
164165
].join('\n'),
165166
{
166167
parser: 'typescript',

packages/ts/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export declare namespace store {
2626
/** If the entity was not created in the block, this function will return null. */
2727
// Matches the host function https://github.com/graphprotocol/graph-node/blob/9f4a1821146b18f6f49165305e9a8c0795120fad/runtime/wasm/src/module/mod.rs#L1091-L1099
2828
function get_in_block(entity: string, id: string): Entity | null;
29+
function loadRelated(entity: string, id: string, field: string): Array<Entity>;
2930
function set(entity: string, id: string, data: Entity): void;
3031
function remove(entity: string, id: string): void;
3132
}

0 commit comments

Comments
 (0)