@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;
    }
}