mirror of
https://github.com/cerc-io/watcher-ts
synced 2026-09-08 16:54:08 +00:00
CLI to flatten and generate GQL schema from Solidity file (#245)
* Add schema generation. * Use yargs and add a script to generate schema. * Add command to generate schema to README.md * Remove use of arrow functions in visitor. * Add function to export schema in visitor. * Create Event union while adding to it. * Add bool to typemappings. * Add method descriptions and remove output type mappings. * Add storage mode. * Add URL support and a flattening script. * Fix parameter for flattening in README.md. * Add setup and references to README.md. Co-authored-by: prathamesh <prathamesh.musale0@gmail.com>
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
//
|
||||
// Copyright 2021 Vulcanize, Inc.
|
||||
//
|
||||
|
||||
import { readFileSync, createWriteStream } from 'fs';
|
||||
import fetch from 'node-fetch';
|
||||
import path from 'path';
|
||||
import yargs from 'yargs';
|
||||
import { hideBin } from 'yargs/helpers';
|
||||
|
||||
import { parse, visit } from '@solidity-parser/parser';
|
||||
|
||||
import { Visitor } from './visitor';
|
||||
|
||||
const MODE_ETH_CALL = 'eth_call';
|
||||
const MODE_STORAGE = 'storage';
|
||||
|
||||
const main = async (): Promise<void> => {
|
||||
const argv = await yargs(hideBin(process.argv))
|
||||
.option('input-file', {
|
||||
alias: 'i',
|
||||
demandOption: true,
|
||||
describe: 'Input contract file path or an url.',
|
||||
type: 'string'
|
||||
})
|
||||
.option('output-file', {
|
||||
alias: 'o',
|
||||
describe: 'Schema output file path.',
|
||||
type: 'string'
|
||||
})
|
||||
.option('mode', {
|
||||
alias: 'm',
|
||||
type: 'string',
|
||||
default: MODE_STORAGE,
|
||||
choices: [MODE_ETH_CALL, MODE_STORAGE]
|
||||
})
|
||||
.argv;
|
||||
|
||||
let data: string;
|
||||
if (argv['input-file'].startsWith('http')) {
|
||||
const response = await fetch(argv['input-file']);
|
||||
data = await response.text();
|
||||
} else {
|
||||
data = readFileSync(path.resolve(argv['input-file'])).toString();
|
||||
}
|
||||
|
||||
const ast = parse(data);
|
||||
|
||||
// Filter out library nodes.
|
||||
ast.children = ast.children.filter(child => !(child.type === 'ContractDefinition' && child.kind === 'library'));
|
||||
|
||||
const visitor = new Visitor();
|
||||
|
||||
if (argv.mode === MODE_ETH_CALL) {
|
||||
visit(ast, {
|
||||
FunctionDefinition: visitor.functionDefinitionVisitor.bind(visitor),
|
||||
EventDefinition: visitor.eventDefinitionVisitor.bind(visitor)
|
||||
});
|
||||
} else {
|
||||
visit(ast, {
|
||||
StateVariableDeclaration: visitor.stateVariableDeclarationVisitor.bind(visitor),
|
||||
EventDefinition: visitor.eventDefinitionVisitor.bind(visitor)
|
||||
});
|
||||
}
|
||||
|
||||
const outStream = argv['output-file'] ? createWriteStream(path.resolve(argv['output-file'])) : process.stdout;
|
||||
visitor.exportSchema(outStream);
|
||||
};
|
||||
|
||||
main().catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
@@ -0,0 +1,228 @@
|
||||
//
|
||||
// Copyright 2021 Vulcanize, Inc.
|
||||
//
|
||||
|
||||
import { GraphQLSchema, printSchema } from 'graphql';
|
||||
import { SchemaComposer } from 'graphql-compose';
|
||||
import { Writable } from 'stream';
|
||||
|
||||
export interface Param {
|
||||
name: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export class Schema {
|
||||
_composer: SchemaComposer;
|
||||
_typeMapping: Map<string, string>;
|
||||
_events: Array<string>;
|
||||
|
||||
constructor () {
|
||||
this._composer = new SchemaComposer();
|
||||
this._typeMapping = new Map();
|
||||
this._events = [];
|
||||
|
||||
this._addBasicTypes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a query to the schema with the given parameters.
|
||||
* @param name Name of the query.
|
||||
* @param params Parameters to the query.
|
||||
* @param returnType Return type for the query.
|
||||
*/
|
||||
addQuery (name: string, params: Array<Param>, returnType: string): void {
|
||||
// TODO: Handle cases where returnType/params type is an array.
|
||||
const queryObject: { [key: string]: any; } = {};
|
||||
queryObject[name] = {
|
||||
// Get type composer object for return type from the schema composer.
|
||||
type: this._composer.getOTC(`Result${this._typeMapping.get(returnType)}`).NonNull,
|
||||
args: {
|
||||
blockHash: 'String!',
|
||||
contractAddress: 'String!'
|
||||
}
|
||||
};
|
||||
|
||||
if (params.length > 0) {
|
||||
queryObject[name].args = params.reduce((acc, curr) => {
|
||||
acc[curr.name] = this._typeMapping.get(curr.type) + '!';
|
||||
return acc;
|
||||
}, queryObject[name].args);
|
||||
}
|
||||
|
||||
// Add a query to the schema composer using queryObject.
|
||||
this._composer.Query.addFields(queryObject);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a type to the schema for an event.
|
||||
* @param name Event name.
|
||||
* @param params Event parameters.
|
||||
*/
|
||||
addEventType (name: string, params: Array<Param>): void {
|
||||
name = `${name}Event`;
|
||||
|
||||
const typeObject: any = {};
|
||||
typeObject.name = name;
|
||||
typeObject.fields = {};
|
||||
|
||||
if (params.length > 0) {
|
||||
typeObject.fields = params.reduce((acc, curr) => {
|
||||
acc[curr.name] = this._typeMapping.get(curr.type) + '!';
|
||||
return acc;
|
||||
}, typeObject.fields);
|
||||
}
|
||||
|
||||
// Create a type composer to add the required type in the schema composer.
|
||||
this._composer.createObjectTC(typeObject);
|
||||
|
||||
this._events.push(name);
|
||||
this._addToEventUnion(name);
|
||||
|
||||
if (this._events.length === 1) {
|
||||
this._addEventsRelatedTypes();
|
||||
this._addEventsQuery();
|
||||
this._addEventSubscription();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the schema from the schema composer.
|
||||
* @returns GraphQLSchema object.
|
||||
*/
|
||||
buildSchema (): GraphQLSchema {
|
||||
return this._composer.buildSchema();
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes schema to a stream.
|
||||
* @param outStream A writable output stream to write the schema to.
|
||||
*/
|
||||
exportSchema (outStream: Writable): void {
|
||||
// Get schema as a string from GraphQLSchema.
|
||||
const schema = printSchema(this.buildSchema());
|
||||
outStream.write(schema);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds basic types to the schema and typemapping.
|
||||
*/
|
||||
_addBasicTypes (): void {
|
||||
// Create a scalar type composer to add the scalar BigInt in the schema composer.
|
||||
this._composer.createScalarTC({
|
||||
name: 'BigInt'
|
||||
});
|
||||
|
||||
// Create a type composer to add the type Proof in the schema composer.
|
||||
this._composer.createObjectTC({
|
||||
name: 'Proof',
|
||||
fields: {
|
||||
data: 'String!'
|
||||
}
|
||||
});
|
||||
|
||||
this._composer.createObjectTC({
|
||||
name: 'ResultBoolean',
|
||||
fields: {
|
||||
value: 'Boolean!',
|
||||
proof: () => this._composer.getOTC('Proof')
|
||||
}
|
||||
});
|
||||
|
||||
this._composer.createObjectTC({
|
||||
name: 'ResultString',
|
||||
fields: {
|
||||
value: 'String!',
|
||||
proof: () => this._composer.getOTC('Proof')
|
||||
}
|
||||
});
|
||||
|
||||
this._composer.createObjectTC({
|
||||
name: 'ResultInt',
|
||||
fields: {
|
||||
value: () => 'Int!',
|
||||
proof: () => this._composer.getOTC('Proof')
|
||||
}
|
||||
});
|
||||
|
||||
this._composer.createObjectTC({
|
||||
name: 'ResultBigInt',
|
||||
fields: {
|
||||
// Get type composer object for BigInt scalar from the schema composer.
|
||||
value: () => this._composer.getSTC('BigInt').NonNull,
|
||||
proof: () => this._composer.getOTC('Proof')
|
||||
}
|
||||
});
|
||||
|
||||
// TODO Get typemapping from ethersjs.
|
||||
this._typeMapping.set('string', 'String');
|
||||
this._typeMapping.set('uint8', 'Int');
|
||||
this._typeMapping.set('uint256', 'BigInt');
|
||||
this._typeMapping.set('address', 'String');
|
||||
this._typeMapping.set('bool', 'Boolean');
|
||||
this._typeMapping.set('bytes4', 'String');
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds types 'ResultEvent' and 'WatchedEvent' to the schema.
|
||||
*/
|
||||
_addEventsRelatedTypes (): void {
|
||||
// Create the ResultEvent type.
|
||||
const resultEventName = 'ResultEvent';
|
||||
this._composer.createObjectTC({
|
||||
name: resultEventName,
|
||||
fields: {
|
||||
// Get type composer object for Event union from the schema composer.
|
||||
event: () => this._composer.getUTC('Event').NonNull,
|
||||
proof: () => this._composer.getOTC('Proof')
|
||||
}
|
||||
});
|
||||
|
||||
// Create the WatchedEvent type.
|
||||
const watchedEventName = 'WatchedEvent';
|
||||
this._composer.createObjectTC({
|
||||
name: watchedEventName,
|
||||
fields: {
|
||||
blockHash: 'String!',
|
||||
contractAddress: 'String!',
|
||||
event: () => this._composer.getOTC(resultEventName).NonNull
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a query for events to the schema.
|
||||
*/
|
||||
_addEventsQuery (): void {
|
||||
this._composer.Query.addFields({
|
||||
events: {
|
||||
type: [this._composer.getOTC('ResultEvent').NonNull],
|
||||
args: {
|
||||
blockHash: 'String!',
|
||||
contractAddress: 'String!',
|
||||
name: 'String'
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an event subscription to the schema.
|
||||
*/
|
||||
_addEventSubscription (): void {
|
||||
// Add a subscription to the schema composer.
|
||||
this._composer.Subscription.addFields({
|
||||
onEvent: () => this._composer.getOTC('WatchedEvent').NonNull
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an 'Event' union (if doesn't exist) to the schema. Adds the specified event to the 'Event' union.
|
||||
* @param event Event type name to add to the union.
|
||||
*/
|
||||
_addToEventUnion (event: string): void {
|
||||
// Get (or create if doesn't exist) type composer object for Event union from the schema composer.
|
||||
const eventUnion = this._composer.getOrCreateUTC('Event');
|
||||
// Add a new type to the union.
|
||||
eventUnion.addType(this._composer.getOTC(event));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
//
|
||||
// Copyright 2021 Vulcanize, Inc.
|
||||
//
|
||||
|
||||
import { Writable } from 'stream';
|
||||
import { Schema, Param } from './schema';
|
||||
|
||||
export class Visitor {
|
||||
_schema: Schema;
|
||||
|
||||
constructor () {
|
||||
this._schema = new Schema();
|
||||
}
|
||||
|
||||
/**
|
||||
* Visitor function for function definitions.
|
||||
* @param node ASTNode for a function definition.
|
||||
*/
|
||||
functionDefinitionVisitor (node: any): void {
|
||||
if (node.stateMutability === 'view' && (node.visibility === 'external' || node.visibility === 'public')) {
|
||||
const name = node.name;
|
||||
const params = node.parameters.map((item: any) => {
|
||||
return { name: item.name, type: item.typeName.name };
|
||||
});
|
||||
|
||||
// TODO Handle multiple return parameters and array return type.
|
||||
const returnType = node.returnParameters[0].typeName.name;
|
||||
|
||||
this._schema.addQuery(name, params, returnType);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Visitor function for state variable declarations.
|
||||
* @param node ASTNode for a state variable declaration.
|
||||
*/
|
||||
stateVariableDeclarationVisitor (node: any): void {
|
||||
// TODO Handle multiples variables in a single line.
|
||||
// TODO Handle array types.
|
||||
let name: string = node.variables[0].name;
|
||||
name = name.startsWith('_') ? name.substring(1) : name;
|
||||
|
||||
const params: Param[] = [];
|
||||
|
||||
let typeName = node.variables[0].typeName;
|
||||
let numParams = 0;
|
||||
|
||||
// If the variable type is mapping, extract key as a param:
|
||||
// Eg. mapping(address => mapping(address => uint256)) private _allowances;
|
||||
while (typeName.type === 'Mapping') {
|
||||
params.push({ name: `key${numParams.toString()}`, type: typeName.keyType.name });
|
||||
typeName = typeName.valueType;
|
||||
numParams++;
|
||||
}
|
||||
|
||||
const returnType = typeName.name;
|
||||
|
||||
this._schema.addQuery(name, params, returnType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Visitor function for event definitions.
|
||||
* @param node ASTNode for an event definition.
|
||||
*/
|
||||
eventDefinitionVisitor (node: any): void {
|
||||
const name = node.name;
|
||||
const params = node.parameters.map((item: any) => {
|
||||
return { name: item.name, type: item.typeName.name };
|
||||
});
|
||||
|
||||
this._schema.addEventType(name, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes schema to a stream.
|
||||
* @param outStream A writable output stream to write the schema to.
|
||||
*/
|
||||
exportSchema (outStream: Writable): void {
|
||||
this._schema.exportSchema(outStream);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user