feat(plugins/scripts-runner): introduce exec command to run scripts

This commit introduces a new feature that enables users to run (migration) scripts.
Similar to deployment hooks, scripts are functions that may perform operations on newly
deployed Smart Contracts.

Therefore a script needs to export a function that has access to some dependencies:

```
// scripts/001-some-script.js

module.exports = async ({contracts, web3, logger}) => {
  ...
};
```

Where `contracts` is a map of newly deployed Smart Contract instances, `web3` a blockchain connector
instance and `logger` Embark's logger instance. Script functions can but don't have to be `async`.

To execute such a script users use the newly introduced `exec` command:

```
$ embark exec development scripts/001-some-script.js
```

In the example above, `development` defines the environment in which Smart Contracts are being
deployed to as well as where tracking data is stored.
Alternativey, users can also provide a directory in which case Embark will try to execute every
script living inside of it:

```
$ embark exec development scripts
```

Scripts can fail and therefore emit an error accordingly. When this happens, Embark will
abort the script execution (in case multiple are scheduled to run) and informs the user
about the original error:

```
.. 001_foo.js running....
Script '001_foo.js' failed to execute. Original error: Error: Some error
```

It's recommended for scripts to emit proper instances of `Error`.

(Migration) scripts can be tracked as well but there are a couple of rules to be aware of:

- Generally, tracking all scripts that have been executed by default is not a good thing because
  some scripts might be one-off operations.
- OTOH, there might be scripts that should always be tracked by default
- Therefore, we introduce a dedicated `migrations` directory in which scripts live that should be
  tracked by default
- Any other scripts that does not live in the specified `migrations` directory will not be tracked **unless**
- The new `--track` option was provided

For more information see: https://notes.status.im/h8XwB7xkR7GKnfNh6OnPMQ
This commit is contained in:
Pascal Precht 2020-02-04 11:47:12 +01:00 committed by Michael Bradley, Jr
parent 0f59e0c216
commit 40c3d98217
22 changed files with 927 additions and 32 deletions

View File

@ -108,5 +108,6 @@
},
"environments": {
"development": "development"
}
}
},
"defaultMigrationsDir": "migrations"
}

View File

