@graphprotocol/graph-cli
Version:
CLI for building for and deploying to The Graph
365 lines (364 loc) • 16.6 kB
JavaScript
import debug from '../debug.js';
import * as typesCodegen from './types/index.js';
import * as tsCodegen from './typescript.js';
import * as util from './util.js';
class IdField {
static BYTES = Symbol('Bytes');
static STRING = Symbol('String');
kind;
constructor(idField) {
if (idField?.type.kind !== 'NonNullType') {
throw Error('id field must be non-nullable');
}
if (idField.type.type.kind !== 'NamedType') {
throw Error('id field must be a named type');
}
const typeName = idField.type.type.name.value;
this.kind = typeName === 'Bytes' ? IdField.BYTES : IdField.STRING;
}
typeName() {
return this.kind === IdField.BYTES ? 'Bytes' : 'string';
}
gqlTypeName() {
return this.kind === IdField.BYTES ? 'Bytes' : 'String';
}
tsNamedType() {
return tsCodegen.namedType(this.typeName());
}
tsValueFrom() {
return this.kind === IdField.BYTES ? 'Value.fromBytes(id)' : 'Value.fromString(id)';
}
tsValueKind() {
return this.kind === IdField.BYTES ? 'ValueKind.BYTES' : 'ValueKind.STRING';
}
tsValueToString() {
return this.kind == IdField.BYTES ? 'id.toBytes().toHexString()' : 'id.toString()';
}
tsToString() {
return this.kind == IdField.BYTES ? 'id.toHexString()' : 'id';
}
static fromFields(fields) {
const idField = fields?.find(field => field.name.value === 'id');
return new IdField(idField);
}
static fromTypeDef(def) {
return IdField.fromFields(def.fields);
}
}
const schemaCodeGeneratorDebug = debug('graph-cli:SchemaCodeGenerator');
export default class SchemaCodeGenerator {
schema;
constructor(schema) {
this.schema = schema;
this.schema = schema;
}
generateModuleImports() {
return [
tsCodegen.moduleImports([
// Base classes
'TypedMap',
'Entity',
'Value',
'ValueKind',
// APIs
'store',
// Basic Scalar types
'Bytes',
'BigInt',
'BigDecimal',
], '@graphprotocol/graph-ts'),
];
}
generateTypes(generateStoreMethods = true) {
return this.schema.ast.definitions
.map(def => {
if (this._isEntityTypeDefinition(def)) {
schemaCodeGeneratorDebug.extend('generateTypes')(`Generating entity type for ${def.name.value}`);
return this._generateEntityType(def, generateStoreMethods);
}
})
.filter(Boolean);
}
generateDerivedLoaders() {
// This gets all the interfaces in the schema
// We can think of more optimized ways to do this
const interfaces = this.schema.ast.definitions.filter(def => this._isInterfaceDefinition(def)).map(def => def.name.value);
const fields = this.schema.ast.definitions.filter(def => {
return this._isEntityTypeDefinition(def);
})
.flatMap((def) => def.fields)
.filter(def => this._isDerivedField(def))
.filter(def => def?.type !== undefined).map(def => this._getTypeNameForField(def.type));
schemaCodeGeneratorDebug.extend('generateDerivedLoaders')(`Generating derived loaders for ${fields}`);
return [...new Set(fields)].map(typeName => {
// do not support interfaces
if (!interfaces.includes(typeName)) {
return this._generateDerivedLoader(typeName);
}
});
}
_isEntityTypeDefinition(def) {
return (def.kind === 'ObjectTypeDefinition' &&
def.directives?.find(directive => directive.name.value === 'entity') !== undefined);
}
_isDerivedField(field) {
return (field?.directives?.find(directive => directive.name.value === 'derivedFrom') !== undefined);
}
_isInterfaceDefinition(def) {
return def.kind === 'InterfaceTypeDefinition';
}
_generateEntityType(def, generateStoreMethods = true) {
const name = def.name.value;
const klass = tsCodegen.klass(name, { export: true, extends: 'Entity' });
const fields = def.fields;
const idField = IdField.fromFields(fields);
// Generate and add a constructor
klass.addMethod(this._generateConstructor(name, fields));
if (generateStoreMethods) {
// Generate and add save() and getById() methods
this._generateStoreMethods(name, idField).forEach(method => klass.addMethod(method));
}
// Generate and add entity field getters and setters
def.fields
?.reduce((methods, field) => methods.concat(this._generateEntityFieldMethods(def, field)), [])
.forEach((method) => klass.addMethod(method));
return klass;
}
_generateDerivedLoader(typeName) {
// <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) {
if (gqlType.kind === 'NonNullType') {
return this._getTypeNameForField(gqlType.type);
}
if (gqlType.kind === 'ListType') {
return this._getTypeNameForField(gqlType.type);
}
if (gqlType.kind === 'NamedType') {
return gqlType.name.value;
}
throw new Error(`Unknown type kind: ${gqlType}`);
}
_generateConstructor(_entityName, fields) {
const idField = IdField.fromFields(fields);
return tsCodegen.method('constructor', [tsCodegen.param('id', idField.tsNamedType())], undefined, `
super()
this.set('id', ${idField.tsValueFrom()})
`);
}
_generateStoreMethods(entityName, idField) {
return [
tsCodegen.method('save', [], tsCodegen.namedType('void'), `
let id = this.get('id')
assert(id != null,
'Cannot save ${entityName} entity without an ID')
if (id) {
assert(id.kind == ${idField.tsValueKind()},
\`Entities of type ${entityName} must have an ID of type ${idField.gqlTypeName()} but the id '\${id.displayData()}' is of type \${id.displayKind()}\`)
store.set('${entityName}', ${idField.tsValueToString()}, this)
}`),
tsCodegen.staticMethod('loadInBlock', [tsCodegen.param('id', tsCodegen.namedType(idField.typeName()))], tsCodegen.nullableType(tsCodegen.namedType(entityName)), `
return changetype<${entityName} | null>(store.get_in_block('${entityName}', ${idField.tsToString()}))
`),
tsCodegen.staticMethod('load', [tsCodegen.param('id', tsCodegen.namedType(idField.typeName()))], tsCodegen.nullableType(tsCodegen.namedType(entityName)), `
return changetype<${entityName} | null>(store.get('${entityName}', ${idField.tsToString()}))
`),
];
}
_generateEntityFieldMethods(entityDef, fieldDef) {
return [
this._generateEntityFieldGetter(entityDef, fieldDef),
this._generateEntityFieldSetter(entityDef, fieldDef),
]
// generator can return null if the field is not supported
// so we filter all falsy values
.filter(Boolean);
}
_generateEntityFieldGetter(_entityDef, fieldDef) {
const isDerivedField = this._isDerivedField(fieldDef);
const name = fieldDef.name.value;
const safeName = util.handleReservedWord(name);
if (isDerivedField) {
schemaCodeGeneratorDebug.extend('_generateEntityFieldGetter')(`Generating derived field getter for ${name}`);
return this._generateDerivedFieldGetter(_entityDef, fieldDef);
}
const gqlType = fieldDef.type;
const fieldValueType = this._valueTypeFromGraphQl(gqlType);
const returnType = this._typeFromGraphQl(gqlType);
const isNullable = returnType instanceof tsCodegen.NullableType;
const primitiveDefault = returnType instanceof tsCodegen.NamedType ? returnType.getPrimitiveDefault() : null;
const getNonNullable = `if (!value || value.kind == ValueKind.NULL) {
${primitiveDefault === null
? "throw new Error('Cannot return null for a required field.')"
: `return ${primitiveDefault}`}
} else {
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 ${safeName}`, [], returnType, `
let value = this.get('${name}')
${isNullable ? getNullable : getNonNullable}
`);
}
_generateDerivedFieldGetter(entityDef, fieldDef) {
const entityName = entityDef.name.value;
const name = fieldDef.name.value;
const safeName = util.handleReservedWord(name);
schemaCodeGeneratorDebug.extend('_generateDerivedFieldGetter')(`Generating derived field '${name}' getter for Entity '${entityName}'`);
const gqlType = fieldDef.type;
schemaCodeGeneratorDebug.extend('_generateDerivedFieldGetter')("Derived field's type: %M", gqlType);
const returnType = this._returnTypeForDervied(gqlType);
schemaCodeGeneratorDebug.extend('_generateDerivedFieldGetter')("Derived field's return type: %M", returnType);
const obj = this.schema.ast.definitions.find(def => {
if (def.kind === 'ObjectTypeDefinition') {
const defobj = def;
return defobj.name.value == this._baseType(gqlType);
}
return false;
});
if (!obj) {
schemaCodeGeneratorDebug.extend('_generateDerivedFieldGetter')("Could not find object type definition for derived field's base type: %M", obj);
return null;
}
schemaCodeGeneratorDebug.extend('_generateDerivedFieldGetter')("Found object type definition for derived field's base type: %M", obj);
const idf = IdField.fromTypeDef(entityDef);
const idIsBytes = idf.typeName() == 'Bytes';
const toValueString = idIsBytes ? '.toBytes().toHexString()' : '.toString()';
return tsCodegen.method(`get ${safeName}`, [], returnType, `
return new ${returnType}('${entityName}', this.get('id')!${toValueString}, '${name}')
`);
}
_returnTypeForDervied(gqlType) {
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, fieldDef) {
const name = fieldDef.name.value;
const safeName = util.handleReservedWord(name);
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 ${safeName}`, [], returnType, `
let value = this.get('${name}')
${isNullable ? getNullable : getNonNullable}
`);
}
_generateEntityFieldSetter(_entityDef, fieldDef) {
const name = fieldDef.name.value;
const safeName = util.handleReservedWord(name);
const isDerivedField = !!fieldDef.directives?.find(directive => directive.name.value === 'derivedFrom');
// We cannot have setters for derived fields
if (isDerivedField)
return null;
const gqlType = fieldDef.type;
const fieldValueType = this._valueTypeFromGraphQl(gqlType);
const paramType = this._typeFromGraphQl(gqlType);
const isNullable = paramType instanceof tsCodegen.NullableType;
const paramTypeString = isNullable ? paramType.inner.toString() : paramType.toString();
const isArray = paramType instanceof tsCodegen.ArrayType;
if (isArray && paramType.inner instanceof tsCodegen.NullableType) {
const baseType = this._baseType(gqlType);
throw new Error(`
GraphQL schema can't have List's with Nullable members.
Error in '${name}' field of type '[${baseType}]'.
Suggestion: add an '!' to the member type of the List, change from '[${baseType}]' to '[${baseType}!]'`);
}
const setNonNullable = `
this.set('${name}', ${typesCodegen.valueFromAsc(`value`, fieldValueType)})
`;
const setNullable = `
if (!value) {
this.unset('${name}')
} else {
this.set('${name}', ${typesCodegen.valueFromAsc(`<${paramTypeString}>value`, fieldValueType)})
}
`;
return tsCodegen.method(`set ${safeName}`, [tsCodegen.param('value', paramType)], undefined, isNullable ? setNullable : setNonNullable);
}
_resolveFieldType(gqlType) {
const typeName = gqlType.name.value;
// If this is a reference to another type, the field has the type of
// the referred type's id field
const typeDef = this.schema.ast.definitions?.find(def => (this._isEntityTypeDefinition(def) || this._isInterfaceDefinition(def)) &&
def.name.value === typeName);
if (typeDef) {
return IdField.fromTypeDef(typeDef).typeName();
}
return typeName;
}
/** Return the type that values for this field must have. For scalar
* types, that's the type from the subgraph schema. For references to
* other entity types, this is the same as the type of the id of the
* referred type, i.e., `string` or `Bytes`*/
_valueTypeFromGraphQl(gqlType) {
if (gqlType.kind === 'NonNullType') {
return this._valueTypeFromGraphQl(gqlType.type);
}
if (gqlType.kind === 'ListType') {
return '[' + this._valueTypeFromGraphQl(gqlType.type) + ']';
}
return this._resolveFieldType(gqlType);
}
/** Determine the base type of `gqlType` by removing any non-null
* constraints and using the type of elements of lists */
_baseType(gqlType) {
if (gqlType.kind === 'NonNullType') {
return this._baseType(gqlType.type);
}
if (gqlType.kind === 'ListType') {
return this._baseType(gqlType.type);
}
return gqlType.name.value;
}
_typeFromGraphQl(gqlType, nullable = true) {
if (gqlType.kind === 'NonNullType') {
return this._typeFromGraphQl(gqlType.type, false);
}
if (gqlType.kind === 'ListType') {
const type = tsCodegen.arrayType(this._typeFromGraphQl(gqlType.type));
return nullable ? tsCodegen.nullableType(type) : type;
}
// NamedType
const type = tsCodegen.namedType(typesCodegen.ascTypeForValue(this._resolveFieldType(gqlType)));
// In AssemblyScript, primitives cannot be nullable.
return nullable && !type.isPrimitive() ? tsCodegen.nullableType(type) : type;
}
}