UNPKG

@graphprotocol/graph-cli

Version:

CLI for building for and deploying to The Graph

436 lines (435 loc) • 19.4 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const path_1 = __importDefault(require("path")); const url_1 = require("url"); const gluegun_1 = require("gluegun"); const ipfs_http_client_1 = require("ipfs-http-client"); const core_1 = require("@oclif/core"); const auth_1 = require("../command-helpers/auth"); const compiler_1 = require("../command-helpers/compiler"); const DataSourcesExtractor = __importStar(require("../command-helpers/data-sources")); const ipfs_1 = require("../command-helpers/ipfs"); const jsonrpc_1 = require("../command-helpers/jsonrpc"); const network_1 = require("../command-helpers/network"); const node_1 = require("../command-helpers/node"); const version_1 = require("../command-helpers/version"); const constants_1 = require("../constants"); const debug_1 = __importDefault(require("../debug")); const protocols_1 = __importDefault(require("../protocols")); const headersFlag = core_1.Flags.custom({ summary: 'Add custom headers that will be used by the IPFS HTTP client.', aliases: ['hdr'], parse: val => JSON.parse(val), default: {}, }); const productOptions = ['subgraph-studio', 'hosted-service']; const deployDebugger = (0, debug_1.default)('graph-cli:deploy'); class DeployCommand extends core_1.Command { async run() { const { args: { 'subgraph-name': subgraphNameArg, 'subgraph-manifest': manifest }, flags: { product: productFlag, studio, 'deploy-key': deployKeyFlag, 'access-token': accessToken, 'version-label': versionLabelFlag, ipfs, headers, node: nodeFlag, 'output-dir': outputDir, 'skip-migrations': skipMigrations, watch, 'debug-fork': debugFork, network, 'network-file': networkFile, 'ipfs-hash': ipfsHash, 'from-hosted-service': hostedServiceSubgraphName, }, } = await this.parse(DeployCommand); if (hostedServiceSubgraphName) { const { health, subgraph: subgraphIpfsHash } = await (0, node_1.getHostedServiceSubgraphId)({ subgraphName: hostedServiceSubgraphName, }); const safeToDeployFailedSubgraph = health === 'failed' ? await core_1.ux.confirm('This subgraph has failed indexing on hosted service. Do you wish to continue the deploy?') : true; if (!safeToDeployFailedSubgraph) return; const safeToDeployUnhealthySubgraph = health === 'unhealthy' ? await core_1.ux.confirm('This subgraph is not healthy on hosted service. Do you wish to continue the deploy?') : true; if (!safeToDeployUnhealthySubgraph) return; const { node } = (0, node_1.chooseNodeUrl)({ studio: true, product: undefined }); // shouldn't happen, but we need to satisfy the compiler if (!node) { this.error('No node URL available'); } const requestUrl = new url_1.URL(node); const client = (0, jsonrpc_1.createJsonRpcClient)(requestUrl); // Exit with an error code if the client couldn't be created if (!client) { this.error('Failed to create RPC client'); } // Use the deploy key, if one is set let deployKey = deployKeyFlag; if (!deployKey && accessToken) { deployKey = accessToken; // backwards compatibility } deployKey = await (0, auth_1.identifyDeployKey)(node, deployKey); if (!deployKey) { this.error('No deploy key available'); } // @ts-expect-error options property seems to exist client.options.headers = { ...constants_1.GRAPH_CLI_SHARED_HEADERS, Authorization: 'Bearer ' + deployKey, }; const subgraphName = await core_1.ux.prompt('What is the name of the subgraph you want to deploy?', { required: true, }); // Ask for label if not on hosted service const versionLabel = versionLabelFlag || (await core_1.ux.prompt('Which version label to use? (e.g. "v0.0.1")', { required: true, })); const spinner = gluegun_1.print.spin(`Deploying to Graph node ${requestUrl}`); client.request('subgraph_deploy', { name: subgraphName, ipfs_hash: subgraphIpfsHash, version_label: versionLabel, debug_fork: debugFork, }, async ( // @ts-expect-error TODO: why are the arguments not typed? requestError, // @ts-expect-error TODO: why are the arguments not typed? jsonRpcError, // @ts-expect-error TODO: why are the arguments not typed? res) => { deployDebugger('requestError: %O', requestError); deployDebugger('jsonRpcError: %O', jsonRpcError); if (jsonRpcError) { const message = jsonRpcError?.message || jsonRpcError?.code?.toString(); deployDebugger('message: %O', message); let errorMessage = `Failed to deploy to Graph node ${requestUrl}: ${message}`; // Provide helpful advice when the subgraph has not been created yet if (message?.match(/subgraph name not found/)) { errorMessage += ` Make sure to create the subgraph first by running the following command: $ graph create --node ${node} ${subgraphName}`; } if (message?.match(/auth failure/)) { errorMessage += '\nYou may need to authenticate first.'; } spinner.fail(errorMessage); process.exit(1); } else if (requestError) { spinner.fail(`HTTP error deploying the subgraph ${requestError.code}`); process.exit(1); } else { spinner.stop(); const base = requestUrl.protocol + '//' + requestUrl.hostname; let playground = res.playground; let queries = res.queries; // Add a base URL if graph-node did not return the full URL if (playground.charAt(0) === ':') { playground = base + playground; } if (queries.charAt(0) === ':') { queries = base + queries; } gluegun_1.print.success(`Deployed to ${playground}`); gluegun_1.print.info('\nSubgraph endpoints:'); gluegun_1.print.info(`Queries (HTTP): ${queries}`); gluegun_1.print.info(``); process.exit(0); } }); return; } const subgraphName = subgraphNameArg || (await core_1.ux.prompt('What is the subgraph name?', { required: true, })); // We are given a node URL, so we prioritize that over the product flag const product = nodeFlag ? productFlag : studio ? 'subgraph-studio' : productFlag || (await gluegun_1.prompt .ask([ { name: 'product', message: 'Which product to deploy for?', required: true, type: 'select', choices: productOptions, }, ]) .then(({ product }) => product)); const { node } = (0, node_1.chooseNodeUrl)({ product, studio, node: nodeFlag, }); if (!node) { // shouldn't happen, but we do the check to satisfy TS this.error('No Graph node provided'); } const isStudio = node.match(/studio/); const isHostedService = node.match(/thegraph.com/) && !isStudio; const requestUrl = new url_1.URL(node); const client = (0, jsonrpc_1.createJsonRpcClient)(requestUrl); // Exit with an error code if the client couldn't be created if (!client) { this.exit(1); return; } // Use the deploy key, if one is set let deployKey = deployKeyFlag; if (!deployKey && accessToken) { deployKey = accessToken; // backwards compatibility } deployKey = await (0, auth_1.identifyDeployKey)(node, deployKey); if (deployKey !== undefined && deployKey !== null) { // @ts-expect-error options property seems to exist client.options.headers = { ...constants_1.GRAPH_CLI_SHARED_HEADERS, Authorization: 'Bearer ' + deployKey, }; } // Ask for label if not on hosted service let versionLabel = versionLabelFlag; if (!versionLabel && !isHostedService) { versionLabel = await core_1.ux.prompt('Which version label to use? (e.g. "v0.0.1")', { required: true, }); } const deploySubgraph = async (ipfsHash) => { const spinner = gluegun_1.print.spin(`Deploying to Graph node ${requestUrl}`); client.request('subgraph_deploy', { name: subgraphName, ipfs_hash: ipfsHash, version_label: versionLabel, debug_fork: debugFork, }, async ( // @ts-expect-error TODO: why are the arguments not typed? requestError, // @ts-expect-error TODO: why are the arguments not typed? jsonRpcError, // @ts-expect-error TODO: why are the arguments not typed? res) => { deployDebugger('requestError: %O', requestError); deployDebugger('jsonRpcError: %O', jsonRpcError); if (jsonRpcError) { const message = jsonRpcError?.message || jsonRpcError?.code?.toString(); deployDebugger('message: %O', message); let errorMessage = `Failed to deploy to Graph node ${requestUrl}: ${message}`; // Provide helpful advice when the subgraph has not been created yet if (message?.match(/subgraph name not found/)) { if (isHostedService) { errorMessage += '\nYou may need to create it at https://thegraph.com/explorer/dashboard.'; } else { errorMessage += ` Make sure to create the subgraph first by running the following command: $ graph create --node ${node} ${subgraphName}`; } } if (message?.match(/auth failure/)) { errorMessage += '\nYou may need to authenticate first.'; } spinner.fail(errorMessage); this.exit(1); } else if (requestError) { spinner.fail(`HTTP error deploying the subgraph ${requestError.code}`); this.exit(1); } else { spinner.stop(); const base = requestUrl.protocol + '//' + requestUrl.hostname; let playground = res.playground; let queries = res.queries; // Add a base URL if graph-node did not return the full URL if (playground.charAt(0) === ':') { playground = base + playground; } if (queries.charAt(0) === ':') { queries = base + queries; } if (isHostedService) { gluegun_1.print.success(`Deployed to https://thegraph.com/explorer/subgraph/${subgraphName}`); } else { gluegun_1.print.success(`Deployed to ${playground}`); } gluegun_1.print.info('\nSubgraph endpoints:'); gluegun_1.print.info(`Queries (HTTP): ${queries}`); gluegun_1.print.info(``); process.exit(0); } }); }; // we are provided the IPFS hash, so we deploy directly if (ipfsHash) { // Connect to the IPFS node (if a node address was provided) const ipfsClient = (0, ipfs_http_client_1.create)({ url: (0, compiler_1.appendApiVersionForGraph)(ipfs.toString()), headers: { ...headers, ...constants_1.GRAPH_CLI_SHARED_HEADERS, }, }); // Fetch the manifest from IPFS const manifestBuffer = ipfsClient.cat(ipfsHash); let manifestFile = ''; for await (const chunk of manifestBuffer) { manifestFile += chunk.toString(); } if (!manifestFile) { this.error(`Could not find subgraph manifest at IPFS hash ${ipfsHash}`, { exit: 1 }); } await ipfsClient.pin.add(ipfsHash); await deploySubgraph(ipfsHash); return; } let protocol; try { // Checks to make sure deploy doesn't run against // older subgraphs (both apiVersion and graph-ts version). // // We don't want the deploy to run without these conditions // because that would mean the CLI would try to compile code // using the wrong AssemblyScript compiler. await (0, version_1.assertManifestApiVersion)(manifest, '0.0.5'); await (0, version_1.assertGraphTsVersion)(path_1.default.dirname(manifest), '0.25.0'); const dataSourcesAndTemplates = await DataSourcesExtractor.fromFilePath(manifest); protocol = protocols_1.default.fromDataSources(dataSourcesAndTemplates); } catch (e) { this.error(e, { exit: 1 }); } if (network) { const identifierName = protocol.getContract().identifierName(); await (0, network_1.updateSubgraphNetwork)(manifest, network, networkFile, identifierName); } const compiler = (0, compiler_1.createCompiler)(manifest, { ipfs, headers, outputDir, outputFormat: 'wasm', skipMigrations, blockIpfsMethods: isStudio || undefined, protocol, }); // Exit with an error code if the compiler couldn't be created if (!compiler) { this.exit(1); return; } if (watch) { await compiler.watchAndCompile(async (ipfsHash) => { if (ipfsHash !== undefined) { await deploySubgraph(ipfsHash); } }); } else { const result = await compiler.compile({ validate: true }); if (result === undefined || result === false) { // Compilation failed, not deploying. process.exitCode = 1; return; } await deploySubgraph(result); } } } DeployCommand.description = 'Deploys a subgraph to a Graph node.'; DeployCommand.args = { 'subgraph-name': core_1.Args.string({}), 'subgraph-manifest': core_1.Args.string({ default: 'subgraph.yaml', }), }; DeployCommand.flags = { help: core_1.Flags.help({ char: 'h', }), product: core_1.Flags.string({ summary: 'Select a product for which to authenticate.', options: productOptions, }), studio: core_1.Flags.boolean({ summary: 'Shortcut for "--product subgraph-studio".', exclusive: ['product'], }), node: core_1.Flags.string({ summary: 'Graph node for which to initialize.', char: 'g', }), 'deploy-key': core_1.Flags.string({ summary: 'User deploy key.', exclusive: ['access-token'], }), 'access-token': core_1.Flags.string({ exclusive: ['deploy-key'], deprecated: { to: 'deploy-key', message: "In next version, we are removing this flag in favor of '--deploy-key'", }, }), 'version-label': core_1.Flags.string({ summary: 'Version label used for the deployment.', char: 'l', }), ipfs: core_1.Flags.string({ summary: 'Upload build results to an IPFS node.', char: 'i', default: ipfs_1.DEFAULT_IPFS_URL, }), 'ipfs-hash': core_1.Flags.string({ summary: 'IPFS hash of the subgraph manifest to deploy.', required: false, }), headers: headersFlag(), 'debug-fork': core_1.Flags.string({ summary: 'ID of a remote subgraph whose store will be GraphQL queried.', }), 'output-dir': core_1.Flags.directory({ summary: 'Output directory for build results.', char: 'o', default: 'build/', }), 'skip-migrations': core_1.Flags.boolean({ summary: 'Skip subgraph migrations.', }), watch: core_1.Flags.boolean({ summary: 'Regenerate types when subgraph files change.', char: 'w', }), network: core_1.Flags.string({ summary: 'Network configuration to use from the networks config file.', }), // TODO: should be networksFile (with an "s"), or? 'network-file': core_1.Flags.file({ summary: 'Networks config file path.', default: 'networks.json', }), 'from-hosted-service': core_1.Flags.string({ summary: 'Hosted service Subgraph Name to deploy to studio.', }), }; exports.default = DeployCommand;