@ -27,7 +27,7 @@ import { readJsonSync } from 'fs-extra';
const cloneDeep = require('lodash.clonedeep');
const { replaceZeroAddressShorthand } = AddressUtils;
import { getBlockchainDefaults, getContractDefaults } from './configDefaults';
import { getBlockchainDefaults, getContractDefaults, embarkConfigDefaults } from './configDefaults';
const constants = readJsonSync(path.join(__dirname, '../constants.json'));
@ -45,6 +45,7 @@ export interface EmbarkConfig {
generationDir?: string;
plugins?: any;
buildDir?: string;
migrations: string;
}
export class Config {
@ -83,7 +84,7 @@ export class Config {
events: Events;
embarkConfig: any = {};
embarkConfig: EmbarkConfig = embarkConfigDefaults;
context: any;
@ -629,17 +630,7 @@ export class Config {
}
loadEmbarkConfigFile() {
const configObject = {
options: {
solc: {
"optimize": true,
"optimize-runs": 200
}
},
generationDir: "embarkArtifacts"
};
this.embarkConfig = recursiveMerge(configObject, this.embarkConfig);
this.embarkConfig = recursiveMerge(embarkConfigDefaults, this.embarkConfig);
const contracts = this.embarkConfig.contracts;
// determine contract 'root' directories

View File

@ -4,6 +4,22 @@ import { join } from "path";
const constants = readJsonSync(join(__dirname, '../constants.json'));
export const embarkConfigDefaults = {
contracts: [],
config: '',
migrations: 'migrations',
versions: {
solc: "0.6.1"
},
options: {
solc: {
"optimize": true,
"optimize-runs": 200
}
},
generationDir: "embarkArtifacts"
};
export function getBlockchainDefaults(env) {
const defaults = {
clientConfig: {

View File

@ -91,6 +91,9 @@ export interface Configuration {
cert: string;
};
};
contractsConfig: {
tracking?: boolean | string;
};
plugins: EmbarkPlugins;
reloadConfig(): void;
}

View File

@ -15,6 +15,7 @@ class Cmd {
this.demo();
this.build();
this.run();
this.exec();
this.console();
this.blockchain();
this.simulator();
@ -174,6 +175,30 @@ class Cmd {
});
}
exec() {
program
.command('exec [environment] [script|directory]')
.option('-t, --track', __('Force tracking of migration script', false))
.description(__("Executes specified scripts or all scripts in 'directory'"))
.action((env, target, options) => {
embark.exec({
env,
target,
forceTracking: options.track
}, (err) => {
if (err) {
console.error(err.message ? err.message : err);
process.exit(1);
}
console.log('Done.');
// TODO(pascal): Ideally this shouldn't be needed.
// Seems like there's a pending child process at this point that needs
// to be stopped.
process.exit(0);
});
});
}
console() {
program
.command('console [environment]')

View File

@ -352,6 +352,44 @@ class EmbarkController {
});
}
exec(options, callback) {
const engine = new Engine({
env: options.env,
embarkConfig: options.embarkConfig || 'embark.json'
});
engine.init({}, () => {
engine.registerModuleGroup("coreComponents", {
disableServiceMonitor: true
});
engine.registerModuleGroup("stackComponents");
engine.registerModuleGroup("blockchain");
engine.registerModuleGroup("compiler");
engine.registerModuleGroup("contracts");
engine.registerModulePackage('embark-deploy-tracker', {
plugins: engine.plugins
});
engine.registerModulePackage('embark-scripts-runner');
engine.startEngine(async (err) => {
if (err) {
return callback(err);
}
try {
await engine.events.request2("blockchain:node:start", engine.config.blockchainConfig);
const [contractsList, contractDependencies] = await compileSmartContracts(engine);
await engine.events.request2("deployment:contracts:deploy", contractsList, contractDependencies);
await engine.events.request2('scripts-runner:initialize');
await engine.events.request2('scripts-runner:execute', options.target, options.forceTracking);
} catch (err) {
return callback(err);
}
callback();
});
});
}
console(options) {
this.context = options.context || [constants.contexts.run, constants.contexts.console];
const REPL = require('./dashboard/repl.js');

View File

@ -0,0 +1,10 @@
embark-scripts-runner
==========================
> Embark Scripts Runner
Plugin to run migration scripts for Smart Contract Deployment
Visit [embark.status.im](https://embark.status.im/) to get started with
[Embark](https://github.com/embarklabs/embark).

View File

@ -0,0 +1,78 @@
{
"name": "embark-scripts-runner",
"version": "5.1.0",
"description": "Embark Scripts Runner",
"repository": {
"directory": "packages/plugins/scripts-runner",
"type": "git",
"url": "https://github.com/embarklabs/embark/"
},
"author": "Iuri Matias <iuri.matias@gmail.com>",
"license": "MIT",
"bugs": "https://github.com/embarklabs/embark/issues",
"keywords": [],
"files": [
"dist/"
],
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"embark-collective": {
"build:node": true,
"typecheck": true
},
"scripts": {
"_build": "npm run solo -- build",
"_typecheck": "npm run solo -- typecheck",
"ci": "npm run qa",
"clean": "npm run reset",
"lint": "npm-run-all lint:*",
"lint:ts": "tslint -c tslint.json \"src/**/*.ts\"",
"qa": "npm-run-all lint _typecheck _build test",
"reset": "npx rimraf dist embark-*.tgz package",
"solo": "embark-solo",
"test": "jest"
},
"dependencies": {
"async": "2.6.1",
"@babel/runtime-corejs3": "7.7.4",
"core-js-pure": "3.6.4",
"embark-core": "^5.2.0-nightly.1",
"embark-i18n": "^5.1.1",
"embark-logger": "^5.1.2-nightly.0",
"embark-utils": "^5.2.0-nightly.1",
"fs-extra": "8.1.0",
"web3": "1.2.4"
},
"devDependencies": {
"@babel/core": "7.7.4",
"@types/node": "^10.5.3",
"babel-jest": "24.9.0",
"embark-solo": "^5.1.1-nightly.2",
"embark-testing": "^5.1.0",
"jest": "24.9.0",
"npm-run-all": "4.1.5",
"rimraf": "3.0.0",
"tmp-promise": "1.1.0"
},
"engines": {
"node": ">=10.17.0",
"npm": ">=6.11.3",
"yarn": ">=1.19.1"
},
"jest": {
"collectCoverage": true,
"testEnvironment": "node",
"testMatch": [
"**/test/**/*.js"
],
"transform": {
"\\.(js|ts)$": [
"babel-jest",
{
"rootMode": "upward"
}
]
}
}
}

View File

@ -0,0 +1,60 @@
import path from 'path';
import { Stats } from 'fs';
enum FileType {
SymbolicLink = 'symbolic link',
Socket = 'socket',
Unknown = 'unknown'
}
export class InitializationError extends Error {
name = 'InitalizationError';
constructor(public innerError: Error) {
super();
this.message = `Failed to initalize tracking file: Orignal Error: ${innerError}`;
}
}
export class UnsupportedTargetError extends Error {
name = 'UnsupportedTargetError';
constructor(public stats: Stats) {
super();
// We can't access `this` before `super()` is called so we have to
// set `this.message` after that to get a dedicated error message.
this.setMessage();
}
private setMessage() {
let ftype = FileType.Unknown;
if (this.stats.isSymbolicLink()) {
ftype = FileType.SymbolicLink;
} else if (this.stats.isSocket()) {
ftype = FileType.Socket;
}
this.message = `Script execution target not supported. Expected file path or directory, got ${ftype} type.`;
}
}
export class ScriptExecutionError extends Error {
name = 'ScriptExecutionError';
constructor(public target: string, public innerError: Error) {
super();
this.message = `Script '${path.basename(target)}' failed to execute. Original error: ${innerError.stack}`;
}
}
export class ScriptTrackingError extends Error {
name = 'ScriptTrackingError';
constructor(public innerError: Error) {
super();
this.message = `Couldn't track script due execption. Original error: ${innerError.stack}`;
}
}

View File

@ -0,0 +1,202 @@
import {
InitializationError,
UnsupportedTargetError,
ScriptExecutionError,
ScriptTrackingError
} from './error';
import { FileSystemTracker, ScriptsTracker, TrackingData } from './tracker';
import AsyncIterator from 'core-js-pure/features/async-iterator';
import { Embark, Callback } from 'embark-core';
import { __ } from 'embark-i18n';
import { Logger } from 'embark-logger';
import { dappPath } from 'embark-utils';
import * as fs from 'fs-extra';
import path from 'path';
import Web3 from "web3";
import { BlockTransactionObject } from 'web3-eth';
export enum ScriptsRunnerCommand {
Initialize = 'scripts-runner:initialize',
Execute = 'scripts-runner:execute'
}
export enum ScriptsRunnerEvent {
Executed = 'scripts-runner:script:executed'
}
export interface ScriptsRunnerPluginOptions {
tracker?: ScriptsTracker;
}
export interface ScriptDependencies {
web3: Web3 | null;
contracts: any;
logger: Logger;
}
export interface ExecuteOptions {
target: string;
dependencies: ScriptDependencies;
forceTracking: boolean;
}
export default class ScriptsRunnerPlugin {
private _web3: Web3 | null = null;
private _block: BlockTransactionObject | null = null;
private trackingEnabled: boolean;
private tracker: ScriptsTracker;
constructor(private embark: Embark, options?: ScriptsRunnerPluginOptions) {
this.tracker = options?.tracker ? options.tracker : new FileSystemTracker(embark);
this.trackingEnabled = embark.config.contractsConfig.tracking !== false;
// TODO: it'd be wonderful if Embark called `registerCommandHandlers()` for us
this.registerCommandHandlers();
}
private registerCommandHandlers() {
this.embark.events.setCommandHandler(ScriptsRunnerCommand.Initialize, this.initialize.bind(this));
this.embark.events.setCommandHandler(ScriptsRunnerCommand.Execute, this.execute.bind(this));
}
private async initialize(callback: Callback<any>) {
if (!this.trackingEnabled) {
return callback();
}
try {
await this.tracker.ensureTrackingFile();
callback();
} catch (e) {
callback(new InitializationError(e));
}
}
private get web3() {
return (async () => {
if (!this._web3) {
const provider = await this.embark.events.request2('blockchain:client:provider', 'ethereum');
this._web3 = new Web3(provider);
}
return this._web3;
})();
}
private async execute(target: string, forceTracking = false, callback: Callback<any>) {
const targetPath = !path.isAbsolute(target) ? dappPath(target) : target;
try {
const fstat = await fs.stat(targetPath);
if (fstat.isDirectory()) {
const dependencies = await this.getScriptDependencies();
const results = await this.executeAll({ target: targetPath, dependencies, forceTracking });
return callback(null, results);
}
if (fstat.isFile()) {
const dependencies = await this.getScriptDependencies();
const result = await this.executeSingle({ target: targetPath, dependencies, forceTracking });
return callback(null, result);
}
callback(new UnsupportedTargetError(fstat));
} catch (e) {
callback(e);
}
}
private async executeSingle(options: ExecuteOptions) {
const forceTracking = options.forceTracking;
const scriptName = path.basename(options.target);
const scriptDirectory = path.basename(path.dirname(options.target));
const scriptTracked = await this.tracker.isTracked(scriptName);
if (scriptTracked && (scriptDirectory === this.embark.config.embarkConfig.migrations || forceTracking)) {
this.embark.logger.info(__(' ✓ %s already done', scriptName));
return;
}
this.embark.logger.info(__(' %s running....', scriptName));
const scriptToRun = require(options.target);
let result;
try {
result = await scriptToRun(options.dependencies);
} catch (e) {
const error = e instanceof Error ? e : new Error(e);
throw new ScriptExecutionError(options.target, error);
}
this.embark.logger.info(__(' ✓ finished.'));
if (!this.trackingEnabled) {
return result;
}
try {
await this.tracker.track({
scriptName,
scriptDirectory,
forceTracking
});
} catch (e) {
const error = e instanceof Error ? e : new Error(e);
throw new ScriptTrackingError(e);
}
return result;
}
private async executeAll(options: ExecuteOptions) {
const target = options.target;
const files = await fs.readdir(target);
const scripts = await Promise.all(
files.map(async file => {
const targetPath = !path.isAbsolute(target) ? dappPath(target, file) : path.join(target, file);
return { target: targetPath, stats: await fs.stat(targetPath) };
})
);
return AsyncIterator.from(scripts)
.filter(({stats}) => stats.isFile())
.map(fstat => ({
target: fstat.target,
dependencies: options.dependencies,
forceTracking: options.forceTracking
}))
.map(script => this.executeSingle(script))
.toArray();
}
private async getScriptDependencies() {
const contracts = await this.embark.events.request2('contracts:list');
const dependencies: ScriptDependencies = {
logger: this.embark.logger,
web3: null,
contracts: {}
};
dependencies.web3 = await this.web3;
for (const contract of contracts) {
const registeredInVM = this.checkContractRegisteredInVM(contract);
if (!registeredInVM) {
await this.embark.events.request2("embarkjs:contract:runInVm", contract);
}
const contractInstance = await this.embark.events.request2("runcode:eval", contract.className);
dependencies.contracts[contract.className] = contractInstance;
}
return dependencies;
}
private async checkContractRegisteredInVM(contract) {
const checkContract = `
return typeof ${contract.className} !== 'undefined';
`;
return this.embark.events.request2('runcode:eval', checkContract);
}
}

View File

@ -0,0 +1,118 @@
import { Embark } from 'embark-core';
import * as fs from 'fs-extra';
import { dappPath } from 'embark-utils';
import { BlockTransactionObject } from 'web3-eth';
import Web3 from "web3";
export interface TrackingData {
[key: string]: {
migrations: string[];
};
}
export interface TrackConfig {
scriptName: string;
scriptDirectory: string;
forceTracking: boolean;
migrationsDir?: string;
}
export interface ScriptsTracker {
ensureTrackingFile(): Promise<void>;
track(trackConfig: TrackConfig): Promise<void>;
isTracked(scriptName: string): Promise<boolean>;
}
const DEFAULT_TRACKING_FILE_PATH = '.embark/chains.json';
export class FileSystemTracker implements ScriptsTracker {
private _block: BlockTransactionObject | null = null;
private _web3: Web3 | null = null;
private migrationsDirectory: string;
private trackingFilePath: string;
constructor(private embark: Embark) {
this.trackingFilePath = dappPath(embark.config.contractsConfig?.tracking || DEFAULT_TRACKING_FILE_PATH);
this.migrationsDirectory = embark.config.embarkConfig.migrations;
}
private get web3() {
return (async () => {
if (!this._web3) {
const provider = await this.embark.events.request2('blockchain:client:provider', 'ethereum');
this._web3 = new Web3(provider);
}
return this._web3;
})();
}
get block() {
return (async () => {
if (this._block) {
return this._block;
}
const web3 = await this.web3;
try {
this._block = await web3.eth.getBlock(0, true);
} catch (err) {
// Retry with block 1 (Block 0 fails with Ganache-cli using the --fork option)
this._block = await web3.eth.getBlock(1, true);
}
return this._block;
})();
}
async ensureTrackingFile() {
const fstat = await fs.stat(this.trackingFilePath);
if (!fstat.isFile()) {
const block = await this.block;
if (block) {
await fs.outputJSON(this.trackingFilePath, {
[block.hash]: {
name: this.embark.env,
migrations: []
}
});
}
}
}
async track(trackConfig: TrackConfig) {
const {
scriptName,
scriptDirectory,
forceTracking,
} = trackConfig;
if (forceTracking || scriptDirectory === this.migrationsDirectory) {
const block = await this.block;
if (block) {
const trackingData = await fs.readJSON(this.trackingFilePath) as TrackingData;
if (!trackingData[block.hash].migrations) {
trackingData[block.hash].migrations = [];
}
if (!trackingData[block.hash].migrations.includes(scriptName)) {
trackingData[block.hash].migrations.push(scriptName);
await fs.writeJSON(this.trackingFilePath, trackingData, { spaces: 2 });
}
}
}
}
async isTracked(scriptName: string) {
const block = await this.block;
const trackingData = await fs.readJSON(this.trackingFilePath) as TrackingData;
return (block &&
trackingData &&
trackingData[block.hash]?.migrations &&
trackingData[block.hash]?.migrations.includes(scriptName)) ?? false;
}
}

View File

@ -0,0 +1,171 @@
import sinon from 'sinon';
import assert from 'assert';
import path from 'path';
import { fakeEmbark } from 'embark-testing';
import ScriptsRunnerPlugin, { ScriptsRunnerCommand, ScriptsRunnerEvent } from '../src/';
import { file as tmpFile, dir as tmpDir } from 'tmp-promise';
import { promises } from 'fs';
// Due to our `DAPP_PATH` dependency in `embark-utils` `dappPath()`, we need to
// ensure that this environment variable is defined.
process.env.DAPP_PATH = 'something';
async function prepareScriptFile(content, dir) {
const file = await tmpFile({ postfix: '.js', dir});
await promises.writeFile(file.path, content);
return file;
}
const web3Mock = {
eth: {
getBlock: sinon.spy((number, flag) => { hash: 'testhash'})
}
}
describe('plugins/scripts-runner', () => {
let scriptRunner,
testTracker,
runCodeCommandHandler,
blockchainClientProviderCommandHandler,
contractsListCommandHandler,
embark,
plugins;
beforeEach(async () => {
const testBed = fakeEmbark({
contractsConfig: {
},
embarkConfig: {
migrations: 'migrations'
}
});
testTracker = {
web3: web3Mock,
ensureTrackingFile: sinon.spy((hash, env) => {}),
track: sinon.spy(config => {}),
isTracked: sinon.spy(() => false),
setWeb3: sinon.spy(web3 => {})
};
embark = testBed.embark;
scriptRunner = new ScriptsRunnerPlugin(embark, { tracker: testTracker })
runCodeCommandHandler = sinon.spy((code, cb) => {
// `ScriptsRunnerPlugin` requests code evaluation two times.
// It expects a boolean for the first one and an object for
// the second one.
if (code.indexOf('!==') > 0) {
cb(null, true);
}
cb(null, {});
});
blockchainClientProviderCommandHandler = sinon.spy((name, cb) => {
cb(null, 'http://localhost:8545');
});
contractsListCommandHandler = sinon.spy(cb => {
cb(null, [
{ className: 'SimpleStorage' },
{ className: 'AnotherOne' },
{ className: 'Foo' }
])
});
embark.events.setCommandHandler('contracts:list', contractsListCommandHandler);
embark.events.setCommandHandler('runcode:eval', runCodeCommandHandler);
embark.events.setCommandHandler('blockchain:client:provider', blockchainClientProviderCommandHandler);
await embark.events.request2(ScriptsRunnerCommand.Initialize);
});
afterEach(() => {
embark.teardown();
sinon.restore();
});
it('should execute script', async (done) => {
const scriptFile = await prepareScriptFile(`module.exports = () => { return 'done'; }`);
embark.events.request(ScriptsRunnerCommand.Execute, scriptFile.path, false, (err, result) => {
assert.equal(result, 'done');
scriptFile.cleanup();
done();
});
});
it('should execute all scripts in a directory', async (done) => {
const scriptsDir = await tmpDir();
const scriptFile1 = await prepareScriptFile(
`module.exports = () => { return 'done' }`,
scriptsDir.path
);
const scriptFile2 = await prepareScriptFile(
`module.exports = () => { return 'done2' }`,
scriptsDir.path
);
embark.events.request(ScriptsRunnerCommand.Execute, scriptsDir.path, false, (err, result) => {
assert.ok(result.includes('done'));
assert.ok(result.includes('done2'));
scriptsDir.cleanup();
done();
});
});
it('should force track scripts if --track option is applied', async (done) => {
const scriptFile = await prepareScriptFile(`module.exports = () => { return 'done'; }`);
const expectedResult = {
scriptName: path.basename(scriptFile.path),
scriptDirectory: path.basename(path.dirname(scriptFile.path)),
forceTracking: true,
};
embark.events.request(ScriptsRunnerCommand.Execute, scriptFile.path, true, (err, result) => {
assert.equal(result, 'done');
assert(testTracker.track.calledOnce);
assert(testTracker.track.calledWith(expectedResult));
scriptFile.cleanup();
done();
});
});
it('should track automatically if script directory equals migrations directory', async (done) => {
const scriptsDir = await tmpDir();
const migrationsDir = path.join(scriptsDir.path, embark.config.embarkConfig.migrations)
await promises.mkdir(migrationsDir);
const scriptFile = await prepareScriptFile(
`module.exports = () => { return 'done' }`,
migrationsDir
);
const expectedResult = {
scriptName: path.basename(scriptFile.path),
scriptDirectory: path.basename(migrationsDir),
forceTracking: false,
};
embark.events.request(ScriptsRunnerCommand.Execute, scriptFile.path, false, (err, result) => {
assert.equal(result, 'done');
assert(testTracker.track.calledOnce);
assert(testTracker.track.calledWith(expectedResult));
done();
});
});
it('should not execute script if it was tracked', async(done) => {
const scriptFile = await prepareScriptFile(`module.exports = () => { return 'done'; }`);
embark.events.request(ScriptsRunnerCommand.Execute, scriptFile.path, true, (err, result) => {
assert.equal(result, undefined);
done();
});
});
});

View File

@ -0,0 +1,29 @@
{
"compilerOptions": {
"composite": true,
"declarationDir": "./dist",
"rootDir": "./src",
"tsBuildInfoFile": "./node_modules/.cache/tsc/tsconfig.embark-scripts-runner.tsbuildinfo"
},
"extends": "../../../tsconfig.base.json",
"include": [
"src/**/*"
],
"references": [
{
"path": "../../core/core"
},
{
"path": "../../core/i18n"
},
{
"path": "../../core/logger"
},
{
"path": "../../core/utils"
},
{
"path": "../../utils/testing"
}
]
}

View File

@ -0,0 +1,4 @@
{
"extends": "../../../tslint.json"
}

View File

@ -13,6 +13,7 @@ docs:
environments: environments.html
configuration: configuration.html
pipeline_and_webpack: pipeline_and_webpack.html
executing_migration_scripts: executing_scripts.html
# setting_up_storages: foo.html
# uploading_data: foo.html
# configuring_whisper: foo.html

View File

@ -151,6 +151,16 @@ $ embark reset
Resets embarks state on this dapp including clearing cache.
## exec
```
$ embark exec [environment] [file|directory]
```
Executes a given (migration) script to perform complex after deployment operations.
It's required to specifiy the `environment` in which the script(s) will be executed in. In addition it's possible to specificy a directory in which multiple script live in. Embark will execute them one by one.
## upload
```

View File

@ -0,0 +1,115 @@
title: Executing Scripts
layout: docs
---
There are various features in Embark that help you making the deployment of your DApps and Smart Contracts as smooth as possible. Next to general [Smart Contract Configurations](contracts_configuration.html), [Deployment Hooks](contracts_configuration.html#Deployment-hooks) and [Cockpit](cockpit_introduction.html), there's the possibility to run (migration) scripts as well.
In this guide we'll explore why scripts are useful and how they can be run.
## Why scripts?
Given that Embark supports [afterDeploy](contracts_configuration.html#afterDeploy-hook) hooks that make it extremely easy to perform custom operations after all of your Smart Contracts have been deployed, you might wonder when and where scripts can be useful.
It's important to note that `afterDeploy` hooks are executed every time all Smart Contracts have been deployed. Often there are cases where running a (migration) script manually is what you really need.
Scripts let you do exactly that as they can be run at any time, regardless of what your app's current deployment status is.
## What's a script?
A script is really just a file with an exported function that has special dependencies injected into it. Here's what it could look like:
```
modules.exports = async ({ contracts, web3, logger}) => {
...
};
```
The injected parameters are:
- `contracts` - A map object containing all of your Smart Contracts as Embark Smart Contract instances.
- `web3` - A web3 instances to give you access to things like accounts.
- `logger` - Embark's custom logger.
Scripts can be located anywhere on your machine, but should most likely live inside your project's file tree in a dedicated folder.
## Running scripts
To run a script, use the CLI `exec` command and specify an environment as well as the script to be executed:
```
$ embark exec development scripts/001.js
```
The command above will execute the function in `scripts/001.js` and ensures that Smart Contracts are deployed in the `development` environment.
If you have multiple scripts that should run in order, it's also possible to specify the directory in which they live in:
```
$ embark exec development scripts
```
Embark will then find all script files inside the specified directory (in this case `scripts`) and then run them one by one. If any of the scripts fails by emitting an error, Embark will abort the execution. Scripts are executed in sequence, which means all following scripts won't be executed in case of an error.
## Error Handling
It's possible and recommended for scripts to emit proper errors in case they fail to do their job. There are several ways to emit an error depending on how you write your function. Scripts are executed asyncronously, so one way to emit an error is to reject a promise:
```
modules.exports = () => {
return new Promise((resolve, reject) => {
reject(new Error('Whoops, something went wrong'));
});
};
// or
modules.exports = () => {
return Promise.reject(new Error ('Whoops, something went wrong'));
};
```
If your script uses the `async/await` syntax, errors are emitted by default when using other `async` APIs that fail:
```
module.exports = async () => {
await someAPIThatFails(); // this will emit an error
};
```
If an error is emitted, Embark will do its best to give you information about the original error:
```
001.js running....
Script '001.js' failed to execute. Original error: Error: Whoops, something went wrong
```
## Tracking scripts
Just like Smart Contract deployments are tracked, (migration) scripts can be tracked as well. Since scripts can be one-off operations, Embark will not track whether they have been executed by default. Users are always able to run a script using the `exec` command as discussed in the previous sections.
To have Embark "remember" that a certain script was already run, you can use the `--track` option of the `exec` command, which will force tracking for this particular script:
```
$ embark exec development scripts/001.js --track
```
If we try to run the script again with the `--track` option, Embark will notice that the script has already been executed and tell us that it's "already done".
```
$ embark exec development scripts/001.js --track
.. 001.js already done
```
If however, we don't provide the `--track` flag, Embark will execute the script as usual.
For cases in which we **do** want to track a set of scripts, especially when the main use case are migration operations, we can put our scripts in a special "migrations" directory. All scripts inside that directory will be tracked by default.
The directory can be specified using the `migrations` property in your project's embark.json:
```
{
...
migrations: 'migrations'
}
```
If no such property is specified, Embark will default to "migrations". Running any script or set of scripts is then automatically tracked.

View File

@ -288,6 +288,7 @@ sidebar:
installation: Installation
faq: FAQ
creating_project: Creating apps
executing_migration_scripts: Executing Scripts
structure: App structure
running_apps: Running apps
dashboard: Using the dashboard

View File

@ -15,4 +15,4 @@
"strict": true,
"target": "ESNext"
}
}
}

View File

@ -103,6 +103,9 @@
{
"path": "packages/plugins/scaffolding"
},
{
"path": "packages/plugins/scripts-runner"
},
{
"path": "packages/plugins/snark"
},

View File

@ -18,7 +18,8 @@
"ordered-imports": false,
"quotemark": [false, "single"],
"trailing-comma": false,
"no-irregular-whitespace": false
"no-irregular-whitespace": false,
"max-classes-per-file": false
},
"rulesDirectory": []
}

View File

@ -4512,6 +4512,11 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.14.17.tgz#b96d4dd3e427382482848948041d3754d40fd5ce"
integrity sha512-p/sGgiPaathCfOtqu2fx5Mu1bcjuP8ALFg4xpGgNkcin7LwRyzUKniEHBKdcE1RPsenq5JVPIpMTJSygLboygQ==
"@types/node@^10.5.3":
version "10.17.14"
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.14.tgz#b6c60ebf2fb5e4229fdd751ff9ddfae0f5f31541"
integrity sha512-G0UmX5uKEmW+ZAhmZ6PLTQ5eu/VPaT+d/tdLd5IFsKRPcbe6lPxocBtcYBFSaLaCW8O60AX90e91Nsp8lVHCNw==
"@types/node@^12.6.1":
version "12.12.6"
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.6.tgz#a47240c10d86a9a57bb0c633f0b2e0aea9ce9253"
@ -7820,6 +7825,11 @@ core-js-compat@^3.1.1:
browserslist "^4.6.6"
semver "^6.3.0"
core-js-pure@3.6.4:
version "3.6.4"
resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.6.4.tgz#4bf1ba866e25814f149d4e9aaa08c36173506e3a"
integrity sha512-epIhRLkXdgv32xIUFaaAry2wdxZYBi6bgM7cB136dzzXXa+dFyRLTZeLUJxnd8ShrmyVXBub63n2NHo2JAt8Cw==
core-js-pure@^3.0.0:
version "3.3.3"
resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.3.3.tgz#c6a796e371782394ffb60d82ff67e0e073070093"
@ -22279,6 +22289,14 @@ title-case@^2.1.0:
no-case "^2.2.0"
upper-case "^1.0.3"
tmp-promise@1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/tmp-promise/-/tmp-promise-1.1.0.tgz#bb924d239029157b9bc1d506a6aa341f8b13e64c"
integrity sha512-8+Ah9aB1IRXCnIOxXZ0uFozV1nMU5xiu7hhFVUSxZ3bYu+psD4TzagCzVbexUCgNNGJnsmNDQlS4nG3mTyoNkw==
dependencies:
bluebird "^3.5.0"
tmp "0.1.0"
tmp@0.0.33, tmp@^0.0.33:
version "0.0.33"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
@ -22286,7 +22304,7 @@ tmp@0.0.33, tmp@^0.0.33:
dependencies:
os-tmpdir "~1.0.2"
tmp@^0.1.0:
tmp@0.1.0, tmp@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.1.0.tgz#ee434a4e22543082e294ba6201dcc6eafefa2877"
integrity sha512-J7Z2K08jbGcdA1kkQpJSqLF6T0tdQqpR2pnSUXsIchbPdTI9v3e85cLW0d6WDhwuAleOV71j2xWs8qMPfK7nKw==
@ -23597,6 +23615,20 @@ web3-utils@1.2.6:
underscore "1.9.1"
utf8 "3.0.0"
web3@1.2.4, web3@^1.2.4:
version "1.2.4"
resolved "https://registry.yarnpkg.com/web3/-/web3-1.2.4.tgz#6e7ab799eefc9b4648c2dab63003f704a1d5e7d9"
integrity sha512-xPXGe+w0x0t88Wj+s/dmAdASr3O9wmA9mpZRtixGZxmBexAF0MjfqYM+MS4tVl5s11hMTN3AZb8cDD4VLfC57A==
dependencies:
"@types/node" "^12.6.1"
web3-bzz "1.2.4"
web3-core "1.2.4"
web3-eth "1.2.4"
web3-eth-personal "1.2.4"
web3-net "1.2.4"
web3-shh "1.2.4"
web3-utils "1.2.4"
web3@1.2.6:
version "1.2.6"
resolved "https://registry.yarnpkg.com/web3/-/web3-1.2.6.tgz#c497dcb14cdd8d6d9fb6b445b3b68ff83f8ccf68"
@ -23611,20 +23643,6 @@ web3@1.2.6:
web3-shh "1.2.6"
web3-utils "1.2.6"
web3@^1.2.4:
version "1.2.4"
resolved "https://registry.yarnpkg.com/web3/-/web3-1.2.4.tgz#6e7ab799eefc9b4648c2dab63003f704a1d5e7d9"
integrity sha512-xPXGe+w0x0t88Wj+s/dmAdASr3O9wmA9mpZRtixGZxmBexAF0MjfqYM+MS4tVl5s11hMTN3AZb8cDD4VLfC57A==
dependencies:
"@types/node" "^12.6.1"
web3-bzz "1.2.4"
web3-core "1.2.4"
web3-eth "1.2.4"
web3-eth-personal "1.2.4"
web3-net "1.2.4"
web3-shh "1.2.4"
web3-utils "1.2.4"
webidl-conversions@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"