@graphprotocol/graph-cli
Version:
CLI for building for and deploying to The Graph
240 lines (236 loc) • 11.6 kB
JavaScript
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));
}
}