UNPKG

@graphprotocol/graph-cli

Version:

CLI for building for and deploying to The Graph

287 lines (246 loc) • 8.46 kB
let fs = require('fs-extra') let path = require('path') let yaml = require('yaml') let { strOptions } = require('yaml/types') let graphql = require('graphql/language') let validation = require('./validation') let subgraphDebug = require('./debug')('graph-cli:subgraph') const throwCombinedError = (filename, errors) => { throw new Error( errors.reduce( (msg, e) => `${msg} Path: ${e.get('path').size === 0 ? '/' : e.get('path').join(' > ')} ${e .get('message') .split('\n') .join('\n ')}`, `Error in ${path.relative(process.cwd(), filename)}:`, ), ) } const buildCombinedWarning = (filename, warnings) => warnings.size > 0 ? warnings.reduce( (msg, w) => `${msg} Path: ${w.get('path').size === 0 ? '/' : w.get('path').join(' > ')} ${w .get('message') .split('\n') .join('\n ')}`, `Warnings in ${path.relative(process.cwd(), filename)}:`, ) + '\n' : null module.exports = class Subgraph { static async validate(data, protocol, { resolveFile }) { subgraphDebug(`Validating Subgraph with protocol "%s"`, protocol) if (protocol.name == null) { return [ { path: [], message: `Unable to determine for which protocol manifest file is built for. Ensure you have at least one 'dataSources' and/or 'templates' elements defined in your subgraph.`, }, ] } // Parse the default subgraph schema let schema = graphql.parse( await fs.readFile( path.join(__dirname, 'protocols', protocol.name, `manifest.graphql`), 'utf-8', ), ) // Obtain the root `SubgraphManifest` type from the schema let rootType = schema.definitions.find(definition => { return definition.name.value === 'SubgraphManifest' }) // Validate the subgraph manifest using this schema return validation.validateManifest(data, rootType, schema, protocol, { resolveFile }) } static validateSchema(manifest, { resolveFile }) { let filename = resolveFile(manifest.schema?.file) let errors = validation.validateSchema(filename) if (errors.size > 0) { errors = errors.groupBy(error => error.get('entity')).sort() let msg = errors.reduce((msg, errors, entity) => { errors = errors.groupBy(error => error.get('directive')) let inner_msgs = errors.reduce((msg, errors, directive) => { return `${msg}${ directive ? ` ${directive}:` : '' } ${errors .map(error => error .get('message') .split('\n') .join('\n '), ) .map(msg => `${directive ? ' ' : ''}- ${msg}`) .join('\n ')}` }, ``) return `${msg} ${entity}:${inner_msgs}` }, `Error in ${path.relative(process.cwd(), filename)}:`) throw new Error(msg) } } static validateRepository(manifest, { resolveFile }) { const repository = manifest.get('repository') return /^https:\/\/github\.com\/graphprotocol\/example-subgraphs?$/.test(repository) ? [ { path: ['repository'], message: `\ The repository is still set to ${repository}. Please replace it with a link to your subgraph source code.`, }, ] : [] } static validateDescription(manifest, { resolveFile }) { // TODO: Maybe implement this in the future for each protocol example description return manifest.get('description', '').startsWith('Gravatar for ') ? [ { path: ['description'], message: `\ The description is still the one from the example subgraph. Please update it to tell users more about your subgraph.`, }, ] : [] } static validateHandlers(manifest, protocol, protocolSubgraph) { return manifest .get('dataSources') .filter(dataSource => protocol.isValidKindName(dataSource.get('kind'))) .reduce((errors, dataSource, dataSourceIndex) => { let path = ['dataSources', dataSourceIndex, 'mapping'] let mapping = dataSource.get('mapping') const handlerTypes = protocolSubgraph.handlerTypes() subgraphDebug( 'Validating dataSource "%s" handlers with %d handlers types defined for protocol', dataSource.get('name'), handlerTypes.size, ) if (handlerTypes.size == 0) { return errors } const areAllHandlersEmpty = handlerTypes .map(handlerType => mapping.get(handlerType, [])) .every(handlers => handlers.isEmpty()) const handlerNamesWithoutLast = handlerTypes.pop().join(', ') return areAllHandlersEmpty ? errors.push({ path: path, message: `\ Mapping has no ${handlerNamesWithoutLast} or ${handlerTypes.get(-1)}. At least one such handler must be defined.`, }) : errors }, []) } static validateContractValues(manifest, protocol) { if (!protocol.hasContract()) { return [] } return validation.validateContractValues(manifest, protocol) } // Validate that data source names are unique, so they don't overwrite each other. static validateUniqueDataSourceNames(manifest) { let names = [] return manifest.get('dataSources').reduce((errors, dataSource, dataSourceIndex) => { let path = ['dataSources', dataSourceIndex, 'name'] let name = dataSource.get('name') if (names.includes(name)) { errors = errors.push({ path, message: `\ More than one data source named '${name}', data source names must be unique.`, }) } names.push(name) return errors }, []) } static validateUniqueTemplateNames(manifest) { let names = [] return manifest.get('templates', []).reduce((errors, template, templateIndex) => { let path = ['templates', templateIndex, 'name'] let name = template.get('name') if (names.includes(name)) { errors = errors.push({ path, message: `\ More than one template named '${name}', template names must be unique.`, }) } names.push(name) return errors }, []) } static dump(manifest) { strOptions.fold.lineWidth = 90 strOptions.defaultType = 'PLAIN' return yaml.stringify(manifest.toJS()) } static async load(filename, { protocol, skipValidation } = { skipValidation: false }) { // Load and validate the manifest let data = null let has_file_data_sources = false if (filename.match(/.js$/)) { data = require(path.resolve(filename)) } else { let raw_data = await fs.readFile(filename, 'utf-8') has_file_data_sources = raw_data.includes('kind: file') data = yaml.parse(raw_data) } // Helper to resolve files relative to the subgraph manifest let resolveFile = maybeRelativeFile => path.resolve(path.dirname(filename), maybeRelativeFile) // TODO: Validation for file data sources if (!has_file_data_sources) { let manifestErrors = await Subgraph.validate(data, protocol, { resolveFile }) if (manifestErrors.size > 0) { throwCombinedError(filename, manifestErrors) } } let manifest = data // Validate the schema Subgraph.validateSchema(manifest, { resolveFile }) // Perform other validations const protocolSubgraph = protocol.getSubgraph({ manifest, resolveFile, }) let errors = skipValidation ? [] : [ ...protocolSubgraph.validateManifest(), ...Subgraph.validateContractValues(manifest, protocol), ...Subgraph.validateUniqueDataSourceNames(manifest), ...Subgraph.validateUniqueTemplateNames(manifest), ...Subgraph.validateHandlers(manifest, protocol, protocolSubgraph), ] if (errors.size > 0) { throwCombinedError(filename, errors) } // Perform warning validations let warnings = skipValidation ? [] : [ ...Subgraph.validateRepository(manifest, { resolveFile }), ...Subgraph.validateDescription(manifest, { resolveFile }), ] return { result: manifest, warning: warnings.size > 0 ? buildCombinedWarning(filename, warnings) : null, } } static async write(manifest, filename) { await fs.writeFile(filename, Subgraph.dump(manifest)) } }