@graphprotocol/graph-cli
Version:
CLI for building for and deploying to The Graph
276 lines (275 loc) • 10.9 kB
JavaScript
import fs from 'node:fs';
import path from 'node:path';
import immutable from 'immutable';
import yaml from 'js-yaml';
import Protocol from '../protocols/index.js';
const List = immutable.List;
const Map = immutable.Map;
/**
* Returns a user-friendly type name for a value.
*/
const typeName = (value) => List.isList(value) ? 'list' : Map.isMap(value) ? 'map' : typeof value;
/**
* Converts an immutable or plain JavaScript value to a YAML string.
*/
const toYAML = (x) => yaml
.dump(typeName(x) === 'list' || typeName(x) === 'map' ? x.toJS() : x, {
indent: 2,
})
.trim();
/**
* Looks up the type of a field in a GraphQL object type.
*/
const getFieldType = (type, fieldName) => {
const fieldDef = type
.get('fields')
.find((field) => field.getIn(['name', 'value']) === fieldName);
if (fieldDef) {
return fieldDef.get('type');
}
};
/**
* Resolves a type in the GraphQL schema.
*/
const resolveType = (schema, type) => type.has('type')
? resolveType(schema, type.get('type'))
: type.get('kind') === 'NamedType'
? schema
.get('definitions')
.find((def) => def.getIn(['name', 'value']) === type.getIn(['name', 'value']))
: 'resolveType: unimplemented';
/**
* A map of supported validators.
*/
const validators = immutable.fromJS({
ScalarTypeDefinition: (value, ctx) => validators.get(ctx.getIn(['type', 'name', 'value']))(value, ctx),
UnionTypeDefinition: (value, ctx) => {
const unionVariants = ctx.getIn(['type', 'types']);
let errors = List();
for (const variantType of unionVariants) {
const variantErrors = validateValue(value, ctx.set('type', variantType));
// No errors found, union variant matched, early return
if (variantErrors.isEmpty()) {
return List();
}
errors = errors.push(variantErrors);
}
// Return errors from variant that matched the most
return List(errors.minBy((variantErrors) => variantErrors.count()));
},
NamedType: (value, ctx) => validateValue(value, ctx.update('type', type => resolveType(ctx.get('schema'), type))),
NonNullType: (value, ctx) => value !== null && value !== undefined
? validateValue(value, ctx.update('type', type => type.get('type')))
: immutable.fromJS([
{
path: ctx.get('path'),
message: `No value provided`,
},
]),
ListType: (value, ctx) => List.isList(value)
? value.reduce((errors, value, i) => errors.concat(validateValue(value, ctx.update('path', path => path.push(i)).update('type', type => type.get('type')))), List())
: immutable.fromJS([
{
path: ctx.get('path'),
message: `Expected list, found ${typeName(value)}:\n${toYAML(value)}`,
},
]),
ObjectTypeDefinition: (value, ctx) => {
return Map.isMap(value)
? ctx
.getIn(['type', 'fields'])
.map((fieldDef) => fieldDef.getIn(['name', 'value']))
.concat(value.keySeq())
.toSet()
.reduce((errors, key) => getFieldType(ctx.get('type'), key)
? errors.concat(validateValue(value.get(key), ctx
.update('path', (path) => path.push(key))
.set('type', getFieldType(ctx.get('type'), key))))
: errors.push(key == 'templates' && ctx.get('protocol').hasTemplates()
? immutable.fromJS({
path: ctx.get('path'),
message: `The way to declare data source templates has changed, ` +
`please move the templates from inside data sources to ` +
`a \`templates:\` field at the top level of the manifest.`,
})
: immutable.fromJS({
path: ctx.get('path'),
message: `Unexpected key in map: ${key}`,
})), List())
: immutable.fromJS([
{
path: ctx.get('path'),
message: `Expected map, found ${typeName(value)}:\n${toYAML(value)}`,
},
]);
},
EnumTypeDefinition: (value, ctx) => {
const enumValues = ctx.getIn(['type', 'values']).map((v) => {
return v.getIn(['name', 'value']);
});
const allowedValues = enumValues.toArray().join(', ');
return enumValues.includes(value)
? List()
: immutable.fromJS([
{
path: ctx.get('path'),
message: `Unexpected enum value: ${value}, allowed values: ${allowedValues}`,
},
]);
},
String: (value, ctx) => typeof value === 'string'
? List()
: immutable.fromJS([
{
path: ctx.get('path'),
message: `Expected string, found ${typeName(value)}:\n${toYAML(value)}`,
},
]),
BigInt: (value, ctx) => typeof value === 'number'
? List()
: immutable.fromJS([
{
path: ctx.get('path'),
message: `Expected BigInt, found ${typeName(value)}:\n${toYAML(value)}`,
},
]),
File: (value, ctx) => typeof value === 'string'
? fs.existsSync(ctx.get('resolveFile')(value))
? List()
: immutable.fromJS([
{
path: ctx.get('path'),
message: `File does not exist: ${path.relative(process.cwd(), value)}`,
},
])
: immutable.fromJS([
{
path: ctx.get('path'),
message: `Expected filename, found ${typeName(value)}:\n${value}`,
},
]),
JSON: (value, ctx) => {
try {
JSON.parse(JSON.stringify(value));
return List();
}
catch (e) {
return immutable.fromJS([
{
path: ctx.get('path'),
message: `Invalid JSON value: ${e.message}`,
},
]);
}
},
Boolean: (value, ctx) => typeof value === 'boolean'
? List()
: immutable.fromJS([
{
path: ctx.get('path'),
message: `Expected true or false, found ${typeName(value)}:\n${toYAML(value)}`,
},
]),
});
const validateValue = (value, ctx) => {
const kind = ctx.getIn(['type', 'kind']);
const validator = validators.get(kind);
if (validator !== undefined) {
// If the type is nullable, accept undefined and null; if the nullable
// type is wrapped in a `NonNullType`, the validator for that `NonNullType`
// will catch the missing/unset value
if (kind !== 'NonNullType' && (value === undefined || value === null)) {
return List();
}
return validator(value, ctx);
}
return immutable.fromJS([
{
path: ctx.get('path'),
message: `No validator for unsupported schema type: ${kind}`,
},
]);
};
// Transforms list of data sources like this:
// [
// { name: 'contract0', kind: 'ethereum/contract', network: 'mainnet' },
// { name: 'contract1', kind: 'ethereum', network: 'mainnet' },
// { name: 'contract2', kind: 'ethereum/contract', network: 'gnosis' },
// { name: 'contract3', kind: 'near', network: 'near-mainnet' },
// ]
//
// Into Immutable JS structure like this (protocol kind is normalized):
// {
// ethereum: {
// mainnet: ['contract0', 'contract1'],
// gnosis: ['contract2'],
// },
// near: {
// 'near-mainnet': ['contract3'],
// },
// }
const dataSourceListToMap = (dataSources) => dataSources.reduce((protocolKinds, dataSource) => protocolKinds.update(Protocol.normalizeName(dataSource.kind), (networks) => (networks || immutable.OrderedMap()).update(dataSource.network, (dataSourceNames) => (dataSourceNames || immutable.OrderedSet()).add(dataSource.name))), immutable.OrderedMap());
const validateDataSourceProtocolAndNetworks = (value) => {
const dataSources = [...value.dataSources, ...(value.templates || [])];
const protocolNetworkMap = dataSourceListToMap(dataSources);
if (protocolNetworkMap.size > 1) {
return immutable.fromJS([
{
path: [],
message: `Conflicting protocol kinds used in data sources and templates:
${protocolNetworkMap
.map((dataSourceNames, protocolKind) => ` ${protocolKind === undefined
? 'Data sources and templates having no protocol kind set'
: `Data sources and templates using '${protocolKind}'`}:\n${dataSourceNames
.valueSeq()
.flatten()
.map((ds) => ` - ${ds}`)
.join('\n')}`)
.join('\n')}
Recommendation: Make all data sources and templates use the same protocol kind.`,
},
]);
}
const networks = protocolNetworkMap.first();
if (networks.size > 1) {
return immutable.fromJS([
{
path: [],
message: `Conflicting networks used in data sources and templates:
${networks
.map((dataSources, network) => ` ${network === undefined
? 'Data sources and templates having no network set'
: `Data sources and templates using '${network}'`}:\n${dataSources.map((ds) => ` - ${ds}`).join('\n')}`)
.join('\n')}
Recommendation: Make all data sources and templates use the same network name.`,
},
]);
}
return List();
};
export const validateManifest = (value, type, schema, protocol, { resolveFile }) => {
// Validate manifest using the GraphQL schema that defines its structure
const errors = value !== null && value !== undefined
? validateValue(immutable.fromJS(value), immutable.fromJS({
schema,
type,
path: [],
errors: [],
resolveFile,
protocol,
}))
: immutable.fromJS([
{
path: [],
message: `Expected non-empty value, found ${typeName(value)}:\n ${value}`,
},
]);
// Fail early because a broken manifest prevents us from performing
// additional validation steps
if (!errors.isEmpty()) {
return errors;
}
// Validate that all data sources are for the same `network` and `protocol` (kind)
// (this includes _no_ network/protocol at all)
return validateDataSourceProtocolAndNetworks(value);
};