UNPKG

@graphprotocol/graph-cli

Version:

CLI for building for and deploying to The Graph

240 lines (236 loc) • 11.6 kB
import path from 'node:path'; import { fileURLToPath } from 'node:url'; import fs from 'fs-extra'; import * as graphql from 'graphql/language/index.js'; import immutable from 'immutable'; import yaml, { Scalar } from 'yaml'; import debug from './debug.js'; import * as validation from './validation/index.js'; const subgraphDebug = 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; export default class Subgraph { static async validate(data, protocol, { resolveFile }) { subgraphDebug.extend('validate')('Validating Subgraph with protocol %M', protocol); if (protocol.name == null) { subgraphDebug.extend('validate')('Protocol has no name, skipping validation'); return immutable.fromJS([ { 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 const schema = graphql.parse(await fs.readFile(path.join(fileURLToPath(import.meta.url), '..', 'protocols', // TODO: substreams/triggers is a special case, should be handled better protocol.name === 'substreams/triggers' ? 'substreams' : protocol.name, `manifest.graphql`), 'utf-8')); // Obtain the root `SubgraphManifest` type from the schema const rootType = schema.definitions.find(definition => { // @ts-expect-error TODO: name field does not exist on definition, really? return definition.name.value === 'SubgraphManifest'; }); // Validate the subgraph manifest using this schema return validation.validateManifest(data, rootType, schema, protocol, { resolveFile, }); } static validateSchema(manifest, { resolveFile }) { subgraphDebug.extend('validate')('Validating schema in manifest'); const filename = resolveFile(manifest.getIn(['schema', 'file'])); subgraphDebug.extend('validate')('Loaded schema from %s', filename); const validationErrors = validation.validateSchema(filename); let errors; if (validationErrors.size > 0) { subgraphDebug.extend('validate')('Schema validation failed for %s', filename); errors = validationErrors.groupBy(error => error.get('entity')).sort(); const msg = errors.reduce((msg, errors, entity) => { errors = errors.groupBy((error) => error.get('directive')); const 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) { subgraphDebug.extend('validate')('Validating repository in manifest'); const repository = manifest.get('repository'); return /^https:\/\/github\.com\/graphprotocol\/graph-tooling?$/.test(repository) || // For legacy reasons, we should error on example subgraphs /^https:\/\/github\.com\/graphprotocol\/example-subgraphs?$/.test(repository) ? immutable.List().push(immutable.fromJS({ path: ['repository'], message: `\ The repository is still set to ${repository}. Please replace it with a link to your subgraph source code.`, })) : immutable.List(); } static validateDescription(manifest) { subgraphDebug.extend('validate')('Validating description in manifest'); // TODO: Maybe implement this in the future for each protocol example description return manifest.get('description', '').startsWith('Gravatar for ') ? immutable.List().push(immutable.fromJS({ path: ['description'], message: `\ The description is still the one from the example subgraph. Please update it to tell users more about your subgraph.`, })) : immutable.List(); } static validateHandlers(manifest, protocol, protocolSubgraph) { subgraphDebug.extend('validate')('Validating handlers for protocol %s', protocol?.name); return manifest .get('dataSources') .filter((dataSource) => protocol.isValidKindName(dataSource.get('kind'))) .reduce((errors, dataSource, dataSourceIndex) => { const path = ['dataSources', dataSourceIndex, 'mapping']; const mapping = dataSource.get('mapping'); const handlerTypes = protocolSubgraph.handlerTypes(); subgraphDebug.extend('validate')('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, immutable.List())) .every((handlers) => handlers.isEmpty()); const handlerNamesWithoutLast = handlerTypes.pop().join(', '); return areAllHandlersEmpty ? errors.push(immutable.fromJS({ path, message: `\ Mapping has no ${handlerNamesWithoutLast} or ${handlerTypes.get(-1)}. At least one such handler must be defined.`, })) : errors; }, immutable.List()); } static validateContractValues(manifest, protocol) { subgraphDebug.extend('validate')('Validating contract values for protocol %s', protocol?.name); if (!protocol.hasContract()) { subgraphDebug.extend('validate')('Protocol has no contract, skipping validation'); return immutable.List(); } return validation.validateContractValues(manifest, protocol); } // Validate that data source names are unique, so they don't overwrite each other. static validateUniqueDataSourceNames(manifest) { subgraphDebug.extend('validate')('Validating that data source names are unique'); const names = []; return manifest .get('dataSources') .reduce((errors, dataSource, dataSourceIndex) => { const path = ['dataSources', dataSourceIndex, 'name']; const name = dataSource.get('name'); if (names.includes(name)) { subgraphDebug.extend('validate')("Found duplicate data source name '%s', adding error"); errors = errors.push(immutable.fromJS({ path, message: `\ More than one data source named '${name}', data source names must be unique.`, })); } names.push(name); return errors; }, immutable.List()); } static validateUniqueTemplateNames(manifest) { subgraphDebug.extend('validate')('Validating that template names are unique'); const names = []; return manifest .get('templates', immutable.List()) .reduce((errors, template, templateIndex) => { const path = ['templates', templateIndex, 'name']; const name = template.get('name'); if (names.includes(name)) { subgraphDebug.extend('validate')("Found duplicate template name '%s', adding error"); errors = errors.push(immutable.fromJS({ path, message: `\ More than one template named '${name}', template names must be unique.`, })); } names.push(name); return errors; }, immutable.List()); } static dump(manifest) { return yaml.stringify(manifest.toJS(), { defaultKeyType: Scalar.PLAIN, lineWidth: 90 }); } 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 = await import(path.resolve(filename)); } else { subgraphDebug('Loading manifest from %s', filename); const raw_data = await fs.readFile(filename, 'utf-8'); subgraphDebug('Checking for file data sources in %s', filename); has_file_data_sources = raw_data.includes('kind: file'); subgraphDebug('Parsing manifest from %s', filename); data = yaml.parse(raw_data); } // Helper to resolve files relative to the subgraph manifest const resolveFile = maybeRelativeFile => path.resolve(path.dirname(filename), maybeRelativeFile); // TODO: Validation for file data sources if (!has_file_data_sources) { subgraphDebug('Validating manifest from %s', filename); const manifestErrors = await Subgraph.validate(data, protocol, { resolveFile, }); if (manifestErrors.size > 0) { subgraphDebug('Manifest validation failed for %s', filename); throwCombinedError(filename, manifestErrors); } } const manifest = immutable.fromJS(data); subgraphDebug.extend('manifest')('Loaded: %M', manifest); // Validate the schema subgraphDebug.extend('manifest')('Validating schema'); Subgraph.validateSchema(manifest, { resolveFile }); // Perform other validations const protocolSubgraph = protocol.getSubgraph({ manifest, resolveFile, }); const errors = skipValidation ? immutable.List() : immutable.List.of(...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 const warnings = skipValidation ? immutable.List() : immutable.List.of(...Subgraph.validateRepository(manifest), ...Subgraph.validateDescription(manifest)); return { result: manifest, warning: warnings.size > 0 ? buildCombinedWarning(filename, warnings) : null, }; } static async write(manifest, filename) { await fs.writeFile(filename, Subgraph.dump(manifest)); } }