Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85aba29cf2 | ||
|
|
2c66afbcd7 | ||
|
|
69ad521482 | ||
|
|
d30b4ea4f2 | ||
|
|
51915f7506 | ||
|
|
ebfb4965c7 | ||
|
|
b0fff9a1dd | ||
|
|
2c092e8d04 | ||
|
|
af6190ea09 | ||
|
|
0f95267e84 | ||
|
|
f0bcf930f4 | ||
|
|
0558792282 | ||
|
|
62cca62432 | ||
|
|
7092d33338 | ||
|
|
11a65c525c | ||
|
|
3d57f6030d | ||
|
|
b124bea974 | ||
|
|
f0a7a7d999 | ||
|
|
7d6629e5b4 | ||
|
|
d761f88b30 | ||
|
|
948246cf83 | ||
|
|
e11f2c8c59 | ||
|
|
95fa6280c5 | ||
|
|
fd126c7bea | ||
|
|
f292888195 | ||
|
|
adf91df23a | ||
|
|
ede6ad1c8f | ||
|
|
ee13d5c1df | ||
|
|
eb2b3a0263 | ||
|
|
5088574437 | ||
|
|
d61f4f3cb4 | ||
|
|
1a455741ce | ||
|
|
180c2d963e | ||
|
|
2598704690 | ||
|
|
e2909797fc | ||
|
|
79765c7998 | ||
|
|
9c6549258e | ||
|
|
b4cdf76905 | ||
|
|
622ea44a6f | ||
|
|
d947c8f30a | ||
|
|
481988cb08 | ||
|
|
4580f0fc2e | ||
|
|
eba52442ec | ||
|
|
7ddf728db2 | ||
|
|
829a581727 | ||
|
|
fa03716cb2 | ||
|
|
bd3e841e21 | ||
|
|
8dc9ddf65e | ||
|
|
7d1b334bda | ||
|
|
ade1429ce3 | ||
|
|
5d1ba5903c | ||
|
|
a49f5d7582 | ||
|
|
436d9b9cb1 | ||
|
|
f7d520c933 | ||
|
|
e1a0d894a2 | ||
|
|
ba81766f6c | ||
|
|
b96f6c4d31 | ||
|
|
c5cfa85f4c | ||
|
|
702e839f5b | ||
|
|
ca354de229 |
+2
-5
@@ -5,21 +5,18 @@ go:
|
||||
services:
|
||||
- postgresql
|
||||
addons:
|
||||
postgresql: '9.6'
|
||||
postgresql: '11.2'
|
||||
go_import_path: github.com/vulcanize/vulcanizedb
|
||||
before_install:
|
||||
- make installtools
|
||||
- bash ./scripts/install-postgres-10.sh
|
||||
- bash ./scripts/install-postgres-11.sh
|
||||
- curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
|
||||
- echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
|
||||
- sudo apt-get update && sudo apt-get install yarn
|
||||
before_script:
|
||||
- sudo -u postgres createdb vulcanize_private
|
||||
- make migrate NAME=vulcanize_private
|
||||
- cd postgraphile && yarn
|
||||
script:
|
||||
- yarn test
|
||||
- cd ../
|
||||
- make test
|
||||
- make integrationtest
|
||||
notifications:
|
||||
|
||||
Generated
+17
@@ -321,6 +321,22 @@
|
||||
revision = "c01d1270ff3e442a8a57cddc1c92dc1138598194"
|
||||
version = "v1.2.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:1d7e1867c49a6dd9856598ef7c3123604ea3daabf5b83f303ff457bcbc410b1d"
|
||||
name = "github.com/pkg/errors"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "ba968bfe8b2f7e042a574c888954fccecfa385b4"
|
||||
version = "v0.8.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:fdbe7e05d74cc4d175cc4515a7807a5bb8b66ebe130da382b99713c9038648ae"
|
||||
name = "github.com/pressly/goose"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "e4b98955473e91a12fc7d8816c28d06376d1d92c"
|
||||
version = "v2.6.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:7143292549152d009ca9e9c493b74736a2ebd93f921bea8a4b308d7cc5edc6b3"
|
||||
name = "github.com/rjeczalik/notify"
|
||||
@@ -552,6 +568,7 @@
|
||||
"github.com/onsi/ginkgo",
|
||||
"github.com/onsi/gomega",
|
||||
"github.com/onsi/gomega/ghttp",
|
||||
"github.com/pressly/goose",
|
||||
"github.com/sirupsen/logrus",
|
||||
"github.com/spf13/cobra",
|
||||
"github.com/spf13/viper",
|
||||
|
||||
@@ -7,35 +7,40 @@
|
||||
|
||||
|
||||
## Table of Contents
|
||||
1. [Background](../staging/README.md#background)
|
||||
1. [Dependencies](../staging/README.md#dependencies)
|
||||
1. [Install](../staging/README.md#install)
|
||||
1. [Usage](../staging/README.md#usage)
|
||||
1. [Tests](../staging/README.md#tests)
|
||||
1. [API](../staging/README.md#API)
|
||||
1. [Contributing](../staging/README.md#contributing)
|
||||
1. [License](../staging/README.md#license)
|
||||
1. [Background](#background)
|
||||
1. [Install](#install)
|
||||
1. [Usage](#usage)
|
||||
1. [Contributing](#contributing)
|
||||
1. [License](#license)
|
||||
|
||||
|
||||
## Background
|
||||
The same data structures and encodings that make Ethereum an effective and trust-less distributed virtual machine
|
||||
complicate data accessibility and usability for dApp developers. VulcanizeDB improves Ethereum data accessibility by
|
||||
providing a suite of tools to ease the extraction and transformation of data into a more useful state.
|
||||
complicate data accessibility and usability for dApp developers. VulcanizeDB improves Ethereum data accessibility by
|
||||
providing a suite of tools to ease the extraction and transformation of data into a more useful state, including
|
||||
allowing for exposing aggregate data from a suite of smart contracts.
|
||||
|
||||
VulanizeDB includes processes that sync, transform and expose data. Syncing involves
|
||||
querying an Ethereum node and then persisting core data into a Postgres database. Transforming focuses on using previously synced data to
|
||||
query for and transform log event and storage data for specifically configured smart contract addresses. Exposing data is a matter of getting
|
||||
data from VulcanizeDB's underlying Postgres database and making it accessible.
|
||||
|
||||
## Dependencies
|
||||

|
||||
|
||||
## Install
|
||||
|
||||
1. [Dependencies](#dependencies)
|
||||
1. [Building the project](#building-the-project)
|
||||
1. [Setting up the database](#setting-up-the-database)
|
||||
1. [Configuring a synced Ethereum node](#configuring-a-synced-ethereum-node)
|
||||
|
||||
### Dependencies
|
||||
- Go 1.11+
|
||||
- Postgres 10.6
|
||||
- Postgres 11.2
|
||||
- Ethereum Node
|
||||
- [Go Ethereum](https://ethereum.github.io/go-ethereum/downloads/) (1.8.23+)
|
||||
- [Parity 1.8.11+](https://github.com/paritytech/parity/releases)
|
||||
|
||||
|
||||
## Install
|
||||
1. [Building the project](../staging/README.md#building-the-project)
|
||||
1. [Setting up the database](../staging/README.md#setting-up-the-database)
|
||||
1. [Configuring a synced Ethereum node](../staging/README.md#configuring-a-synced-ethereum-node)
|
||||
|
||||
### Building the project
|
||||
Download the codebase to your local `GOPATH` via:
|
||||
|
||||
@@ -68,7 +73,8 @@ It can be additionally helpful to add `$GOPATH/bin` to your shell's `$PATH`.
|
||||
|
||||
* See below for configuring additional environments
|
||||
|
||||
In some cases (such as recent Ubuntu systems), it may be necessary to overcome failures of password authentication from localhost. To allow access on Ubuntu, set localhost connections via hostname, ipv4, and ipv6 from peer/md5 to trust in: /etc/postgresql/<version>/pg_hba.conf
|
||||
In some cases (such as recent Ubuntu systems), it may be necessary to overcome failures of password authentication from
|
||||
localhost. To allow access on Ubuntu, set localhost connections via hostname, ipv4, and ipv6 from peer/md5 to trust in: /etc/postgresql/<version>/pg_hba.conf
|
||||
|
||||
(It should be noted that trusted auth should only be enabled on systems without sensitive data in them: development and local test databases)
|
||||
|
||||
@@ -80,8 +86,9 @@ In some cases (such as recent Ubuntu systems), it may be necessary to overcome f
|
||||
- The IPC file is called `geth.ipc`.
|
||||
- The geth IPC file path is printed to the console when you start geth.
|
||||
- The default location is:
|
||||
- Mac: `<full home path>/Library/Ethereum`
|
||||
- Mac: `<full home path>/Library/Ethereum/geth.ipc`
|
||||
- Linux: `<full home path>/ethereum/geth.ipc`
|
||||
- Note: the geth.ipc file may not exist until you've started the geth process
|
||||
|
||||
- For Parity:
|
||||
- The IPC file is called `jsonrpc.ipc`.
|
||||
@@ -98,27 +105,35 @@ In some cases (such as recent Ubuntu systems), it may be necessary to overcome f
|
||||
|
||||
|
||||
## Usage
|
||||
Usage is broken up into two processes:
|
||||
As mentioned above, VulcanizeDB's processes can be split into three categories: syncing, transforming and exposing data.
|
||||
|
||||
### Data syncing
|
||||
To provide data for transformations, raw Ethereum data must first be synced into vDB.
|
||||
This is accomplished through the use of the `headerSync`, `sync`, or `coldImport` commands.
|
||||
These commands are described in detail [here](../staging/documentation/sync.md).
|
||||
To provide data for transformations, raw Ethereum data must first be synced into VulcanizeDB.
|
||||
This is accomplished through the use of the `headerSync`, `fullSync`, or `coldImport` commands.
|
||||
These commands are described in detail [here](documentation/data-syncing.md).
|
||||
|
||||
### Data transformation
|
||||
Contract watchers use the raw data that has been synced into Postgres to filter out and apply transformations to specific data of interest.
|
||||
Data transformation uses the raw data that has been synced into Postgres to filter out and apply transformations to
|
||||
specific data of interest. Since there are different types of data that may be useful for observing smart contracts, it
|
||||
follows that there are different ways to transform this data. We've started by categorizing this into Generic and
|
||||
Custom transformers:
|
||||
|
||||
There is a built-in `contractWatcher` command which provides generic transformation of most contract data.
|
||||
The `contractWatcher` command is described further [here](../staging/documentation/contractWatcher.md).
|
||||
- Generic Contract Transformer: Generic contract transformation can be done using a built-in command,
|
||||
`contractWatcher`, which transforms contract events provided the contract's ABI is available. It also
|
||||
provides some state variable coverage by automating polling of public methods, with some restrictions.
|
||||
`contractWatcher` is described further [here](documentation/generic-transformer.md).
|
||||
|
||||
In many cases a custom transformer or set of transformers will need to be written to provide complete or more comprehensive coverage or to optimize other aspects of the output for a specific end-use.
|
||||
In this case we have provided the `compose`, `execute`, and `composeAndExecute` commands for running custom transformers from external repositories.
|
||||
- Custom Transformers: In many cases custom transformers will need to be written to provide
|
||||
more comprehensive coverage of contract data. In this case we have provided the `compose`, `execute`, and
|
||||
`composeAndExecute` commands for running custom transformers from external repositories. Documentation on how to write,
|
||||
build and run custom transformers as Go plugins can be found
|
||||
[here](documentation/custom-transformers.md).
|
||||
|
||||
Usage of the `compose`, `execute`, and `composeAndExecute` commands is described further [here](../staging/documentation/composeAndExecute.md).
|
||||
### Exposing the data
|
||||
[Postgraphile](https://www.graphile.org/postgraphile/) is used to expose GraphQL endpoints for our database schemas, this is described in detail [here](documentation/postgraphile.md).
|
||||
|
||||
Documentation on how to build custom transformers to work with these commands can be found [here](../staging/documentation/transformers.md).
|
||||
|
||||
## Tests
|
||||
### Tests
|
||||
- Replace the empty `ipcPath` in the `environments/infura.toml` with a path to a full node's eth_jsonrpc endpoint (e.g. local geth node ipc path or infura url)
|
||||
- Note: integration tests require configuration with an archival node
|
||||
- `createdb vulcanize_private` will create the test db
|
||||
@@ -126,15 +141,13 @@ Documentation on how to build custom transformers to work with these commands ca
|
||||
- `make test` will run the unit tests and skip the integration tests
|
||||
- `make integrationtest` will run just the integration tests
|
||||
|
||||
## API
|
||||
[Postgraphile](https://www.graphile.org/postgraphile/) is used to expose GraphQL endpoints for our database schemas, this is described in detail [here](../staging/postgraphile/README.md).
|
||||
|
||||
|
||||
## Contributing
|
||||
Contributions are welcome! For more on this, please see [here](../staging/documentation/contributing.md).
|
||||
Contributions are welcome!
|
||||
|
||||
Small note: If editing the Readme, please conform to the [standard-readme specification](https://github.com/RichardLitt/standard-readme).
|
||||
VulcanizeDB follows the [Contributor Covenant Code of Conduct](https://www.contributor-covenant.org/version/1/4/code-of-conduct).
|
||||
|
||||
For more information on contributing, please see [here](documentation/contributing.md).
|
||||
|
||||
## License
|
||||
[AGPL-3.0](../staging/LICENSE) © Vulcanize Inc
|
||||
[AGPL-3.0](LICENSE) © Vulcanize Inc
|
||||
@@ -193,5 +193,5 @@ func composeAndExecute() {
|
||||
func init() {
|
||||
rootCmd.AddCommand(composeAndExecuteCmd)
|
||||
composeAndExecuteCmd.Flags().BoolVarP(&recheckHeadersArg, "recheck-headers", "r", false, "whether to re-check headers for watched events")
|
||||
composeAndExecuteCmd.Flags().DurationVarP(&queueRecheckInterval, "queue-recheck-interval", "q", 5*time.Minute, "how often to recheck queued storage diffs")
|
||||
composeAndExecuteCmd.Flags().DurationVarP(&queueRecheckInterval, "queue-recheck-interval", "q", 5*time.Minute, "interval duration for rechecking queued storage diffs (ex: 5m30s)")
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ import (
|
||||
|
||||
st "github.com/vulcanize/vulcanizedb/libraries/shared/transformer"
|
||||
ft "github.com/vulcanize/vulcanizedb/pkg/contract_watcher/full/transformer"
|
||||
lt "github.com/vulcanize/vulcanizedb/pkg/contract_watcher/header/transformer"
|
||||
ht "github.com/vulcanize/vulcanizedb/pkg/contract_watcher/header/transformer"
|
||||
"github.com/vulcanize/vulcanizedb/utils"
|
||||
)
|
||||
|
||||
@@ -99,7 +99,7 @@ func contractWatcher() {
|
||||
con.PrepConfig()
|
||||
switch mode {
|
||||
case "header":
|
||||
t = lt.NewTransformer(con, blockChain, &db)
|
||||
t = ht.NewTransformer(con, blockChain, &db)
|
||||
case "full":
|
||||
t = ft.NewTransformer(con, blockChain, &db)
|
||||
default:
|
||||
|
||||
+4
-6
@@ -17,7 +17,6 @@ package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"plugin"
|
||||
syn "sync"
|
||||
"time"
|
||||
@@ -82,7 +81,7 @@ func execute() {
|
||||
log.Info("linking plugin", pluginPath)
|
||||
plug, err := plugin.Open(pluginPath)
|
||||
if err != nil {
|
||||
log.Debug("linking plugin failed")
|
||||
log.Warn("linking plugin failed")
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -90,15 +89,14 @@ func execute() {
|
||||
log.Info("loading transformers from plugin")
|
||||
symExporter, err := plug.Lookup("Exporter")
|
||||
if err != nil {
|
||||
log.Debug("loading Exporter symbol failed")
|
||||
log.Warn("loading Exporter symbol failed")
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Assert that the symbol is of type Exporter
|
||||
exporter, ok := symExporter.(Exporter)
|
||||
if !ok {
|
||||
log.Debug("plugged-in symbol not of type Exporter")
|
||||
os.Exit(1)
|
||||
log.Fatal("plugged-in symbol not of type Exporter")
|
||||
}
|
||||
|
||||
// Use the Exporters export method to load the EventTransformerInitializer, StorageTransformerInitializer, and ContractTransformerInitializer sets
|
||||
@@ -139,7 +137,7 @@ func execute() {
|
||||
func init() {
|
||||
rootCmd.AddCommand(executeCmd)
|
||||
executeCmd.Flags().BoolVarP(&recheckHeadersArg, "recheck-headers", "r", false, "whether to re-check headers for watched events")
|
||||
executeCmd.Flags().DurationVarP(&queueRecheckInterval, "queue-recheck-interval", "q", 5*time.Minute, "how often to recheck queued storage diffs")
|
||||
executeCmd.Flags().DurationVarP(&queueRecheckInterval, "queue-recheck-interval", "q", 5*time.Minute, "interval duration for rechecking queued storage diffs (ex: 5m30s)")
|
||||
}
|
||||
|
||||
type Exporter interface {
|
||||
|
||||
@@ -29,14 +29,14 @@ import (
|
||||
"github.com/vulcanize/vulcanizedb/utils"
|
||||
)
|
||||
|
||||
// syncCmd represents the sync command
|
||||
var syncCmd = &cobra.Command{
|
||||
Use: "sync",
|
||||
// fullSyncCmd represents the fullSync command
|
||||
var fullSyncCmd = &cobra.Command{
|
||||
Use: "fullSync",
|
||||
Short: "Syncs VulcanizeDB with local ethereum node",
|
||||
Long: `Syncs VulcanizeDB with local ethereum node. Populates
|
||||
Postgres with blocks, transactions, receipts, and logs.
|
||||
|
||||
./vulcanizedb sync --starting-block-number 0 --config public.toml
|
||||
./vulcanizedb fullSync --starting-block-number 0 --config public.toml
|
||||
|
||||
Expects ethereum node to be running and requires a .toml config:
|
||||
|
||||
@@ -49,14 +49,14 @@ Expects ethereum node to be running and requires a .toml config:
|
||||
ipcPath = "/Users/user/Library/Ethereum/geth.ipc"
|
||||
`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
sync()
|
||||
fullSync()
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(syncCmd)
|
||||
rootCmd.AddCommand(fullSyncCmd)
|
||||
|
||||
syncCmd.Flags().Int64VarP(&startingBlockNumber, "starting-block-number", "s", 0, "Block number to start syncing from")
|
||||
fullSyncCmd.Flags().Int64VarP(&startingBlockNumber, "starting-block-number", "s", 0, "Block number to start syncing from")
|
||||
}
|
||||
|
||||
func backFillAllBlocks(blockchain core.BlockChain, blockRepository datastore.BlockRepository, missingBlocksPopulated chan int, startingBlockNumber int64) {
|
||||
@@ -67,20 +67,20 @@ func backFillAllBlocks(blockchain core.BlockChain, blockRepository datastore.Blo
|
||||
missingBlocksPopulated <- populated
|
||||
}
|
||||
|
||||
func sync() {
|
||||
func fullSync() {
|
||||
ticker := time.NewTicker(pollingInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
blockChain := getBlockChain()
|
||||
lastBlock, err := blockChain.LastBlock()
|
||||
if err != nil {
|
||||
log.Error("sync: Error getting last block: ", err)
|
||||
log.Error("fullSync: Error getting last block: ", err)
|
||||
}
|
||||
if lastBlock.Int64() == 0 {
|
||||
log.Fatal("geth initial: state sync not finished")
|
||||
}
|
||||
if startingBlockNumber > lastBlock.Int64() {
|
||||
log.Fatal("sync: starting block number > current block number")
|
||||
log.Fatal("fullSync: starting block number > current block number")
|
||||
}
|
||||
|
||||
db := utils.LoadPostgres(databaseConfig, blockChain.Node())
|
||||
@@ -94,9 +94,9 @@ func sync() {
|
||||
case <-ticker.C:
|
||||
window, err := validator.ValidateBlocks()
|
||||
if err != nil {
|
||||
log.Error("sync: error in validateBlocks: ", err)
|
||||
log.Error("fullSync: error in validateBlocks: ", err)
|
||||
}
|
||||
log.Info(window.GetString())
|
||||
log.Debug(window.GetString())
|
||||
case <-missingBlocksPopulated:
|
||||
go backFillAllBlocks(blockChain, blockRepository, missingBlocksPopulated, startingBlockNumber)
|
||||
}
|
||||
+1
-1
@@ -88,7 +88,7 @@ func headerSync() {
|
||||
if err != nil {
|
||||
log.Error("headerSync: ValidateHeaders failed: ", err)
|
||||
}
|
||||
log.Info(window.GetString())
|
||||
log.Debug(window.GetString())
|
||||
case n := <-missingBlocksPopulated:
|
||||
if n == 0 {
|
||||
time.Sleep(3 * time.Second)
|
||||
|
||||
+26
-8
@@ -18,7 +18,6 @@ package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -56,17 +55,26 @@ const (
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "vulcanizedb",
|
||||
PersistentPreRun: database,
|
||||
PersistentPreRun: initFuncs,
|
||||
}
|
||||
|
||||
func Execute() {
|
||||
log.Info("----- Starting vDB -----")
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
log.Fatal(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func initFuncs(cmd *cobra.Command, args []string) {
|
||||
database(cmd, args)
|
||||
|
||||
logLvlErr := logLevel(cmd, args)
|
||||
if logLvlErr != nil {
|
||||
log.Fatal("Could not set log level: ", logLvlErr)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func database(cmd *cobra.Command, args []string) {
|
||||
ipc = viper.GetString("client.ipcpath")
|
||||
levelDbPath = viper.GetString("client.leveldbpath")
|
||||
@@ -81,6 +89,16 @@ func database(cmd *cobra.Command, args []string) {
|
||||
viper.Set("database.config", databaseConfig)
|
||||
}
|
||||
|
||||
func logLevel(cmd *cobra.Command, args []string) error {
|
||||
lvl, err := log.ParseLevel(viper.GetString("log.level"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.SetLevel(lvl)
|
||||
log.Info("Log level set to ", lvl.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
cobra.OnInitialize(initConfig)
|
||||
// When searching for env variables, replace dots in config keys with underscores
|
||||
@@ -97,6 +115,7 @@ func init() {
|
||||
rootCmd.PersistentFlags().String("client-levelDbPath", "", "location of levelDb chaindata")
|
||||
rootCmd.PersistentFlags().String("filesystem-storageDiffsPath", "", "location of storage diffs csv file")
|
||||
rootCmd.PersistentFlags().String("exporter-name", "exporter", "name of exporter plugin")
|
||||
rootCmd.PersistentFlags().String("log-level", log.InfoLevel.String(), "Log level (trace, debug, info, warn, error, fatal, panic")
|
||||
|
||||
viper.BindPFlag("database.name", rootCmd.PersistentFlags().Lookup("database-name"))
|
||||
viper.BindPFlag("database.port", rootCmd.PersistentFlags().Lookup("database-port"))
|
||||
@@ -107,6 +126,7 @@ func init() {
|
||||
viper.BindPFlag("client.levelDbPath", rootCmd.PersistentFlags().Lookup("client-levelDbPath"))
|
||||
viper.BindPFlag("filesystem.storageDiffsPath", rootCmd.PersistentFlags().Lookup("filesystem-storageDiffsPath"))
|
||||
viper.BindPFlag("exporter.fileName", rootCmd.PersistentFlags().Lookup("exporter-name"))
|
||||
viper.BindPFlag("log.level", rootCmd.PersistentFlags().Lookup("log-level"))
|
||||
}
|
||||
|
||||
func initConfig() {
|
||||
@@ -116,16 +136,14 @@ func initConfig() {
|
||||
noConfigError := "No config file passed with --config flag"
|
||||
fmt.Println("Error: ", noConfigError)
|
||||
log.Fatal(noConfigError)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := viper.ReadInConfig(); err == nil {
|
||||
log.Printf("Using config file: %s\n\n", viper.ConfigFileUsed())
|
||||
} else {
|
||||
invalidConfigError := "Couldn't read config file"
|
||||
fmt.Println("Error: ", invalidConfigError)
|
||||
log.Fatal(invalidConfigError)
|
||||
os.Exit(1)
|
||||
formattedError := fmt.Sprintf("%s: %s", invalidConfigError, err.Error())
|
||||
log.Fatal(formattedError)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,7 +151,7 @@ func getBlockChain() *geth.BlockChain {
|
||||
rawRpcClient, err := rpc.Dial(ipc)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
log.Fatal("Could not dial client: ", err)
|
||||
}
|
||||
rpcClient := client.NewRpcClient(rawRpcClient, ipc)
|
||||
ethClient := ethclient.NewClient(rawRpcClient)
|
||||
|
||||
@@ -5,7 +5,8 @@ CREATE TABLE public.queued_storage (
|
||||
block_hash BYTEA,
|
||||
contract BYTEA,
|
||||
storage_key BYTEA,
|
||||
storage_value BYTEA
|
||||
storage_value BYTEA,
|
||||
UNIQUE (block_height, block_hash, contract, storage_key, storage_value)
|
||||
);
|
||||
|
||||
-- +goose Down
|
||||
|
||||
+11
-16
@@ -2,8 +2,8 @@
|
||||
-- PostgreSQL database dump
|
||||
--
|
||||
|
||||
-- Dumped from database version 10.6
|
||||
-- Dumped by pg_dump version 10.6
|
||||
-- Dumped from database version 11.3
|
||||
-- Dumped by pg_dump version 11.3
|
||||
|
||||
SET statement_timeout = 0;
|
||||
SET lock_timeout = 0;
|
||||
@@ -12,23 +12,10 @@ SET client_encoding = 'UTF8';
|
||||
SET standard_conforming_strings = on;
|
||||
SELECT pg_catalog.set_config('search_path', '', false);
|
||||
SET check_function_bodies = false;
|
||||
SET xmloption = content;
|
||||
SET client_min_messages = warning;
|
||||
SET row_security = off;
|
||||
|
||||
--
|
||||
-- Name: plpgsql; Type: EXTENSION; Schema: -; Owner: -
|
||||
--
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS plpgsql WITH SCHEMA pg_catalog;
|
||||
|
||||
|
||||
--
|
||||
-- Name: EXTENSION plpgsql; Type: COMMENT; Schema: -; Owner: -
|
||||
--
|
||||
|
||||
COMMENT ON EXTENSION plpgsql IS 'PL/pgSQL procedural language';
|
||||
|
||||
|
||||
SET default_tablespace = '';
|
||||
|
||||
SET default_with_oids = false;
|
||||
@@ -803,6 +790,14 @@ ALTER TABLE ONLY public.eth_nodes
|
||||
ADD CONSTRAINT nodes_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: queued_storage queued_storage_block_height_block_hash_contract_storage_key_key; Type: CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.queued_storage
|
||||
ADD CONSTRAINT queued_storage_block_height_block_hash_contract_storage_key_key UNIQUE (block_height, block_hash, contract, storage_key, storage_value);
|
||||
|
||||
|
||||
--
|
||||
-- Name: queued_storage queued_storage_pkey; Type: CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
@@ -1,11 +1,50 @@
|
||||
# Contribution guidelines
|
||||
|
||||
Contributions are welcome! In addition to core contributions, developers are encouraged to build their own custom transformers which
|
||||
can be run together with other custom transformers using the [composeAndExeucte](../../staging/documentation/composeAndExecute.md) command.
|
||||
Contributions are welcome! Please open an Issues or Pull Request for any changes.
|
||||
|
||||
In addition to core contributions, developers are encouraged to build their own custom transformers which
|
||||
can be run together with other custom transformers using the [composeAndExecute](../../staging/documentation/custom-transformers.md) command.
|
||||
|
||||
## Pull Requests
|
||||
- `go fmt` is run as part of `make test` and `make integrationtest`, please make sure to check in the format changes.
|
||||
- Ensure that new code is well tested, including integration testing if applicable.
|
||||
- Make sure the build is passing.
|
||||
- Update the README or any [documentation files](./) as necessary. If editing the Readme, please
|
||||
conform to the
|
||||
[standard-readme specification](https://github.com/RichardLitt/standard-readme).
|
||||
- Once a Pull Request has received two approvals it can be merged in by a core developer.
|
||||
|
||||
## Creating a new migration file
|
||||
1. `make new_migration NAME=add_columnA_to_table1`
|
||||
- This will create a new timestamped migration file in `db/migrations`
|
||||
1. Write the migration code in the created file, under the respective `goose` pragma
|
||||
- Goose automatically runs each migration in a transaction; don't add `BEGIN` and `COMMIT` statements.
|
||||
1. Core migrations should be committed in their `goose fix`ed form.
|
||||
1. Core migrations should be committed in their `goose fix`ed form. To do this, run `make version_migrations` which
|
||||
converts timestamped migrations to migrations versioned by an incremented integer.
|
||||
|
||||
## Diagrams
|
||||
- Diagrams were created with [draw.io](https://www.draw.io).
|
||||
- To update a diagram:
|
||||
1. Go to [draw.io](https://www.draw.io).
|
||||
1. Click on *File > Open from* and choose the location of the diagram you want to update.
|
||||
1. Once open in draw.io, you may update it.
|
||||
1. Export the diagram to this repository's directory and commit it.
|
||||
|
||||
|
||||
## Generating the Changelog
|
||||
We use [github-changelog-generator](https://github.com/github-changelog-generator/github-changelog-generator) to generate release Changelogs. To be consistent with previous Changelogs, the following flags should be passed to the command:
|
||||
|
||||
```
|
||||
--user vulcanize
|
||||
--project vulcanizedb
|
||||
--token {YOUR_GITHUB_TOKEN}
|
||||
--no-issues
|
||||
--usernames-as-github-logins
|
||||
--since-tag {PREVIOUS_RELEASE_TAG}
|
||||
```
|
||||
|
||||
For more information on why your github token is needed, and how to generate it see [https://github
|
||||
.com/github-changelog-generator/github-changelog-generator#github-token](https://github.com/github-changelog-generator/github-changelog-generator#github-token).
|
||||
|
||||
## Code of Conduct
|
||||
VulcanizeDB follows the [Contributor Covenant Code of Conduct](https://www.contributor-covenant.org/version/1/4/code-of-conduct).
|
||||
|
||||
@@ -1,44 +1,80 @@
|
||||
# composeAndExecute
|
||||
The `composeAndExecute` command is used to compose and execute over an arbitrary set of custom transformers.
|
||||
This is accomplished by generating a Go pluggin which allows the `vulcanizedb` binary to link to external transformers, so
|
||||
long as they abide by one of the standard [interfaces](../staging/libraries/shared/transformer).
|
||||
# Custom Transformers
|
||||
When the capabilities of the generic `contractWatcher` are not sufficient, custom transformers tailored to a specific
|
||||
purpose can be leveraged.
|
||||
|
||||
Additionally, there are separate `compose` and `execute` commands to allow pre-building and linking to a pre-built .so file.
|
||||
Individual custom transformers can be composed together from any number of external repositories and executed as a
|
||||
single process using the `compose` and `execute` commands or the `composeAndExecute` command. This is accomplished by
|
||||
generating a Go plugin which allows the `vulcanizedb` binary to link to the external transformers, so long as they
|
||||
abide by one of the standard [interfaces](../staging/libraries/shared/transformer).
|
||||
|
||||
**NOTE:**
|
||||
1. It is necessary that the .so file was built with the same exact dependencies that are present in the execution environment,
|
||||
i.e. we need to `compose` and `execute` the plugin .so file with the same exact version of vulcanizeDB.
|
||||
1. The plugin migrations are run during the plugin's composition. As such, if `execute` is used to run a prebuilt .so in a different
|
||||
environment than the one it was composed in then the migrations for that plugin will first need to be manually ran against that environment's Postgres database.
|
||||
## Writing custom transformers
|
||||
For help with writing different types of custom transformers please see below:
|
||||
|
||||
These commands require Go 1.11+ and use [Go plugins](https://golang.org/pkg/plugin/) which only work on Unix-based systems.
|
||||
There is also an ongoing [conflict](https://github.com/golang/go/issues/20481) between Go plugins and the use vendored dependencies which
|
||||
imposes certain limitations on how the plugins are built.
|
||||
Storage Transformers: transform data derived from contract storage tries
|
||||
* [Guide](../../staging/libraries/shared/factories/storage/README.md)
|
||||
* [Example](../../staging/libraries/shared/factories/storage/EXAMPLE.md)
|
||||
|
||||
## Commands
|
||||
The `compose` and `composeAndExecute` commands assume you are in the vulcanizdb directory located at your system's `$GOPATH`,
|
||||
and that all of the transformer repositories for building the plugin are present at their `$GOPATH` directories.
|
||||
Event Transformers: transform data derived from Ethereum log events
|
||||
* [Guide](../../staging/libraries/shared/factories/event/README.md)
|
||||
* [Example 1](https://github.com/vulcanize/ens_transformers/tree/master/transformers/registar)
|
||||
* [Example 2](https://github.com/vulcanize/ens_transformers/tree/master/transformers/registry)
|
||||
* [Example 3](https://github.com/vulcanize/ens_transformers/tree/master/transformers/resolver)
|
||||
|
||||
The `execute` command does not require the plugin transformer dependencies be located in their
|
||||
`$GOPATH` directories, instead it expects a prebuilt .so file (of the name specified in the config file)
|
||||
to be in `$GOPATH/src/github.com/vulcanize/vulcanizedb/plugins/` and, as noted above, also expects the plugin
|
||||
db migrations to have already been ran against the database.
|
||||
Contract Transformers: transform data derived from Ethereum log events and use it to poll public contract methods
|
||||
* [Example 1](https://github.com/vulcanize/account_transformers)
|
||||
* [Example 2](https://github.com/vulcanize/ens_transformers/tree/master/transformers/domain_records)
|
||||
|
||||
compose:
|
||||
## Preparing custom transformers to work as part of a plugin
|
||||
To plug in an external transformer we need to:
|
||||
|
||||
`./vulcanizedb compose --config=./environments/config_name.toml`
|
||||
1. Create a package that exports a variable `TransformerInitializer`, `StorageTransformerInitializer`, or `ContractTransformerInitializer` that are of type [TransformerInitializer](../staging/libraries/shared/transformer/event_transformer.go#L33)
|
||||
or [StorageTransformerInitializer](../../staging/libraries/shared/transformer/storage_transformer.go#L31),
|
||||
or [ContractTransformerInitializer](../../staging/libraries/shared/transformer/contract_transformer.go#L31), respectively
|
||||
2. Design the transformers to work in the context of their [event](../staging/libraries/shared/watcher/event_watcher.go#L83),
|
||||
[storage](../../staging/libraries/shared/watcher/storage_watcher.go#L53),
|
||||
or [contract](../../staging/libraries/shared/watcher/contract_watcher.go#L68) watcher execution modes
|
||||
3. Create db migrations to run against vulcanizeDB so that we can store the transformer output
|
||||
* Do not `goose fix` the transformer migrations, this is to ensure they are always ran after the core vulcanizedb migrations which are kept in their fixed form
|
||||
* Specify migration locations for each transformer in the config with the `exporter.transformer.migrations` fields
|
||||
* If the base vDB migrations occupy this path as well, they need to be in their `goose fix`ed form
|
||||
as they are [here](../../staging/db/migrations)
|
||||
|
||||
execute:
|
||||
To update a plugin repository with changes to the core vulcanizedb repository, run `dep ensure` to update its dependencies.
|
||||
|
||||
`./vulcanizedb execute --config=./environments/config_name.toml`
|
||||
## Building and Running Custom Transformers
|
||||
### Commands
|
||||
* The `compose`, `execute`, `composeAndExecute` commands require Go 1.11+ and use [Go plugins](https://golang
|
||||
.org/pkg/plugin/) which only work on Unix-based systems.
|
||||
|
||||
composeAndExecute:
|
||||
* There is an ongoing [conflict](https://github.com/golang/go/issues/20481) between Go plugins and the use of vendored
|
||||
dependencies which imposes certain limitations on how the plugins are built.
|
||||
|
||||
`./vulcanizedb composeAndExecute --config=./environments/config_name.toml`
|
||||
* Separate `compose` and `execute` commands allow pre-building and linking to the pre-built .so file. So, if
|
||||
these are run independently, instead of using `composeAndExecute`, a couple of things need to be considered:
|
||||
* It is necessary that the .so file was built with the same exact dependencies that are present in the execution
|
||||
environment, i.e. we need to `compose` and `execute` the plugin .so file with the same exact version of vulcanizeDB.
|
||||
* The plugin migrations are run during the plugin's composition. As such, if `execute` is used to run a prebuilt .so
|
||||
in a different environment than the one it was composed in, then the database structure will need to be loaded
|
||||
into the environment's Postgres database. This can either be done by manually loading the plugin's schema into
|
||||
Postgres, or by manually running the plugin's migrations.
|
||||
|
||||
* The `compose` and `composeAndExecute` commands assume you are in the vulcanizdb directory located at your system's
|
||||
`$GOPATH`, and that the plugin dependencies are present at their `$GOPATH` directories.
|
||||
|
||||
## Flags
|
||||
* The `execute` command does not require the plugin transformer dependencies be located in their `$GOPATH` directories,
|
||||
instead it expects a .so file (of the name specified in the config file) to be in
|
||||
`$GOPATH/src/github.com/vulcanize/vulcanizedb/plugins/` and, as noted above, also expects the plugin db migrations to
|
||||
have already been ran against the database.
|
||||
|
||||
The `compose` and `composeAndExecute` commands can be passed optional flags to specify the operation of the watchers:
|
||||
* Usage:
|
||||
* compose: `./vulcanizedb compose --config=environments/config_name.toml`
|
||||
|
||||
* execute: `./vulcanizedb execute --config=environments/config_name.toml`
|
||||
|
||||
* composeAndExecute: `./vulcanizedb composeAndExecute --config=environments/config_name.toml`
|
||||
|
||||
### Flags
|
||||
The `execute` and `composeAndExecute` commands can be passed optional flags to specify the operation of the watchers:
|
||||
|
||||
- `--recheck-headers`/`-r` - specifies whether to re-check headers for events after the header has already been queried for watched logs.
|
||||
Can be useful for redundancy if you suspect that your node is not always returning all desired logs on every query.
|
||||
@@ -50,7 +86,7 @@ Defaults to `false`.
|
||||
Argument is expected to be a duration (integer measured in nanoseconds): e.g. `-q=10m30s` (for 10 minute, 30 second intervals).
|
||||
Defaults to `5m` (5 minutes).
|
||||
|
||||
## Configuration
|
||||
### Configuration
|
||||
A .toml config file is specified when executing the commands.
|
||||
The config provides information for composing a set of transformers from external repositories:
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
# Syncing commands
|
||||
These commands are used to sync raw Ethereum data into Postgres, with varying levels of data granularity.
|
||||
|
||||
## headerSync
|
||||
Syncs block headers from a running Ethereum node into the VulcanizeDB table `headers`.
|
||||
- Queries the Ethereum node using RPC calls.
|
||||
- Validates headers from the last 15 blocks to ensure that data is up to date.
|
||||
- Useful when you want a minimal baseline from which to track targeted data on the blockchain (e.g. individual smart
|
||||
contract storage values or event logs).
|
||||
- Handles chain reorgs by [validating the most recent blocks' hashes](../pkg/history/header_validator.go). If the hash is
|
||||
different from what we have already stored in the database, the header record will be updated.
|
||||
|
||||
#### Usage
|
||||
- Run: `./vulcanizedb headerSync --config <config.toml> --starting-block-number <block-number>`
|
||||
- The config file must be formatted as follows, and should contain an ipc path to a running Ethereum node:
|
||||
```toml
|
||||
[database]
|
||||
name = "vulcanize_public"
|
||||
hostname = "localhost"
|
||||
user = "vulcanize"
|
||||
password = "vulcanize"
|
||||
port = 5432
|
||||
|
||||
[client]
|
||||
ipcPath = <path to a running Ethereum node>
|
||||
```
|
||||
- Alternatively, the ipc path can be passed as a flag instead `--client-ipcPath`.
|
||||
|
||||
## fullSync
|
||||
Syncs blocks, transactions, receipts and logs from a running Ethereum node into VulcanizeDB tables named
|
||||
`blocks`, `uncles`, `full_sync_transactions`, `full_sync_receipts` and `logs`.
|
||||
- Queries the Ethereum node using RPC calls.
|
||||
- Validates headers from the last 15 blocks to ensure that data is up to date.
|
||||
- Useful when you want to maintain a broad cache of what's happening on the blockchain.
|
||||
- Handles chain reorgs by [validating the most recent blocks' hashes](../pkg/history/header_validator.go). If the hash is
|
||||
different from what we have already stored in the database, the header record will be updated.
|
||||
|
||||
#### Usage
|
||||
- Run `./vulcanizedb fullSync --config <config.toml> --starting-block-number <block-number>`
|
||||
- The config file must be formatted as follows, and should contain an ipc path to a running Ethereum node:
|
||||
```toml
|
||||
[database]
|
||||
name = "vulcanize_public"
|
||||
hostname = "localhost"
|
||||
user = "vulcanize"
|
||||
password = "vulcanize"
|
||||
port = 5432
|
||||
|
||||
[client]
|
||||
ipcPath = <path to a running Ethereum node>
|
||||
```
|
||||
- Alternatively, the ipc path can be passed as a flag instead `--client-ipcPath`.
|
||||
|
||||
*Please note, that if you are fast syncing your Ethereum node, wait for the initial sync to finish.*
|
||||
|
||||
## coldImport
|
||||
Syncs VulcanizeDB from Geth's underlying LevelDB datastore and persists Ethereum blocks,
|
||||
transactions, receipts and logs into VulcanizeDB tables named `blocks`, `uncles`,
|
||||
`full_sync_transactions`, `full_sync_receipts` and `logs` respectively.
|
||||
|
||||
#### Usage
|
||||
1. Ensure the Ethereum node you're point at is not running, and that it has synced to the desired block height.
|
||||
1. Run `./vulcanizedb coldImport --config <config.toml>`
|
||||
1. Optional flags:
|
||||
- `--starting-block-number <block number>`/`-s <block number>`: block number to start syncing from
|
||||
- `--ending-block-number <block number>`/`-e <block number>`: block number to sync to
|
||||
- `--all`/`-a`: sync all missing blocks
|
||||
|
||||
The config file can be formatted as follows, and must contain the LevelDB path.
|
||||
|
||||
```toml
|
||||
[database]
|
||||
name = "vulcanize_public"
|
||||
hostname = "localhost"
|
||||
user = "vulcanize"
|
||||
password = "vulcanize"
|
||||
port = 5432
|
||||
|
||||
[client]
|
||||
leveldbpath = "/Users/user/Library/Ethereum/geth/chaindata"
|
||||
```
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
@@ -1,4 +1,4 @@
|
||||
# contractWatcher
|
||||
# Generic Transformer
|
||||
The `contractWatcher` command is a built-in generic contract watcher. It can watch any and all events for a given contract provided the contract's ABI is available.
|
||||
It also provides some state variable coverage by automating polling of public methods, with some restrictions:
|
||||
1. The method must have 2 or less arguments
|
||||
@@ -0,0 +1,34 @@
|
||||
# Postgraphile
|
||||
|
||||
You can expose VulcanizeDB data via [Postgraphile](https://github.com/graphile/postgraphile).
|
||||
Check out [their documentation](https://www.graphile.org/postgraphile/) for the most up-to-date instructions on installing, running, and customizing Postgraphile.
|
||||
|
||||
## Simple Setup
|
||||
|
||||
As of April 30, 2019, you can run Postgraphile pointed at the default `vulcanize_public` database with the following commands:
|
||||
|
||||
```
|
||||
npm install -g postgraphile
|
||||
postgraphile --connection postgres://localhost/vulcanize_public --schema=public,custom --disable-default-mutations --no-ignore-rbac
|
||||
```
|
||||
|
||||
Arguments:
|
||||
- `--connection` specifies the database. The above command connects to the default `vulcanize_public` database
|
||||
defined in [the example config](../environments/public.toml.example).
|
||||
- `--schema` defines what schema(s) to expose. The above exposes the `public` schema (for core VulcanizeDB data) as well as a `custom` schema (where `custom` is the name of a schema defined in executed transformers).
|
||||
- `--disable-default-mutations` prevents Postgraphile from exposing create, update, and delete operations on your data, which are otherwise enabled by default.
|
||||
- `--no-ignore-rbac` ensures that Postgraphile will only expose the tables, columns, fields, and query functions that the user has explicit access to.
|
||||
|
||||
## Customizing Postgraphile
|
||||
|
||||
By default, Postgraphile will expose queries for all tables defined in your chosen database/schema(s), including [filtering](https://www.graphile.org/postgraphile/filtering/) and [auto-discovered relations](https://www.graphile.org/postgraphile/relations/).
|
||||
|
||||
If you'd like to expose more customized windows into your data, there are some techniques you can apply when writing migrations:
|
||||
|
||||
- [Computed columns](https://www.graphile.org/postgraphile/computed-columns/) enable you to derive additional fields from types defined in your database.
|
||||
For example, you could write a function to expose a block header's state root over Postgraphile with a computed column - without modifying the `public.headers` table.
|
||||
- [Custom queries](https://www.graphile.org/postgraphile/custom-queries/) enable you to provide on-demand access to more complex data (e.g. the product of joining and filtering several tables' data based on a passed argument).
|
||||
For example, you could write a custom query to get the block timestamp for every transaction originating from a given address.
|
||||
- [Subscriptions](https://www.graphile.org/postgraphile/subscriptions/) enable you to publish data as it is coming into your database.
|
||||
|
||||
The above list is not exhaustive - please see the Postgraphile documentation for a more comprehensive and up-to-date description of available features.
|
||||
@@ -1,26 +0,0 @@
|
||||
# Syncing commands
|
||||
These commands are used to sync raw Ethereum data into Postgres.
|
||||
|
||||
## headerSync
|
||||
Syncs VulcanizeDB with the configured Ethereum node, populating only block headers.
|
||||
This command is useful when you want a minimal baseline from which to track targeted data on the blockchain (e.g. individual smart contract storage values or event logs).
|
||||
1. Start Ethereum node
|
||||
1. In a separate terminal start VulcanizeDB:
|
||||
- `./vulcanizedb headerSync --config <config.toml> --starting-block-number <block-number>`
|
||||
|
||||
## sync
|
||||
Syncs VulcanizeDB with the configured Ethereum node, populating blocks, transactions, receipts, and logs.
|
||||
This command is useful when you want to maintain a broad cache of what's happening on the blockchain.
|
||||
1. Start Ethereum node (**if fast syncing your Ethereum node, wait for initial sync to finish**)
|
||||
1. In a separate terminal start VulcanizeDB:
|
||||
- `./vulcanizedb sync --config <config.toml> --starting-block-number <block-number>`
|
||||
|
||||
## coldImport
|
||||
Sync VulcanizeDB from the LevelDB underlying a Geth node.
|
||||
1. Assure node is not running, and that it has synced to the desired block height.
|
||||
1. Start vulcanize_db
|
||||
- `./vulcanizedb coldImport --config <config.toml>`
|
||||
1. Optional flags:
|
||||
- `--starting-block-number <block number>`/`-s <block number>`: block number to start syncing from
|
||||
- `--ending-block-number <block number>`/`-e <block number>`: block number to sync to
|
||||
- `--all`/`-a`: sync all missing blocks
|
||||
@@ -1,40 +0,0 @@
|
||||
# Custom transformers
|
||||
When the capabilities of the generic `contractWatcher` are not sufficient, custom transformers tailored to a specific
|
||||
purpose can be leveraged.
|
||||
|
||||
Individual transformers can be composed together from any number of external repositories and executed as a single process using
|
||||
the `compose` and `execute` commands or the `composeAndExecute` command.
|
||||
|
||||
## Writing custom transformers
|
||||
For help with writing different types of custom transformers for the `composeAndExecute` set of commands, please see the below:
|
||||
|
||||
Storage Transformers
|
||||
* [Guide](../../staging/libraries/shared/factories/storage/README.md)
|
||||
* [Example](../../staging/libraries/shared/factories/storage/EXAMPLE.md)
|
||||
|
||||
Event Transformers
|
||||
* [Guide](../../staging/libraries/shared/factories/event/README.md)
|
||||
* [Example 1](https://github.com/vulcanize/ens_transformers/tree/master/transformers/registar)
|
||||
* [Example 2](https://github.com/vulcanize/ens_transformers/tree/master/transformers/registry)
|
||||
* [Example 3](https://github.com/vulcanize/ens_transformers/tree/master/transformers/resolver)
|
||||
|
||||
Contract Transformers
|
||||
* [Example 1](https://github.com/vulcanize/account_transformers)
|
||||
* [Example 2](https://github.com/vulcanize/ens_transformers/tree/master/transformers/domain_records)
|
||||
|
||||
## Preparing custom transformers to work as part of a plugin
|
||||
To plug in an external transformer we need to:
|
||||
|
||||
1. Create a package that exports a variable `TransformerInitializer`, `StorageTransformerInitializer`, or `ContractTransformerInitializer` that are of type [TransformerInitializer](../staging/libraries/shared/transformer/event_transformer.go#L33)
|
||||
or [StorageTransformerInitializer](../../staging/libraries/shared/transformer/storage_transformer.go#L31),
|
||||
or [ContractTransformerInitializer](../../staging/libraries/shared/transformer/contract_transformer.go#L31), respectively
|
||||
2. Design the transformers to work in the context of their [event](../staging/libraries/shared/watcher/event_watcher.go#L83),
|
||||
[storage](../../staging/libraries/shared/watcher/storage_watcher.go#L53),
|
||||
or [contract](../../staging/libraries/shared/watcher/contract_watcher.go#L68) watcher execution modes
|
||||
3. Create db migrations to run against vulcanizeDB so that we can store the transformer output
|
||||
* Do not `goose fix` the transformer migrations, this is to ensure they are always ran after the core vulcanizedb migrations which are kept in their fixed form
|
||||
* Specify migration locations for each transformer in the config with the `exporter.transformer.migrations` fields
|
||||
* If the base vDB migrations occupy this path as well, they need to be in their `goose fix`ed form
|
||||
as they are [here](../../staging/db/migrations)
|
||||
|
||||
To update a plugin repository with changes to the core vulcanizedb repository, run `dep ensure` to update its dependencies.
|
||||
@@ -17,14 +17,11 @@
|
||||
package event
|
||||
|
||||
import (
|
||||
"github.com/vulcanize/vulcanizedb/pkg/core"
|
||||
"github.com/vulcanize/vulcanizedb/pkg/datastore/postgres"
|
||||
)
|
||||
|
||||
type Repository interface {
|
||||
Create(headerID int64, models []interface{}) error
|
||||
MarkHeaderChecked(headerID int64) error
|
||||
MissingHeaders(startingBlockNumber, endingBlockNumber int64) ([]core.Header, error)
|
||||
RecheckHeaders(startingBlockNumber, endingBlockNUmber int64) ([]core.Header, error)
|
||||
SetDB(db *postgres.DB)
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ import (
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/vulcanize/vulcanizedb/libraries/shared/constants"
|
||||
"github.com/vulcanize/vulcanizedb/libraries/shared/transformer"
|
||||
"github.com/vulcanize/vulcanizedb/pkg/core"
|
||||
"github.com/vulcanize/vulcanizedb/pkg/datastore/postgres"
|
||||
@@ -37,7 +36,7 @@ func (transformer Transformer) NewTransformer(db *postgres.DB) transformer.Event
|
||||
return transformer
|
||||
}
|
||||
|
||||
func (transformer Transformer) Execute(logs []types.Log, header core.Header, recheckHeaders constants.TransformerExecution) error {
|
||||
func (transformer Transformer) Execute(logs []types.Log, header core.Header) error {
|
||||
transformerName := transformer.Config.TransformerName
|
||||
config := transformer.Config
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ import (
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"github.com/vulcanize/vulcanizedb/libraries/shared/constants"
|
||||
"github.com/vulcanize/vulcanizedb/libraries/shared/factories/event"
|
||||
"github.com/vulcanize/vulcanizedb/libraries/shared/mocks"
|
||||
"github.com/vulcanize/vulcanizedb/libraries/shared/test_data"
|
||||
@@ -60,14 +59,14 @@ var _ = Describe("Transformer", func() {
|
||||
})
|
||||
|
||||
It("marks header checked if no logs returned", func() {
|
||||
err := t.Execute([]types.Log{}, headerOne, constants.HeaderMissing)
|
||||
err := t.Execute([]types.Log{}, headerOne)
|
||||
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
repository.AssertMarkHeaderCheckedCalledWith(headerOne.Id)
|
||||
})
|
||||
|
||||
It("doesn't attempt to convert or persist an empty collection when there are no logs", func() {
|
||||
err := t.Execute([]types.Log{}, headerOne, constants.HeaderMissing)
|
||||
err := t.Execute([]types.Log{}, headerOne)
|
||||
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(converter.ToEntitiesCalledCounter).To(Equal(0))
|
||||
@@ -76,7 +75,7 @@ var _ = Describe("Transformer", func() {
|
||||
})
|
||||
|
||||
It("does not call repository.MarkCheckedHeader when there are logs", func() {
|
||||
err := t.Execute(logs, headerOne, constants.HeaderMissing)
|
||||
err := t.Execute(logs, headerOne)
|
||||
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
repository.AssertMarkHeaderCheckedNotCalled()
|
||||
@@ -85,14 +84,14 @@ var _ = Describe("Transformer", func() {
|
||||
It("returns error if marking header checked returns err", func() {
|
||||
repository.SetMarkHeaderCheckedError(fakes.FakeError)
|
||||
|
||||
err := t.Execute([]types.Log{}, headerOne, constants.HeaderMissing)
|
||||
err := t.Execute([]types.Log{}, headerOne)
|
||||
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err).To(MatchError(fakes.FakeError))
|
||||
})
|
||||
|
||||
It("converts an eth log to an entity", func() {
|
||||
err := t.Execute(logs, headerOne, constants.HeaderMissing)
|
||||
err := t.Execute(logs, headerOne)
|
||||
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(converter.ContractAbi).To(Equal(config.ContractAbi))
|
||||
@@ -102,7 +101,7 @@ var _ = Describe("Transformer", func() {
|
||||
It("returns an error if converter fails", func() {
|
||||
converter.ToEntitiesError = fakes.FakeError
|
||||
|
||||
err := t.Execute(logs, headerOne, constants.HeaderMissing)
|
||||
err := t.Execute(logs, headerOne)
|
||||
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err).To(MatchError(fakes.FakeError))
|
||||
@@ -111,7 +110,7 @@ var _ = Describe("Transformer", func() {
|
||||
It("converts an entity to a model", func() {
|
||||
converter.EntitiesToReturn = []interface{}{test_data.GenericEntity{}}
|
||||
|
||||
err := t.Execute(logs, headerOne, constants.HeaderMissing)
|
||||
err := t.Execute(logs, headerOne)
|
||||
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(converter.EntitiesToConvert[0]).To(Equal(test_data.GenericEntity{}))
|
||||
@@ -121,7 +120,7 @@ var _ = Describe("Transformer", func() {
|
||||
converter.EntitiesToReturn = []interface{}{test_data.GenericEntity{}}
|
||||
converter.ToModelsError = fakes.FakeError
|
||||
|
||||
err := t.Execute(logs, headerOne, constants.HeaderMissing)
|
||||
err := t.Execute(logs, headerOne)
|
||||
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err).To(MatchError(fakes.FakeError))
|
||||
@@ -130,7 +129,7 @@ var _ = Describe("Transformer", func() {
|
||||
It("persists the record", func() {
|
||||
converter.ModelsToReturn = []interface{}{test_data.GenericModel{}}
|
||||
|
||||
err := t.Execute(logs, headerOne, constants.HeaderMissing)
|
||||
err := t.Execute(logs, headerOne)
|
||||
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(repository.PassedHeaderID).To(Equal(headerOne.Id))
|
||||
@@ -139,7 +138,7 @@ var _ = Describe("Transformer", func() {
|
||||
|
||||
It("returns error if persisting the record fails", func() {
|
||||
repository.SetCreateError(fakes.FakeError)
|
||||
err := t.Execute(logs, headerOne, constants.HeaderMissing)
|
||||
err := t.Execute(logs, headerOne)
|
||||
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err).To(MatchError(fakes.FakeError))
|
||||
|
||||
@@ -100,6 +100,7 @@ type Repository interface {
|
||||
A contract-specific implementation of the repository interface enables the transformer to write the decoded storage value to the appropriate table in postgres.
|
||||
|
||||
The `Create` function is expected to recognize and persist a given storage value by the variable's name, as indicated on the row's metadata.
|
||||
Note: we advise silently discarding duplicates in `Create` - as it's possible that you may read the same diff several times, and an error will trigger the storage watcher to queue that diff for later processing.
|
||||
|
||||
The `SetDB` function is required for the repository to connect to the database.
|
||||
|
||||
|
||||
@@ -100,4 +100,54 @@ var _ = Describe("Storage transformer", func() {
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err).To(MatchError(fakes.FakeError))
|
||||
})
|
||||
|
||||
Describe("when a storage row contains more than one item packed in storage", func() {
|
||||
var (
|
||||
rawValue = common.HexToAddress("000000000000000000000000000000000000000000000002a300000000002a30")
|
||||
fakeBlockNumber = 123
|
||||
fakeBlockHash = "0x67890"
|
||||
packedTypes = make(map[int]utils.ValueType)
|
||||
)
|
||||
packedTypes[0] = utils.Uint48
|
||||
packedTypes[1] = utils.Uint48
|
||||
|
||||
var fakeMetadata = utils.StorageValueMetadata{
|
||||
Name: "",
|
||||
Keys: nil,
|
||||
Type: utils.PackedSlot,
|
||||
PackedTypes: packedTypes,
|
||||
}
|
||||
|
||||
It("passes the decoded data items to the repository", func() {
|
||||
mappings.Metadata = fakeMetadata
|
||||
fakeRow := utils.StorageDiffRow{
|
||||
Contract: common.Address{},
|
||||
BlockHash: common.HexToHash(fakeBlockHash),
|
||||
BlockHeight: fakeBlockNumber,
|
||||
StorageKey: common.Hash{},
|
||||
StorageValue: rawValue.Hash(),
|
||||
}
|
||||
|
||||
err := t.Execute(fakeRow)
|
||||
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(repository.PassedBlockNumber).To(Equal(fakeBlockNumber))
|
||||
Expect(repository.PassedBlockHash).To(Equal(common.HexToHash(fakeBlockHash).Hex()))
|
||||
Expect(repository.PassedMetadata).To(Equal(fakeMetadata))
|
||||
expectedPassedValue := make(map[int]string)
|
||||
expectedPassedValue[0] = "10800"
|
||||
expectedPassedValue[1] = "172800"
|
||||
Expect(repository.PassedValue.(map[int]string)).To(Equal(expectedPassedValue))
|
||||
})
|
||||
|
||||
It("returns error if creating a row fails", func() {
|
||||
mappings.Metadata = fakeMetadata
|
||||
repository.CreateErr = fakes.FakeError
|
||||
|
||||
err := t.Execute(utils.StorageDiffRow{StorageValue: rawValue.Hash()})
|
||||
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err).To(MatchError(fakes.FakeError))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -17,9 +17,12 @@
|
||||
package fetcher
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/vulcanize/vulcanizedb/libraries/shared/storage/utils"
|
||||
"github.com/vulcanize/vulcanizedb/pkg/fs"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type IStorageFetcher interface {
|
||||
@@ -39,6 +42,7 @@ func (storageFetcher CsvTailStorageFetcher) FetchStorageDiffs(out chan<- utils.S
|
||||
if tailErr != nil {
|
||||
errs <- tailErr
|
||||
}
|
||||
log.Debug("fetching storage diffs...")
|
||||
for line := range t.Lines {
|
||||
row, parseErr := utils.FromStrings(strings.Split(line.Text, ","))
|
||||
if parseErr != nil {
|
||||
|
||||
@@ -19,7 +19,6 @@ package mocks
|
||||
import (
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
|
||||
"github.com/vulcanize/vulcanizedb/libraries/shared/constants"
|
||||
shared_t "github.com/vulcanize/vulcanizedb/libraries/shared/transformer"
|
||||
"github.com/vulcanize/vulcanizedb/pkg/core"
|
||||
"github.com/vulcanize/vulcanizedb/pkg/datastore/postgres"
|
||||
@@ -33,7 +32,7 @@ type MockTransformer struct {
|
||||
config shared_t.EventTransformerConfig
|
||||
}
|
||||
|
||||
func (mh *MockTransformer) Execute(logs []types.Log, header core.Header, recheckHeaders constants.TransformerExecution) error {
|
||||
func (mh *MockTransformer) Execute(logs []types.Log, header core.Header) error {
|
||||
if mh.ExecuteError != nil {
|
||||
return mh.ExecuteError
|
||||
}
|
||||
|
||||
@@ -69,31 +69,6 @@ func MissingHeaders(startingBlockNumber, endingBlockNumber int64, db *postgres.D
|
||||
return result, err
|
||||
}
|
||||
|
||||
func RecheckHeaders(startingBlockNumber, endingBlockNumber int64, db *postgres.DB, checkedHeadersColumn string) ([]core.Header, error) {
|
||||
var result []core.Header
|
||||
var query string
|
||||
var err error
|
||||
|
||||
if endingBlockNumber == -1 {
|
||||
query = `SELECT headers.id, headers.block_number, headers.hash FROM headers
|
||||
LEFT JOIN checked_headers on headers.id = header_id
|
||||
WHERE ` + checkedHeadersColumn + ` between 1 and ` + constants.RecheckHeaderCap + `
|
||||
AND headers.block_number >= $1
|
||||
AND headers.eth_node_fingerprint = $2`
|
||||
err = db.Select(&result, query, startingBlockNumber, db.Node.ID)
|
||||
} else {
|
||||
query = `SELECT headers.id, headers.block_number, headers.hash FROM headers
|
||||
LEFT JOIN checked_headers on headers.id = header_id
|
||||
WHERE ` + checkedHeadersColumn + ` between 1 and ` + constants.RecheckHeaderCap + `
|
||||
AND headers.block_number >= $1
|
||||
AND headers.block_number <= $2
|
||||
AND headers.eth_node_fingerprint = $3`
|
||||
err = db.Select(&result, query, startingBlockNumber, endingBlockNumber, db.Node.ID)
|
||||
}
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
func GetCheckedColumnNames(db *postgres.DB) ([]string, error) {
|
||||
// Query returns `[]driver.Value`, nullable polymorphic interface
|
||||
var queryResult []driver.Value
|
||||
@@ -121,37 +96,47 @@ func GetCheckedColumnNames(db *postgres.DB) ([]string, error) {
|
||||
return columnNames, nil
|
||||
}
|
||||
|
||||
// Builds a SQL string that checks if any column value is 0, given the column names.
|
||||
// Builds a SQL string that checks if any column should be checked/rechecked.
|
||||
// Defaults to FALSE when no columns are provided.
|
||||
// Ex: ["columnA", "columnB"] => "NOT (columnA!=0 AND columnB!=0)"
|
||||
// [] => "FALSE"
|
||||
func CreateNotCheckedSQL(boolColumns []string, recheckHeaders constants.TransformerExecution) string {
|
||||
|
||||
var result bytes.Buffer
|
||||
|
||||
func CreateHeaderCheckedPredicateSQL(boolColumns []string, recheckHeaders constants.TransformerExecution) string {
|
||||
if len(boolColumns) == 0 {
|
||||
return "FALSE"
|
||||
}
|
||||
|
||||
result.WriteString("NOT (")
|
||||
if recheckHeaders {
|
||||
return createHeaderCheckedPredicateSQLForRecheckedHeaders(boolColumns)
|
||||
} else {
|
||||
return createHeaderCheckedPredicateSQLForMissingHeaders(boolColumns)
|
||||
}
|
||||
}
|
||||
|
||||
func createHeaderCheckedPredicateSQLForMissingHeaders(boolColumns []string) string {
|
||||
var result bytes.Buffer
|
||||
result.WriteString(" (")
|
||||
|
||||
// Loop excluding last column name
|
||||
for _, column := range boolColumns[:len(boolColumns)-1] {
|
||||
|
||||
if recheckHeaders {
|
||||
result.WriteString(fmt.Sprintf("%v>=%s AND ", column, constants.RecheckHeaderCap))
|
||||
} else {
|
||||
result.WriteString(fmt.Sprintf("%v!=0 AND ", column))
|
||||
}
|
||||
result.WriteString(fmt.Sprintf("%v=0 OR ", column))
|
||||
}
|
||||
|
||||
// No trailing "OR" for the last column name
|
||||
if recheckHeaders {
|
||||
result.WriteString(fmt.Sprintf("%v>=%s)", boolColumns[len(boolColumns)-1], constants.RecheckHeaderCap))
|
||||
} else {
|
||||
result.WriteString(fmt.Sprintf("%v!=0)", boolColumns[len(boolColumns)-1]))
|
||||
result.WriteString(fmt.Sprintf("%v=0)", boolColumns[len(boolColumns)-1]))
|
||||
|
||||
return result.String()
|
||||
}
|
||||
|
||||
func createHeaderCheckedPredicateSQLForRecheckedHeaders(boolColumns []string) string {
|
||||
var result bytes.Buffer
|
||||
result.WriteString(" (")
|
||||
|
||||
// Loop excluding last column name
|
||||
for _, column := range boolColumns[:len(boolColumns)-1] {
|
||||
result.WriteString(fmt.Sprintf("%v<%s OR ", column, constants.RecheckHeaderCap))
|
||||
}
|
||||
|
||||
// No trailing "OR" for the last column name
|
||||
result.WriteString(fmt.Sprintf("%v<%s)", boolColumns[len(boolColumns)-1], constants.RecheckHeaderCap))
|
||||
|
||||
return result.String()
|
||||
}
|
||||
|
||||
@@ -36,12 +36,12 @@ import (
|
||||
)
|
||||
|
||||
var _ = Describe("Repository", func() {
|
||||
Describe("MarkHeaderChecked", func() {
|
||||
var (
|
||||
checkedHeadersColumn string
|
||||
db *postgres.DB
|
||||
)
|
||||
var (
|
||||
checkedHeadersColumn string
|
||||
db *postgres.DB
|
||||
)
|
||||
|
||||
Describe("MarkHeaderChecked", func() {
|
||||
BeforeEach(func() {
|
||||
db = test_config.NewTestDB(test_config.NewTestNode())
|
||||
test_config.CleanTestDB(db)
|
||||
@@ -73,11 +73,6 @@ var _ = Describe("Repository", func() {
|
||||
})
|
||||
|
||||
Describe("MarkHeaderCheckedInTransaction", func() {
|
||||
var (
|
||||
checkedHeadersColumn string
|
||||
db *postgres.DB
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
db = test_config.NewTestDB(test_config.NewTestNode())
|
||||
test_config.CleanTestDB(db)
|
||||
@@ -114,16 +109,17 @@ var _ = Describe("Repository", func() {
|
||||
|
||||
Describe("MissingHeaders", func() {
|
||||
var (
|
||||
db *postgres.DB
|
||||
headerRepository datastore.HeaderRepository
|
||||
startingBlockNumber int64
|
||||
endingBlockNumber int64
|
||||
eventSpecificBlockNumber int64
|
||||
outOfRangeBlockNumber int64
|
||||
blockNumbers []int64
|
||||
headerIDs []int64
|
||||
notCheckedSQL string
|
||||
err error
|
||||
hr r2.HeaderRepository
|
||||
columnNames []string
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
@@ -133,14 +129,14 @@ var _ = Describe("Repository", func() {
|
||||
hr = r2.NewHeaderRepository(db)
|
||||
hr.AddCheckColumns(getExpectedColumnNames())
|
||||
|
||||
columnNames, err := shared.GetCheckedColumnNames(db)
|
||||
columnNames, err = shared.GetCheckedColumnNames(db)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
notCheckedSQL = shared.CreateNotCheckedSQL(columnNames, constants.HeaderMissing)
|
||||
notCheckedSQL = shared.CreateHeaderCheckedPredicateSQL(columnNames, constants.HeaderMissing)
|
||||
|
||||
startingBlockNumber = rand.Int63()
|
||||
eventSpecificBlockNumber = startingBlockNumber + 1
|
||||
endingBlockNumber = startingBlockNumber + 2
|
||||
outOfRangeBlockNumber := endingBlockNumber + 1
|
||||
outOfRangeBlockNumber = endingBlockNumber + 1
|
||||
|
||||
blockNumbers = []int64{startingBlockNumber, eventSpecificBlockNumber, endingBlockNumber, outOfRangeBlockNumber}
|
||||
|
||||
@@ -157,6 +153,7 @@ var _ = Describe("Repository", func() {
|
||||
})
|
||||
|
||||
It("only treats headers as checked if the event specific logs have been checked", func() {
|
||||
//add a checked_header record, but don't mark it check for any of the columns
|
||||
_, err = db.Exec(`INSERT INTO public.checked_headers (header_id) VALUES ($1)`, headerIDs[1])
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
@@ -191,83 +188,47 @@ var _ = Describe("Repository", func() {
|
||||
Expect(nodeTwoMissingHeaders[0].BlockNumber).To(Or(Equal(startingBlockNumber+10), Equal(eventSpecificBlockNumber+10), Equal(endingBlockNumber+10)))
|
||||
Expect(nodeTwoMissingHeaders[1].BlockNumber).To(Or(Equal(startingBlockNumber+10), Equal(eventSpecificBlockNumber+10), Equal(endingBlockNumber+10)))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("RecheckHeaders", func() {
|
||||
var (
|
||||
checkedHeadersColumn string
|
||||
db *postgres.DB
|
||||
headerOneID, headerTwoID, headerThreeID, headerFourID int64
|
||||
headerOneErr, headerTwoErr, headerThreeErr, headerFourErr error
|
||||
)
|
||||
It("handles an ending block of -1 ", func() {
|
||||
endingBlock := int64(-1)
|
||||
headers, err := shared.MissingHeaders(startingBlockNumber, endingBlock, db, notCheckedSQL)
|
||||
|
||||
BeforeEach(func() {
|
||||
db = test_config.NewTestDB(test_config.NewTestNode())
|
||||
test_config.CleanTestDB(db)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(len(headers)).To(Equal(4))
|
||||
Expect(headers[0].BlockNumber).To(Or(Equal(startingBlockNumber), Equal(endingBlockNumber), Equal(eventSpecificBlockNumber), Equal(outOfRangeBlockNumber)))
|
||||
Expect(headers[1].BlockNumber).To(Or(Equal(startingBlockNumber), Equal(endingBlockNumber), Equal(eventSpecificBlockNumber), Equal(outOfRangeBlockNumber)))
|
||||
Expect(headers[2].BlockNumber).To(Or(Equal(startingBlockNumber), Equal(endingBlockNumber), Equal(eventSpecificBlockNumber), Equal(outOfRangeBlockNumber)))
|
||||
Expect(headers[3].BlockNumber).To(Or(Equal(startingBlockNumber), Equal(endingBlockNumber), Equal(eventSpecificBlockNumber), Equal(outOfRangeBlockNumber)))
|
||||
|
||||
// create header checked column
|
||||
checkedHeadersColumn = "test_column_checked"
|
||||
_, migrateErr := db.Exec(`ALTER TABLE public.checked_headers ADD COLUMN ` + checkedHeadersColumn + ` integer`)
|
||||
Expect(migrateErr).NotTo(HaveOccurred())
|
||||
})
|
||||
|
||||
// create headers
|
||||
headerRepository := repositories.NewHeaderRepository(db)
|
||||
headerOneID, headerOneErr = headerRepository.CreateOrUpdateHeader(fakes.GetFakeHeader(1))
|
||||
Expect(headerOneErr).NotTo(HaveOccurred())
|
||||
headerTwoID, headerTwoErr = headerRepository.CreateOrUpdateHeader(fakes.GetFakeHeader(2))
|
||||
Expect(headerTwoErr).NotTo(HaveOccurred())
|
||||
headerThreeID, headerThreeErr = headerRepository.CreateOrUpdateHeader(fakes.GetFakeHeader(3))
|
||||
Expect(headerThreeErr).NotTo(HaveOccurred())
|
||||
headerFourID, headerFourErr = headerRepository.CreateOrUpdateHeader(fakes.GetFakeHeader(4))
|
||||
Expect(headerFourErr).NotTo(HaveOccurred())
|
||||
It("when a the `notCheckedSQL` argument allows for rechecks it returns headers where the checked count is less than the maximum", func() {
|
||||
columnName := columnNames[0]
|
||||
recheckedSQL := shared.CreateHeaderCheckedPredicateSQL([]string{columnName}, constants.HeaderRecheck)
|
||||
// mark every header checked at least once
|
||||
// header 4 is marked the maximum number of times, it it is not longer checked
|
||||
|
||||
// mark every header checked at least once, with one fully rechecked (headerThree)
|
||||
maxCheckCount, intConversionErr := strconv.Atoi(constants.RecheckHeaderCap)
|
||||
Expect(intConversionErr).NotTo(HaveOccurred())
|
||||
_, markHeaderOneCheckedErr := db.Exec(
|
||||
`INSERT INTO public.checked_headers (header_id, `+checkedHeadersColumn+`) VALUES ($1, $2)`,
|
||||
headerOneID, maxCheckCount)
|
||||
Expect(markHeaderOneCheckedErr).NotTo(HaveOccurred())
|
||||
_, markHeaderTwoCheckedErr := db.Exec(
|
||||
`INSERT INTO public.checked_headers (header_id, `+checkedHeadersColumn+`) VALUES ($1, $2)`,
|
||||
headerTwoID, maxCheckCount)
|
||||
Expect(markHeaderTwoCheckedErr).NotTo(HaveOccurred())
|
||||
_, markHeaderThreeCheckedErr := db.Exec(
|
||||
`INSERT INTO public.checked_headers (header_id, `+checkedHeadersColumn+`) VALUES ($1, $2)`,
|
||||
headerThreeID, maxCheckCount+1)
|
||||
Expect(markHeaderThreeCheckedErr).NotTo(HaveOccurred())
|
||||
_, markHeaderFourCheckedErr := db.Exec(
|
||||
`INSERT INTO public.checked_headers (header_id, `+checkedHeadersColumn+`) VALUES ($1, $2)`,
|
||||
headerFourID, maxCheckCount)
|
||||
Expect(markHeaderFourCheckedErr).NotTo(HaveOccurred())
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
_, cleanupMigrateErr := db.Exec(`ALTER TABLE public.checked_headers DROP COLUMN ` + checkedHeadersColumn)
|
||||
Expect(cleanupMigrateErr).NotTo(HaveOccurred())
|
||||
})
|
||||
markHeaderOneErr := shared.MarkHeaderChecked(headerIDs[0], db, columnName)
|
||||
Expect(markHeaderOneErr).NotTo(HaveOccurred())
|
||||
markHeaderTwoErr := shared.MarkHeaderChecked(headerIDs[1], db, columnName)
|
||||
Expect(markHeaderTwoErr).NotTo(HaveOccurred())
|
||||
markHeaderThreeErr := shared.MarkHeaderChecked(headerIDs[2], db, columnName)
|
||||
Expect(markHeaderThreeErr).NotTo(HaveOccurred())
|
||||
for i := 0; i <= maxCheckCount; i++ {
|
||||
markHeaderFourErr := shared.MarkHeaderChecked(headerIDs[3], db, columnName)
|
||||
Expect(markHeaderFourErr).NotTo(HaveOccurred())
|
||||
}
|
||||
|
||||
Describe("when no ending block number (ending block number == -1)", func() {
|
||||
It("returns all headers since starting block where checked count is less than cap", func() {
|
||||
headers, err := shared.RecheckHeaders(1, -1, db, checkedHeadersColumn)
|
||||
headers, err := shared.MissingHeaders(1, -1, db, recheckedSQL)
|
||||
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(len(headers)).To(Equal(3))
|
||||
Expect(headers[0].Id).To(Or(Equal(headerOneID), Equal(headerTwoID), Equal(headerFourID)))
|
||||
Expect(headers[1].Id).To(Or(Equal(headerOneID), Equal(headerTwoID), Equal(headerFourID)))
|
||||
Expect(headers[2].Id).To(Or(Equal(headerOneID), Equal(headerTwoID), Equal(headerFourID)))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("when ending block number specified", func() {
|
||||
It("returns headers between starting and ending block where checked count is less than cap", func() {
|
||||
headers, err := shared.RecheckHeaders(1, 3, db, checkedHeadersColumn)
|
||||
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(len(headers)).To(Equal(2))
|
||||
Expect(headers[0].Id).To(Or(Equal(headerOneID), Equal(headerTwoID)))
|
||||
Expect(headers[1].Id).To(Or(Equal(headerOneID), Equal(headerTwoID)))
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(len(headers)).To(Equal(3))
|
||||
Expect(headers[0].Id).To(Or(Equal(headerIDs[0]), Equal(headerIDs[1]), Equal(headerIDs[2])))
|
||||
Expect(headers[1].Id).To(Or(Equal(headerIDs[0]), Equal(headerIDs[1]), Equal(headerIDs[2])))
|
||||
Expect(headers[2].Id).To(Or(Equal(headerIDs[0]), Equal(headerIDs[1]), Equal(headerIDs[2])))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -285,32 +246,49 @@ var _ = Describe("Repository", func() {
|
||||
})
|
||||
})
|
||||
|
||||
Describe("CreateNotCheckedSQL", func() {
|
||||
It("generates a correct SQL string for one column", func() {
|
||||
columns := []string{"columnA"}
|
||||
expected := "NOT (columnA!=0)"
|
||||
actual := shared.CreateNotCheckedSQL(columns, constants.HeaderMissing)
|
||||
Expect(actual).To(Equal(expected))
|
||||
Describe("CreateHeaderCheckedPredicateSQL", func() {
|
||||
Describe("for headers that haven't been checked for logs", func() {
|
||||
It("generates a correct SQL string for one column", func() {
|
||||
columns := []string{"columnA"}
|
||||
expected := " (columnA=0)"
|
||||
actual := shared.CreateHeaderCheckedPredicateSQL(columns, constants.HeaderMissing)
|
||||
Expect(actual).To(Equal(expected))
|
||||
})
|
||||
|
||||
It("generates a correct SQL string for several columns", func() {
|
||||
columns := []string{"columnA", "columnB"}
|
||||
expected := " (columnA=0 OR columnB=0)"
|
||||
actual := shared.CreateHeaderCheckedPredicateSQL(columns, constants.HeaderMissing)
|
||||
Expect(actual).To(Equal(expected))
|
||||
})
|
||||
|
||||
It("defaults to FALSE when there are no columns", func() {
|
||||
expected := "FALSE"
|
||||
actual := shared.CreateHeaderCheckedPredicateSQL([]string{}, constants.HeaderMissing)
|
||||
Expect(actual).To(Equal(expected))
|
||||
})
|
||||
})
|
||||
|
||||
It("generates a correct SQL string for several columns", func() {
|
||||
columns := []string{"columnA", "columnB"}
|
||||
expected := "NOT (columnA!=0 AND columnB!=0)"
|
||||
actual := shared.CreateNotCheckedSQL(columns, constants.HeaderMissing)
|
||||
Expect(actual).To(Equal(expected))
|
||||
})
|
||||
Describe("for headers that are being rechecked for logs", func() {
|
||||
It("generates a correct SQL string for rechecking headers for one column", func() {
|
||||
columns := []string{"columnA"}
|
||||
expected := fmt.Sprintf(" (columnA<%s)", constants.RecheckHeaderCap)
|
||||
actual := shared.CreateHeaderCheckedPredicateSQL(columns, constants.HeaderRecheck)
|
||||
Expect(actual).To(Equal(expected))
|
||||
})
|
||||
|
||||
It("defaults to FALSE when there are no columns", func() {
|
||||
expected := "FALSE"
|
||||
actual := shared.CreateNotCheckedSQL([]string{}, constants.HeaderMissing)
|
||||
Expect(actual).To(Equal(expected))
|
||||
})
|
||||
It("generates a correct SQL string for rechecking headers for several columns", func() {
|
||||
columns := []string{"columnA", "columnB"}
|
||||
expected := fmt.Sprintf(" (columnA<%s OR columnB<%s)", constants.RecheckHeaderCap, constants.RecheckHeaderCap)
|
||||
actual := shared.CreateHeaderCheckedPredicateSQL(columns, constants.HeaderRecheck)
|
||||
Expect(actual).To(Equal(expected))
|
||||
})
|
||||
|
||||
It("generates a correct SQL string for rechecking headers", func() {
|
||||
columns := []string{"columnA", "columnB"}
|
||||
expected := fmt.Sprintf("NOT (columnA>=%s AND columnB>=%s)", constants.RecheckHeaderCap, constants.RecheckHeaderCap)
|
||||
actual := shared.CreateNotCheckedSQL(columns, constants.HeaderRecheck)
|
||||
Expect(actual).To(Equal(expected))
|
||||
It("defaults to FALSE when there are no columns", func() {
|
||||
expected := "FALSE"
|
||||
actual := shared.CreateHeaderCheckedPredicateSQL([]string{}, constants.HeaderRecheck)
|
||||
Expect(actual).To(Equal(expected))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -47,7 +47,7 @@ const (
|
||||
)
|
||||
|
||||
func GetMapping(indexOnContract, key string) common.Hash {
|
||||
keyBytes := common.FromHex("0x" + key + indexOnContract)
|
||||
keyBytes := common.FromHex(key + indexOnContract)
|
||||
encoded := crypto.Keccak256(keyBytes)
|
||||
return common.BytesToHash(encoded)
|
||||
}
|
||||
|
||||
@@ -21,6 +21,16 @@ var _ = Describe("Mappings", func() {
|
||||
expectedStorageKey := common.HexToHash("0xee0c1b59a3856bafbfb8730e7694c4badc271eb5f01ce4a8d7a53d8a6499676f")
|
||||
Expect(storageKey).To(Equal(expectedStorageKey))
|
||||
})
|
||||
|
||||
It("returns same result if value includes hex prefix", func() {
|
||||
indexOfMappingOnContract := storage.IndexZero
|
||||
keyForDesiredValueInMapping := "0x1234567890abcdef"
|
||||
|
||||
storageKey := storage.GetMapping(indexOfMappingOnContract, keyForDesiredValueInMapping)
|
||||
|
||||
expectedStorageKey := common.HexToHash("0xee0c1b59a3856bafbfb8730e7694c4badc271eb5f01ce4a8d7a53d8a6499676f")
|
||||
Expect(storageKey).To(Equal(expectedStorageKey))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("GetNestedMapping", func() {
|
||||
|
||||
@@ -38,7 +38,7 @@ func NewStorageQueue(db *postgres.DB) StorageQueue {
|
||||
func (queue StorageQueue) Add(row utils.StorageDiffRow) error {
|
||||
_, err := queue.db.Exec(`INSERT INTO public.queued_storage (contract,
|
||||
block_hash, block_height, storage_key, storage_value) VALUES
|
||||
($1, $2, $3, $4, $5)`, row.Contract.Bytes(), row.BlockHash.Bytes(),
|
||||
($1, $2, $3, $4, $5) ON CONFLICT DO NOTHING`, row.Contract.Bytes(), row.BlockHash.Bytes(),
|
||||
row.BlockHeight, row.StorageKey.Bytes(), row.StorageValue.Bytes())
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -49,11 +49,22 @@ var _ = Describe("Storage queue", func() {
|
||||
Expect(addErr).NotTo(HaveOccurred())
|
||||
})
|
||||
|
||||
It("adds a storage row to the db", func() {
|
||||
var result utils.StorageDiffRow
|
||||
getErr := db.Get(&result, `SELECT contract, block_hash, block_height, storage_key, storage_value FROM public.queued_storage`)
|
||||
Expect(getErr).NotTo(HaveOccurred())
|
||||
Expect(result).To(Equal(row))
|
||||
Describe("Add", func() {
|
||||
It("adds a storage row to the db", func() {
|
||||
var result utils.StorageDiffRow
|
||||
getErr := db.Get(&result, `SELECT contract, block_hash, block_height, storage_key, storage_value FROM public.queued_storage`)
|
||||
Expect(getErr).NotTo(HaveOccurred())
|
||||
Expect(result).To(Equal(row))
|
||||
})
|
||||
|
||||
It("does not duplicate storage rows", func() {
|
||||
addErr := queue.Add(row)
|
||||
Expect(addErr).NotTo(HaveOccurred())
|
||||
var count int
|
||||
getErr := db.Get(&count, `SELECT count(*) FROM public.queued_storage`)
|
||||
Expect(getErr).NotTo(HaveOccurred())
|
||||
Expect(count).To(Equal(1))
|
||||
})
|
||||
})
|
||||
|
||||
It("deletes storage row from db", func() {
|
||||
|
||||
@@ -23,27 +23,30 @@ import (
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
)
|
||||
|
||||
const (
|
||||
bitsPerByte = 8
|
||||
)
|
||||
|
||||
func Decode(row StorageDiffRow, metadata StorageValueMetadata) (interface{}, error) {
|
||||
switch metadata.Type {
|
||||
case Uint256:
|
||||
return decodeUint256(row.StorageValue.Bytes()), nil
|
||||
return decodeInteger(row.StorageValue.Bytes()), nil
|
||||
case Uint48:
|
||||
return decodeUint48(row.StorageValue.Bytes()), nil
|
||||
return decodeInteger(row.StorageValue.Bytes()), nil
|
||||
case Uint128:
|
||||
return decodeInteger(row.StorageValue.Bytes()), nil
|
||||
case Address:
|
||||
return decodeAddress(row.StorageValue.Bytes()), nil
|
||||
case Bytes32:
|
||||
return row.StorageValue.Hex(), nil
|
||||
case PackedSlot:
|
||||
return decodePackedSlot(row.StorageValue.Bytes(), metadata.PackedTypes), nil
|
||||
default:
|
||||
panic(fmt.Sprintf("can't decode unknown type: %d", metadata.Type))
|
||||
}
|
||||
}
|
||||
|
||||
func decodeUint256(raw []byte) string {
|
||||
n := big.NewInt(0).SetBytes(raw)
|
||||
return n.String()
|
||||
}
|
||||
|
||||
func decodeUint48(raw []byte) string {
|
||||
func decodeInteger(raw []byte) string {
|
||||
n := big.NewInt(0).SetBytes(raw)
|
||||
return n.String()
|
||||
}
|
||||
@@ -51,3 +54,51 @@ func decodeUint48(raw []byte) string {
|
||||
func decodeAddress(raw []byte) string {
|
||||
return common.BytesToAddress(raw).Hex()
|
||||
}
|
||||
|
||||
func decodePackedSlot(raw []byte, packedTypes map[int]ValueType) map[int]string {
|
||||
storageSlotData := raw
|
||||
decodedStorageSlotItems := map[int]string{}
|
||||
numberOfTypes := len(packedTypes)
|
||||
|
||||
for position := 0; position < numberOfTypes; position++ {
|
||||
//get length of remaining storage date
|
||||
lengthOfStorageData := len(storageSlotData)
|
||||
|
||||
//get item details (type, length, starting index, value bytes)
|
||||
itemType := packedTypes[position]
|
||||
lengthOfItem := getNumberOfBytes(itemType)
|
||||
itemStartingIndex := lengthOfStorageData - lengthOfItem
|
||||
itemValueBytes := storageSlotData[itemStartingIndex:]
|
||||
|
||||
//decode item's bytes and set in results map
|
||||
decodedValue := decodeIndividualItems(itemValueBytes, itemType)
|
||||
decodedStorageSlotItems[position] = decodedValue
|
||||
|
||||
//pop last item off raw slot data before moving on
|
||||
storageSlotData = storageSlotData[0:itemStartingIndex]
|
||||
}
|
||||
|
||||
return decodedStorageSlotItems
|
||||
}
|
||||
|
||||
func decodeIndividualItems(itemBytes []byte, valueType ValueType) string {
|
||||
switch valueType {
|
||||
case Uint48:
|
||||
return decodeInteger(itemBytes)
|
||||
case Uint128:
|
||||
return decodeInteger(itemBytes)
|
||||
default:
|
||||
panic(fmt.Sprintf("can't decode unknown type: %d", valueType))
|
||||
}
|
||||
}
|
||||
|
||||
func getNumberOfBytes(valueType ValueType) int {
|
||||
switch valueType {
|
||||
case Uint48:
|
||||
return 48 / bitsPerByte
|
||||
case Uint128:
|
||||
return 128 / bitsPerByte
|
||||
default:
|
||||
panic(fmt.Sprintf("ValueType %d not recognized", valueType))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,17 @@ var _ = Describe("Storage decoder", func() {
|
||||
Expect(result).To(Equal(big.NewInt(0).SetBytes(fakeInt.Bytes()).String()))
|
||||
})
|
||||
|
||||
It("decodes uint128", func() {
|
||||
fakeInt := common.HexToHash("0000000000000000000000000000000000000000000000000000000000011123")
|
||||
row := utils.StorageDiffRow{StorageValue: fakeInt}
|
||||
metadata := utils.StorageValueMetadata{Type: utils.Uint128}
|
||||
|
||||
result, err := utils.Decode(row, metadata)
|
||||
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(result).To(Equal(big.NewInt(0).SetBytes(fakeInt.Bytes()).String()))
|
||||
})
|
||||
|
||||
It("decodes uint48", func() {
|
||||
fakeInt := common.HexToHash("0000000000000000000000000000000000000000000000000000000000000123")
|
||||
row := utils.StorageDiffRow{StorageValue: fakeInt}
|
||||
@@ -59,4 +70,81 @@ var _ = Describe("Storage decoder", func() {
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(result).To(Equal(fakeAddress.Hex()))
|
||||
})
|
||||
|
||||
Describe("when there are multiple items packed in the storage slot", func() {
|
||||
It("decodes uint48 items", func() {
|
||||
//this is a real storage data example
|
||||
packedStorage := common.HexToHash("000000000000000000000000000000000000000000000002a300000000002a30")
|
||||
row := utils.StorageDiffRow{StorageValue: packedStorage}
|
||||
packedTypes := map[int]utils.ValueType{}
|
||||
packedTypes[0] = utils.Uint48
|
||||
packedTypes[1] = utils.Uint48
|
||||
|
||||
metadata := utils.StorageValueMetadata{
|
||||
Type: utils.PackedSlot,
|
||||
PackedTypes: packedTypes,
|
||||
}
|
||||
|
||||
result, err := utils.Decode(row, metadata)
|
||||
decodedValues := result.(map[int]string)
|
||||
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(decodedValues[0]).To(Equal(big.NewInt(0).SetBytes(common.HexToHash("2a30").Bytes()).String()))
|
||||
Expect(decodedValues[1]).To(Equal(big.NewInt(0).SetBytes(common.HexToHash("2a300").Bytes()).String()))
|
||||
})
|
||||
|
||||
It("decodes 5 uint48 items", func() {
|
||||
//TODO: this packedStorageHex was generated by hand, it would be nice to test this against
|
||||
//real storage data that has several items packed into it
|
||||
packedStorageHex := "0000000A5D1AFFFFFFFFFFFE00000009F3C600000002A300000000002A30"
|
||||
|
||||
packedStorage := common.HexToHash(packedStorageHex)
|
||||
row := utils.StorageDiffRow{StorageValue: packedStorage}
|
||||
packedTypes := map[int]utils.ValueType{}
|
||||
packedTypes[0] = utils.Uint48
|
||||
packedTypes[1] = utils.Uint48
|
||||
packedTypes[2] = utils.Uint48
|
||||
packedTypes[3] = utils.Uint48
|
||||
packedTypes[4] = utils.Uint48
|
||||
|
||||
metadata := utils.StorageValueMetadata{
|
||||
Type: utils.PackedSlot,
|
||||
PackedTypes: packedTypes,
|
||||
}
|
||||
|
||||
result, err := utils.Decode(row, metadata)
|
||||
decodedValues := result.(map[int]string)
|
||||
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(decodedValues[0]).To(Equal(big.NewInt(0).SetBytes(common.HexToHash("2a30").Bytes()).String()))
|
||||
Expect(decodedValues[1]).To(Equal(big.NewInt(0).SetBytes(common.HexToHash("2a300").Bytes()).String()))
|
||||
Expect(decodedValues[2]).To(Equal(big.NewInt(0).SetBytes(common.HexToHash("9F3C6").Bytes()).String()))
|
||||
Expect(decodedValues[3]).To(Equal(big.NewInt(0).SetBytes(common.HexToHash("FFFFFFFFFFFE").Bytes()).String()))
|
||||
Expect(decodedValues[4]).To(Equal(big.NewInt(0).SetBytes(common.HexToHash("A5D1A").Bytes()).String()))
|
||||
})
|
||||
|
||||
It("decodes 2 uint128 items", func() {
|
||||
//TODO: this packedStorageHex was generated by hand, it would be nice to test this against
|
||||
//real storage data that has several items packed into it
|
||||
packedStorageHex := "000000038D7EA4C67FF8E502B6730000" +
|
||||
"0000000000000000AB54A98CEB1F0AD2"
|
||||
packedStorage := common.HexToHash(packedStorageHex)
|
||||
row := utils.StorageDiffRow{StorageValue: packedStorage}
|
||||
packedTypes := map[int]utils.ValueType{}
|
||||
packedTypes[0] = utils.Uint128
|
||||
packedTypes[1] = utils.Uint128
|
||||
|
||||
metadata := utils.StorageValueMetadata{
|
||||
Type: utils.PackedSlot,
|
||||
PackedTypes: packedTypes,
|
||||
}
|
||||
|
||||
result, err := utils.Decode(row, metadata)
|
||||
decodedValues := result.(map[int]string)
|
||||
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(decodedValues[0]).To(Equal(big.NewInt(0).SetBytes(common.HexToHash("AB54A98CEB1F0AD2").Bytes()).String()))
|
||||
Expect(decodedValues[1]).To(Equal(big.NewInt(0).SetBytes(common.HexToHash("38D7EA4C67FF8E502B6730000").Bytes()).String()))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -17,12 +17,9 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
var ErrRowExists = errors.New("parsed row for storage diff already exists")
|
||||
|
||||
type ErrContractNotFound struct {
|
||||
Contract string
|
||||
}
|
||||
|
||||
@@ -16,27 +16,54 @@
|
||||
|
||||
package utils
|
||||
|
||||
import "fmt"
|
||||
|
||||
type ValueType int
|
||||
|
||||
const (
|
||||
Uint256 ValueType = iota
|
||||
Uint48
|
||||
Uint128
|
||||
Bytes32
|
||||
Address
|
||||
PackedSlot
|
||||
)
|
||||
|
||||
type Key string
|
||||
|
||||
type StorageValueMetadata struct {
|
||||
Name string
|
||||
Keys map[Key]string
|
||||
Type ValueType
|
||||
Name string
|
||||
Keys map[Key]string
|
||||
Type ValueType
|
||||
PackedNames map[int]string //zero indexed position in map => name of packed item
|
||||
PackedTypes map[int]ValueType //zero indexed position in map => type of packed item
|
||||
}
|
||||
|
||||
func GetStorageValueMetadata(name string, keys map[Key]string, t ValueType) StorageValueMetadata {
|
||||
func GetStorageValueMetadata(name string, keys map[Key]string, valueType ValueType) StorageValueMetadata {
|
||||
return getMetadata(name, keys, valueType, nil, nil)
|
||||
}
|
||||
|
||||
func GetStorageValueMetadataForPackedSlot(name string, keys map[Key]string, valueType ValueType, packedNames map[int]string, packedTypes map[int]ValueType) StorageValueMetadata {
|
||||
return getMetadata(name, keys, valueType, packedNames, packedTypes)
|
||||
}
|
||||
|
||||
func getMetadata(name string, keys map[Key]string, valueType ValueType, packedNames map[int]string, packedTypes map[int]ValueType) StorageValueMetadata {
|
||||
assertPackedSlotArgs(valueType, packedNames, packedTypes)
|
||||
|
||||
return StorageValueMetadata{
|
||||
Name: name,
|
||||
Keys: keys,
|
||||
Type: t,
|
||||
Name: name,
|
||||
Keys: keys,
|
||||
Type: valueType,
|
||||
PackedNames: packedNames,
|
||||
PackedTypes: packedTypes,
|
||||
}
|
||||
}
|
||||
|
||||
func assertPackedSlotArgs(valueType ValueType, packedNames map[int]string, packedTypes map[int]ValueType) {
|
||||
if valueType == PackedSlot && (packedTypes == nil || packedNames == nil) {
|
||||
panic(fmt.Sprintf("ValueType is PackedSlot. Expected PackedNames and PackedTypes to not be nil, but got PackedNames = %v and PackedTypes = %v", packedNames, packedTypes))
|
||||
} else if (packedNames != nil && packedTypes != nil) && valueType != PackedSlot {
|
||||
panic(fmt.Sprintf("PackedNames and PackedTypes passed in. Expected ValueType to equal PackedSlot (%v), but got %v.", PackedSlot, valueType))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
)
|
||||
|
||||
var _ = Describe("Storage value metadata getter", func() {
|
||||
It("returns a storage value metadata instance with corresponding fields assigned", func() {
|
||||
It("returns storage value metadata for a single storage variable", func() {
|
||||
metadataName := "fake_name"
|
||||
metadataKeys := map[utils.Key]string{"key": "value"}
|
||||
metadataType := utils.Uint256
|
||||
@@ -19,4 +19,60 @@ var _ = Describe("Storage value metadata getter", func() {
|
||||
}
|
||||
Expect(utils.GetStorageValueMetadata(metadataName, metadataKeys, metadataType)).To(Equal(expectedMetadata))
|
||||
})
|
||||
|
||||
Describe("metadata for a packed storaged slot", func() {
|
||||
It("returns metadata for multiple storage variables", func() {
|
||||
metadataName := "fake_name"
|
||||
metadataKeys := map[utils.Key]string{"key": "value"}
|
||||
metadataType := utils.PackedSlot
|
||||
metadataPackedNames := map[int]string{0: "name"}
|
||||
metadataPackedTypes := map[int]utils.ValueType{0: utils.Uint48}
|
||||
|
||||
expectedMetadata := utils.StorageValueMetadata{
|
||||
Name: metadataName,
|
||||
Keys: metadataKeys,
|
||||
Type: metadataType,
|
||||
PackedTypes: metadataPackedTypes,
|
||||
PackedNames: metadataPackedNames,
|
||||
}
|
||||
Expect(utils.GetStorageValueMetadataForPackedSlot(metadataName, metadataKeys, metadataType, metadataPackedNames, metadataPackedTypes)).To(Equal(expectedMetadata))
|
||||
})
|
||||
|
||||
It("panics if PackedTypes are nil when the type is PackedSlot", func() {
|
||||
metadataName := "fake_name"
|
||||
metadataKeys := map[utils.Key]string{"key": "value"}
|
||||
metadataType := utils.PackedSlot
|
||||
metadataPackedNames := map[int]string{0: "name"}
|
||||
|
||||
getMetadata := func() {
|
||||
utils.GetStorageValueMetadataForPackedSlot(metadataName, metadataKeys, metadataType, metadataPackedNames, nil)
|
||||
}
|
||||
Expect(getMetadata).To(Panic())
|
||||
})
|
||||
|
||||
It("panics if PackedNames are nil when the type is PackedSlot", func() {
|
||||
metadataName := "fake_name"
|
||||
metadataKeys := map[utils.Key]string{"key": "value"}
|
||||
metadataType := utils.PackedSlot
|
||||
metadataPackedTypes := map[int]utils.ValueType{0: utils.Uint48}
|
||||
|
||||
getMetadata := func() {
|
||||
utils.GetStorageValueMetadataForPackedSlot(metadataName, metadataKeys, metadataType, nil, metadataPackedTypes)
|
||||
}
|
||||
Expect(getMetadata).To(Panic())
|
||||
})
|
||||
|
||||
It("panics if valueType is not PackedSlot if PackedNames is populated", func() {
|
||||
metadataName := "fake_name"
|
||||
metadataKeys := map[utils.Key]string{"key": "value"}
|
||||
metadataType := utils.Uint48
|
||||
metadataPackedNames := map[int]string{0: "name"}
|
||||
metadataPackedTypes := map[int]utils.ValueType{0: utils.Uint48}
|
||||
|
||||
getMetadata := func() {
|
||||
utils.GetStorageValueMetadataForPackedSlot(metadataName, metadataKeys, metadataType, metadataPackedNames, metadataPackedTypes)
|
||||
}
|
||||
Expect(getMetadata).To(Panic())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -20,13 +20,12 @@ import (
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
|
||||
"github.com/vulcanize/vulcanizedb/libraries/shared/constants"
|
||||
"github.com/vulcanize/vulcanizedb/pkg/core"
|
||||
"github.com/vulcanize/vulcanizedb/pkg/datastore/postgres"
|
||||
)
|
||||
|
||||
type EventTransformer interface {
|
||||
Execute(logs []types.Log, header core.Header, recheckHeaders constants.TransformerExecution) error
|
||||
Execute(logs []types.Log, header core.Header) error
|
||||
GetConfig() EventTransformerConfig
|
||||
}
|
||||
|
||||
|
||||
@@ -96,11 +96,11 @@ func (watcher *EventWatcher) Execute(recheckHeaders constants.TransformerExecuti
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
notCheckedSQL := repository.CreateNotCheckedSQL(checkedColumnNames, recheckHeaders)
|
||||
notCheckedSQL := repository.CreateHeaderCheckedPredicateSQL(checkedColumnNames, recheckHeaders)
|
||||
|
||||
missingHeaders, err := repository.MissingHeaders(*watcher.StartingBlock, -1, watcher.DB, notCheckedSQL)
|
||||
if err != nil {
|
||||
logrus.Error("Fetching of missing headers failed in watcher!")
|
||||
logrus.Error("Couldn't fetch missing headers in watcher: ", err)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -108,7 +108,11 @@ func (watcher *EventWatcher) Execute(recheckHeaders constants.TransformerExecuti
|
||||
// TODO Extend FetchLogs for doing several blocks at a time
|
||||
logs, err := watcher.Fetcher.FetchLogs(watcher.Addresses, watcher.Topics, header)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error while fetching logs for header %v in watcher", header.Id)
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"headerId": header.Id,
|
||||
"headerHash": header.Hash,
|
||||
"blockNumber": header.BlockNumber,
|
||||
}).Errorf("Couldn't fetch logs for header: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -120,6 +124,7 @@ func (watcher *EventWatcher) Execute(recheckHeaders constants.TransformerExecuti
|
||||
|
||||
transformErr := watcher.transformLogs(logs, header)
|
||||
if transformErr != nil {
|
||||
logrus.Error("Could not transform logs: ", transformErr)
|
||||
return transformErr
|
||||
}
|
||||
}
|
||||
@@ -134,7 +139,7 @@ func (watcher *EventWatcher) transformLogs(logs []types.Log, header core.Header)
|
||||
for _, t := range watcher.Transformers {
|
||||
transformerName := t.GetConfig().TransformerName
|
||||
logChunk := chunkedLogs[transformerName]
|
||||
err := t.Execute(logChunk, header, constants.HeaderMissing)
|
||||
err := t.Execute(logChunk, header)
|
||||
if err != nil {
|
||||
logrus.Errorf("%v transformer failed to execute in watcher: %v", transformerName, err)
|
||||
return err
|
||||
|
||||
@@ -78,7 +78,7 @@ func (storageWatcher StorageWatcher) processRow(row utils.StorageDiffRow) {
|
||||
return
|
||||
}
|
||||
executeErr := storageTransformer.Execute(row)
|
||||
if executeErr != nil && executeErr != utils.ErrRowExists {
|
||||
if executeErr != nil {
|
||||
logrus.Warn(fmt.Sprintf("error executing storage transformer: %s", executeErr))
|
||||
queueErr := storageWatcher.Queue.Add(row)
|
||||
if queueErr != nil {
|
||||
@@ -100,7 +100,7 @@ func (storageWatcher StorageWatcher) processQueue() {
|
||||
continue
|
||||
}
|
||||
executeErr := storageTransformer.Execute(row)
|
||||
if executeErr == nil || executeErr == utils.ErrRowExists {
|
||||
if executeErr == nil {
|
||||
storageWatcher.deleteRow(row.Id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,18 +109,6 @@ var _ = Describe("Storage Watcher", func() {
|
||||
close(done)
|
||||
})
|
||||
|
||||
It("does not queue row if transformer execution fails because row already exists", func(done Done) {
|
||||
mockTransformer.ExecuteErr = utils.ErrRowExists
|
||||
|
||||
go storageWatcher.Execute(rows, errs, time.Hour)
|
||||
|
||||
Expect(<-errs).To(BeNil())
|
||||
Consistently(func() bool {
|
||||
return mockQueue.AddCalled
|
||||
}).Should(BeFalse())
|
||||
close(done)
|
||||
})
|
||||
|
||||
It("queues row for later processing if transformer execution fails", func(done Done) {
|
||||
mockTransformer.ExecuteErr = fakes.FakeError
|
||||
|
||||
@@ -199,17 +187,6 @@ var _ = Describe("Storage Watcher", func() {
|
||||
close(done)
|
||||
})
|
||||
|
||||
It("deletes row from queue if transformer execution errors because row already exists", func(done Done) {
|
||||
mockTransformer.ExecuteErr = utils.ErrRowExists
|
||||
|
||||
go storageWatcher.Execute(rows, errs, time.Nanosecond)
|
||||
|
||||
Eventually(func() int {
|
||||
return mockQueue.DeletePassedId
|
||||
}).Should(Equal(row.Id))
|
||||
close(done)
|
||||
})
|
||||
|
||||
It("logs error if deleting persisted row fails", func(done Done) {
|
||||
mockQueue.DeleteErr = fakes.FakeError
|
||||
tempFile, fileErr := ioutil.TempFile("", "log")
|
||||
|
||||
@@ -8,7 +8,10 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.SetFormatter(&log.JSONFormatter{})
|
||||
log.SetFormatter(&log.JSONFormatter{
|
||||
PrettyPrint: true,
|
||||
})
|
||||
log.SetReportCaller(true)
|
||||
file, err := os.OpenFile("vulcanizedb.log",
|
||||
os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
|
||||
if err == nil {
|
||||
|
||||
@@ -164,7 +164,7 @@ func (tr *Transformer) Init() error {
|
||||
// Iterates through stored, initialized contract objects
|
||||
// Iterates through contract's event filters, grabbing watched event logs
|
||||
// Uses converter to convert logs into custom log type
|
||||
// Persists converted logs into custuom postgres tables
|
||||
// Persists converted logs into custom postgres tables
|
||||
// Calls selected methods, using token holder address generated during event log conversion
|
||||
func (tr *Transformer) Execute() error {
|
||||
if len(tr.Contracts) == 0 {
|
||||
|
||||
@@ -30,7 +30,7 @@ type BlockChain interface {
|
||||
GetBlockByNumber(blockNumber int64) (Block, error)
|
||||
GetEthLogsWithCustomQuery(query ethereum.FilterQuery) ([]types.Log, error)
|
||||
GetHeaderByNumber(blockNumber int64) (Header, error)
|
||||
GetHeaderByNumbers(blockNumbers []int64) ([]Header, error)
|
||||
GetHeadersByNumbers(blockNumbers []int64) ([]Header, error)
|
||||
GetLogs(contract Contract, startingBlockNumber *big.Int, endingBlockNumber *big.Int) ([]Log, error)
|
||||
GetTransactions(transactionHashes []common.Hash) ([]TransactionModel, error)
|
||||
LastBlock() (*big.Int, error)
|
||||
|
||||
@@ -98,7 +98,7 @@ func (chain *MockBlockChain) GetHeaderByNumber(blockNumber int64) (core.Header,
|
||||
return core.Header{BlockNumber: blockNumber}, nil
|
||||
}
|
||||
|
||||
func (chain *MockBlockChain) GetHeaderByNumbers(blockNumbers []int64) ([]core.Header, error) {
|
||||
func (chain *MockBlockChain) GetHeadersByNumbers(blockNumbers []int64) ([]core.Header, error) {
|
||||
var headers []core.Header
|
||||
for _, blockNumber := range blockNumbers {
|
||||
var header = core.Header{BlockNumber: int64(blockNumber)}
|
||||
|
||||
@@ -79,7 +79,7 @@ func (blockChain *BlockChain) GetHeaderByNumber(blockNumber int64) (header core.
|
||||
return blockChain.getPOWHeader(blockNumber)
|
||||
}
|
||||
|
||||
func (blockChain *BlockChain) GetHeaderByNumbers(blockNumbers []int64) (header []core.Header, err error) {
|
||||
func (blockChain *BlockChain) GetHeadersByNumbers(blockNumbers []int64) (header []core.Header, err error) {
|
||||
if blockChain.node.NetworkID == core.KOVAN_NETWORK_ID {
|
||||
return blockChain.getPOAHeaders(blockNumbers)
|
||||
}
|
||||
@@ -129,6 +129,9 @@ func (blockChain *BlockChain) GetTransactions(transactionHashes []common.Hash) (
|
||||
|
||||
func (blockChain *BlockChain) LastBlock() (*big.Int, error) {
|
||||
block, err := blockChain.ethClient.HeaderByNumber(context.Background(), nil)
|
||||
if err != nil {
|
||||
return big.NewInt(0), err
|
||||
}
|
||||
return block.Number, err
|
||||
}
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ var _ = Describe("Geth blockchain", func() {
|
||||
})
|
||||
|
||||
It("fetches headers with multiple blocks", func() {
|
||||
_, err := blockChain.GetHeaderByNumbers([]int64{100, 99})
|
||||
_, err := blockChain.GetHeadersByNumbers([]int64{100, 99})
|
||||
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
mockRpcClient.AssertBatchCalledWith("eth_getBlockByNumber", 2)
|
||||
@@ -139,7 +139,7 @@ var _ = Describe("Geth blockchain", func() {
|
||||
blockNumber := hexutil.Big(*big.NewInt(100))
|
||||
mockRpcClient.SetReturnPOAHeaders([]vulcCore.POAHeader{{Number: &blockNumber}})
|
||||
|
||||
_, err := blockChain.GetHeaderByNumbers([]int64{100, 99})
|
||||
_, err := blockChain.GetHeadersByNumbers([]int64{100, 99})
|
||||
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
mockRpcClient.AssertBatchCalledWith("eth_getBlockByNumber", 2)
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package history
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/vulcanize/vulcanizedb/pkg/core"
|
||||
@@ -35,7 +36,7 @@ func PopulateMissingBlocks(blockchain core.BlockChain, blockRepository datastore
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
log.Printf("Backfilling %d blocks\n\n", len(blockRange))
|
||||
log.Debug(getBlockRangeString(blockRange))
|
||||
_, err = RetrieveAndUpdateBlocks(blockchain, blockRepository, blockRange)
|
||||
if err != nil {
|
||||
log.Error("PopulateMissingBlocks: error gettings/updating blocks: ", err)
|
||||
@@ -61,3 +62,7 @@ func RetrieveAndUpdateBlocks(blockchain core.BlockChain, blockRepository datasto
|
||||
}
|
||||
return len(blockNumbers), nil
|
||||
}
|
||||
|
||||
func getBlockRangeString(blockRange []int64) string {
|
||||
return fmt.Sprintf("Backfilling |%v| blocks", len(blockRange))
|
||||
}
|
||||
|
||||
@@ -24,14 +24,14 @@ import (
|
||||
"github.com/vulcanize/vulcanizedb/pkg/datastore/postgres/repositories"
|
||||
)
|
||||
|
||||
func PopulateMissingHeaders(blockchain core.BlockChain, headerRepository datastore.HeaderRepository, startingBlockNumber int64) (int, error) {
|
||||
lastBlock, err := blockchain.LastBlock()
|
||||
func PopulateMissingHeaders(blockChain core.BlockChain, headerRepository datastore.HeaderRepository, startingBlockNumber int64) (int, error) {
|
||||
lastBlock, err := blockChain.LastBlock()
|
||||
if err != nil {
|
||||
log.Error("PopulateMissingHeaders: Error getting last block: ", err)
|
||||
return 0, err
|
||||
}
|
||||
|
||||
blockNumbers, err := headerRepository.MissingBlockNumbers(startingBlockNumber, lastBlock.Int64(), blockchain.Node().ID)
|
||||
blockNumbers, err := headerRepository.MissingBlockNumbers(startingBlockNumber, lastBlock.Int64(), blockChain.Node().ID)
|
||||
if err != nil {
|
||||
log.Error("PopulateMissingHeaders: Error getting missing block numbers: ", err)
|
||||
return 0, err
|
||||
@@ -39,8 +39,8 @@ func PopulateMissingHeaders(blockchain core.BlockChain, headerRepository datasto
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
log.Printf("Backfilling %d blocks\n\n", len(blockNumbers))
|
||||
_, err = RetrieveAndUpdateHeaders(blockchain, headerRepository, blockNumbers)
|
||||
log.Debug(getBlockRangeString(blockNumbers))
|
||||
_, err = RetrieveAndUpdateHeaders(blockChain, headerRepository, blockNumbers)
|
||||
if err != nil {
|
||||
log.Error("PopulateMissingHeaders: Error getting/updating headers:", err)
|
||||
return 0, err
|
||||
@@ -48,8 +48,8 @@ func PopulateMissingHeaders(blockchain core.BlockChain, headerRepository datasto
|
||||
return len(blockNumbers), nil
|
||||
}
|
||||
|
||||
func RetrieveAndUpdateHeaders(chain core.BlockChain, headerRepository datastore.HeaderRepository, blockNumbers []int64) (int, error) {
|
||||
headers, err := chain.GetHeaderByNumbers(blockNumbers)
|
||||
func RetrieveAndUpdateHeaders(blockChain core.BlockChain, headerRepository datastore.HeaderRepository, blockNumbers []int64) (int, error) {
|
||||
headers, err := blockChain.GetHeadersByNumbers(blockNumbers)
|
||||
for _, header := range headers {
|
||||
_, err = headerRepository.CreateOrUpdateHeader(header)
|
||||
if err != nil {
|
||||
|
||||
@@ -50,6 +50,6 @@ func MakeRange(min, max int64) []int64 {
|
||||
}
|
||||
|
||||
func (window ValidationWindow) GetString() string {
|
||||
return fmt.Sprintf("Validating Blocks |%v|-- Validation Window --|%v}|",
|
||||
return fmt.Sprintf("Validating Blocks |%v|-- Validation Window --|%v|",
|
||||
window.LowerBound, window.UpperBound)
|
||||
}
|
||||
|
||||
@@ -17,13 +17,15 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/lib/pq"
|
||||
"github.com/pressly/goose"
|
||||
"github.com/vulcanize/vulcanizedb/pkg/config"
|
||||
"github.com/vulcanize/vulcanizedb/pkg/plugin/helpers"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
@@ -36,6 +38,7 @@ type manager struct {
|
||||
GenConfig config.Plugin
|
||||
DBConfig config.Database
|
||||
tmpMigDir string
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
// Manager requires both filled in generator and database configs
|
||||
@@ -46,6 +49,22 @@ func NewMigrationManager(gc config.Plugin, dbc config.Database) *manager {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *manager) setDB() error {
|
||||
var pgStr string
|
||||
if len(m.DBConfig.User) > 0 && len(m.DBConfig.Password) > 0 {
|
||||
pgStr = fmt.Sprintf("postgresql://%s:%s@%s:%d/%s?sslmode=disable",
|
||||
m.DBConfig.User, m.DBConfig.Password, m.DBConfig.Hostname, m.DBConfig.Port, m.DBConfig.Name)
|
||||
} else {
|
||||
pgStr = fmt.Sprintf("postgres://%s:%d/%s?sslmode=disable", m.DBConfig.Hostname, m.DBConfig.Port, m.DBConfig.Name)
|
||||
}
|
||||
dbConnector, err := pq.NewConnector(pgStr)
|
||||
if err != nil {
|
||||
return errors.New(fmt.Sprintf("can't connect to db: %s", err.Error()))
|
||||
}
|
||||
m.db = sql.OpenDB(dbConnector)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *manager) RunMigrations() error {
|
||||
// Get paths to db migrations from the plugin config
|
||||
paths, err := m.GenConfig.GetMigrationsPaths()
|
||||
@@ -73,17 +92,19 @@ func (m *manager) RunMigrations() error {
|
||||
// Setup a temporary directory to hold transformer db migrations
|
||||
func (m *manager) setupMigrationEnv() error {
|
||||
var err error
|
||||
m.tmpMigDir, err = helpers.CleanPath(filepath.Join("$GOPATH/src", m.GenConfig.Home+".plugin_migrations"))
|
||||
m.tmpMigDir, err = helpers.CleanPath(filepath.Join("$GOPATH/src", m.GenConfig.Home, ".plugin_migrations"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.RemoveAll(m.tmpMigDir)
|
||||
if err != nil {
|
||||
return errors.New(fmt.Sprintf("unable to remove file found at %s where tmp directory needs to be written", m.tmpMigDir))
|
||||
removeErr := os.RemoveAll(m.tmpMigDir)
|
||||
if removeErr != nil {
|
||||
removeErrString := "unable to remove file found at %s where tmp directory needs to be written: %s"
|
||||
return errors.New(fmt.Sprintf(removeErrString, m.tmpMigDir, removeErr.Error()))
|
||||
}
|
||||
err = os.Mkdir(m.tmpMigDir, os.FileMode(os.ModePerm))
|
||||
if err != nil {
|
||||
return errors.New(fmt.Sprintf("unable to create temporary migration directory %s", m.tmpMigDir))
|
||||
mkdirErr := os.Mkdir(m.tmpMigDir, os.FileMode(os.ModePerm))
|
||||
if mkdirErr != nil {
|
||||
mkdirErrString := "unable to create temporary migration directory %s: %s"
|
||||
return errors.New(fmt.Sprintf(mkdirErrString, m.tmpMigDir, mkdirErr.Error()))
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -120,28 +141,23 @@ func (m *manager) createMigrationCopies(paths []string) error {
|
||||
}
|
||||
|
||||
func (m *manager) fixAndRun(path string) error {
|
||||
// Setup DB if not set
|
||||
if m.db == nil {
|
||||
setErr := m.setDB()
|
||||
if setErr != nil {
|
||||
return errors.New(fmt.Sprintf("could not open db: %s", setErr.Error()))
|
||||
}
|
||||
}
|
||||
// Fix the migrations
|
||||
cmd := exec.Command("goose", "fix")
|
||||
cmd.Dir = m.tmpMigDir
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
return errors.New(fmt.Sprintf("version fixing for plugin migrations at %s failed: %s", path, err.Error()))
|
||||
fixErr := goose.Fix(m.tmpMigDir)
|
||||
if fixErr != nil {
|
||||
return errors.New(fmt.Sprintf("version fixing for plugin migrations at %s failed: %s", path, fixErr.Error()))
|
||||
}
|
||||
// Run the copied migrations with goose
|
||||
var pgStr string
|
||||
if len(m.DBConfig.User) > 0 && len(m.DBConfig.Password) > 0 {
|
||||
pgStr = fmt.Sprintf("postgresql://%s:%s@%s:%d/%s?sslmode=disable",
|
||||
m.DBConfig.User, m.DBConfig.Password, m.DBConfig.Hostname, m.DBConfig.Port, m.DBConfig.Name)
|
||||
} else {
|
||||
pgStr = fmt.Sprintf("postgres://%s:%d/%s?sslmode=disable", m.DBConfig.Hostname, m.DBConfig.Port, m.DBConfig.Name)
|
||||
upErr := goose.Up(m.db, m.tmpMigDir)
|
||||
if upErr != nil {
|
||||
return errors.New(fmt.Sprintf("db migrations for plugin transformers at %s failed: %s", path, upErr.Error()))
|
||||
}
|
||||
cmd = exec.Command("goose", "postgres", pgStr, "up")
|
||||
cmd.Dir = m.tmpMigDir
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
return errors.New(fmt.Sprintf("db migrations for plugin transformers at %s failed: %s", path, err.Error()))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
node_modules
|
||||
Dockerfile
|
||||
README.md
|
||||
spec
|
||||
.dockerignore
|
||||
@@ -1,9 +0,0 @@
|
||||
FROM mhart/alpine-node:10
|
||||
RUN apk --update --no-cache add make g++ python findutils postgresql-dev
|
||||
|
||||
WORKDIR /app
|
||||
COPY . /app
|
||||
run yarn install
|
||||
RUN ["./node_modules/typescript/bin/tsc"]
|
||||
EXPOSE 3000
|
||||
CMD ["node", "/app/build/dist/index.js"]
|
||||
@@ -1,51 +0,0 @@
|
||||
# Vulcanize GraphQL API
|
||||
|
||||
This application utilizes Postgraphile to expose GraphQL endpoints for exposure of the varied data that VulcanizeDB tracks.
|
||||
|
||||
## Docker use
|
||||
_Note: currently this image is ~500MB large (unpacked)_
|
||||
|
||||
Build the docker image in this directory. Start the `GraphiQL` frontend by:
|
||||
* Setting the env variables for the database connection: `DATABASE_HOST`,
|
||||
`DATABASE_NAME`, `DATABASE_USER`, `DATABASE_PASSWORD` (and optionally
|
||||
`DATABASE_PORT` if running on non-standard port).
|
||||
* The specified user needs to be `superuser` on the vulcanizeDB database,
|
||||
so postgraphile can setup watch fixtures keeping track of live schema
|
||||
changes.
|
||||
* To limit the amount of available queries in GraphQL, a restricted user can be used
|
||||
for postgraphile introspection by adding env variables `GQ_USER` and `GQ_PASSWORD`.
|
||||
* By doing `GRANT [SELECT | EXECUTE]` on tables/functions for this user,
|
||||
you can selectively assign things you want available in GraphQL.
|
||||
* You still need to pass in a superuser with `DATABASE_USER` & `DATABASE_PASSWORD` for
|
||||
the postgraphile watch fixtures to work.
|
||||
* By default, postgraphile publishes the `public` schema. This can be expanded with for example `GQ_SCHEMAS=public,maker`
|
||||
* Run the container (ex. `docker run -e DATABASE_HOST=localhost -e DATABASE_NAME=my_database -e DATABASE_USER=superuser -e DATABASE_PASSWORD=superuser -e GQ_USER=graphql -e GQ_PASSWORD=graphql -e GQ_SCHEMAS=public,anotherSchema -d my-postgraphile-image`)
|
||||
* GraphiQL frontend is available at `:3000/graphiql`
|
||||
GraphQL endpoint is available at `:3000/graphql`
|
||||
|
||||
By default, this build will expose only the "public" schema and will disable mutations - to change mutation behaviour, you can use an optional config file `config.toml` and set the env var `POSTGRAPHILE_CONFIG_PATH` to point to its location. Example `toml`:
|
||||
|
||||
```
|
||||
[database]
|
||||
name = "vulcanize_public"
|
||||
hostname = "localhost"
|
||||
port = 5432
|
||||
gq_schemas = ["public", "yourschema"]
|
||||
gq_user = "graphql"
|
||||
gq_password = "graphql"
|
||||
disable_default_mutations = false
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
*This application assumes the use of the [Yarn package manager](https://yarnpkg.com/en/). The use of npm may produce unexpected results.*
|
||||
|
||||
Install dependencies with `yarn` and execute `yarn build`. The bundle produced by Webpack will be present in `build/dist/`.
|
||||
|
||||
## Running
|
||||
|
||||
Provide the built bundle to node as a runnable script: `node ./build/dist/vulcanize-postgraphile-server.js`
|
||||
|
||||
## Testing
|
||||
|
||||
Tests are executed via Jasmine with a console reporter via the `yarn test` task.
|
||||
@@ -1,57 +0,0 @@
|
||||
{
|
||||
"name": "vulcanizedb",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"build": "rm -rf ./build/dist && webpack --config=./webpack.config.js",
|
||||
"lint": "tslint --project ./tsconfig.json --config ./tslint.json",
|
||||
"start": "npm run build && node ./build/dist/vulcanize-postgraphile-server.js",
|
||||
"dev": "./node_modules/typescript/bin/tsc && node build/dist/src/index.js",
|
||||
"test": "rm -rf ./build/spec && tsc --build ./tsconfig.test.json && jasmine --config=./spec/support/jasmine.json",
|
||||
"test:ci": "npm run lint && npm run test"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/vulcanize/vulcanizedb.git"
|
||||
},
|
||||
"author": "Vulcanize, Inc.",
|
||||
"license": "AGPL-3.0",
|
||||
"bugs": {
|
||||
"url": "https://github.com/vulcanize/vulcanizedb/issues"
|
||||
},
|
||||
"homepage": "https://github.com/vulcanize/vulcanizedb",
|
||||
"dependencies": {
|
||||
"express-session": "1.15.6",
|
||||
"graphql-subscriptions": "0.5.8",
|
||||
"lodash": ">=4.17.11",
|
||||
"passport": "0.4.0",
|
||||
"pg": "6.4.2",
|
||||
"pg-native": "3.0.0",
|
||||
"postgraphile": "4.4.0-beta.11",
|
||||
"subscriptions-transport-ws": "0.9.14",
|
||||
"toml": "2.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "4.16.0",
|
||||
"@types/express-session": "1.15.10",
|
||||
"@types/graphql": "^0.13.4",
|
||||
"@types/jasmine": "2.8.8",
|
||||
"@types/lodash": "4.14.116",
|
||||
"@types/node": "^10.12.21",
|
||||
"@types/passport": "0.4.6",
|
||||
"@graphile-contrib/pg-simplify-inflector": "3.0.0",
|
||||
"awesome-typescript-loader": "5.2.0",
|
||||
"jasmine": "3.2.0",
|
||||
"jasmine-ts-console-reporter": "3.1.1",
|
||||
"source-map-loader": "0.2.4",
|
||||
"tslint": "5.11.0",
|
||||
"tslint-eslint-rules": "5.4.0",
|
||||
"typescript": "3.0.1",
|
||||
"webpack": "4.17.1",
|
||||
"webpack-cli": "3.1.0",
|
||||
"webpack-dev-server": ">=3.1.11"
|
||||
},
|
||||
"resolutions": {
|
||||
"pg": "6.4.2"
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
import { parseConfig } from '../../src/config/parse';
|
||||
|
||||
describe('parseConfig', () => {
|
||||
let configPath: string;
|
||||
let tomlContents: string;
|
||||
let parsedToml: object;
|
||||
let readCallback: jasmine.Spy;
|
||||
let tomlParseCallback: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
configPath = '/example/config/path.toml';
|
||||
tomlContents = `[database]\nname = 'example_database'\n `
|
||||
+ `hostname = 'example.com'\nport = 1234`;
|
||||
|
||||
parsedToml = {
|
||||
database: {
|
||||
hostname: 'example.com',
|
||||
name: 'example_database',
|
||||
port: '1234',
|
||||
user: 'user',
|
||||
password: 'password'
|
||||
}
|
||||
};
|
||||
|
||||
readCallback = jasmine.createSpy('readCallback');
|
||||
readCallback.and.returnValue(tomlContents);
|
||||
|
||||
tomlParseCallback = jasmine.createSpy('tomlParseCallback');
|
||||
tomlParseCallback.and.returnValue(parsedToml);
|
||||
});
|
||||
|
||||
it('provides the database host', () => {
|
||||
const databaseConfig = parseConfig(
|
||||
readCallback, tomlParseCallback, configPath);
|
||||
|
||||
expect(databaseConfig.host)
|
||||
.toEqual('postgres://user:password@example.com:1234');
|
||||
});
|
||||
|
||||
it('provides the database name', () => {
|
||||
const databaseConfig = parseConfig(
|
||||
readCallback, tomlParseCallback, configPath);
|
||||
|
||||
expect(databaseConfig.database).toEqual('example_database');
|
||||
});
|
||||
|
||||
it('handles a missing config path', () => {
|
||||
const failingCall = () =>
|
||||
parseConfig(readCallback, tomlParseCallback, '');
|
||||
|
||||
tomlParseCallback.and.returnValue({
|
||||
database: { hostname: 'example.com', name: 'example_database' }
|
||||
});
|
||||
|
||||
expect(failingCall).toThrow();
|
||||
});
|
||||
|
||||
it('handles a missing database host', () => {
|
||||
const failingCall = () =>
|
||||
parseConfig(readCallback, tomlParseCallback, configPath);
|
||||
|
||||
tomlParseCallback.and.returnValue({
|
||||
database: { hostname: '', name: 'example_database' }
|
||||
});
|
||||
|
||||
expect(failingCall).toThrow();
|
||||
});
|
||||
|
||||
it('handles a missing database name', () => {
|
||||
const failingCall = () =>
|
||||
parseConfig(readCallback, tomlParseCallback, configPath);
|
||||
|
||||
tomlParseCallback.and.returnValue({
|
||||
database: { hostname: 'example.com', name: '', port: '1234' }
|
||||
});
|
||||
|
||||
expect(failingCall).toThrow();
|
||||
});
|
||||
});
|
||||
@@ -1,7 +0,0 @@
|
||||
// NOTE: This file helps Jasmine
|
||||
// comprehend TS sourcemaps by installing a reporter
|
||||
// specifically for TypeScript
|
||||
const TSConsoleReporter = require('jasmine-ts-console-reporter');
|
||||
|
||||
jasmine.getEnv().clearReporters();
|
||||
jasmine.getEnv().addReporter(new TSConsoleReporter());
|
||||
@@ -1,106 +0,0 @@
|
||||
import { PassportStatic } from 'passport';
|
||||
|
||||
import { PostgraphileMiddleware } from '../../src/adapters/postgraphile';
|
||||
import { buildServerConfig } from '../../src/server/config';
|
||||
|
||||
import {
|
||||
DatabaseConfig,
|
||||
ServerConfig,
|
||||
ServerUtilities
|
||||
} from '../../src/server/interface';
|
||||
|
||||
describe('buildServerConfig', () => {
|
||||
let configParser: jasmine.Spy;
|
||||
let postgraphileMiddleware: PostgraphileMiddleware;
|
||||
let expressSessionHandler: jasmine.Spy;
|
||||
let passportInitializer: jasmine.Spy;
|
||||
let passportSessionHandler: jasmine.Spy;
|
||||
let serverConfig: ServerConfig;
|
||||
|
||||
let serverUtilities: ServerUtilities;
|
||||
let databaseConfig: DatabaseConfig;
|
||||
|
||||
beforeEach(() => {
|
||||
databaseConfig = {
|
||||
host: 'example.com',
|
||||
database: 'example_database',
|
||||
schemas: ['public'],
|
||||
ownerConnectionString: 'postgres://admin:admin@host',
|
||||
disableDefaultMutations: true
|
||||
};
|
||||
|
||||
postgraphileMiddleware = jasmine
|
||||
.createSpyObj<PostgraphileMiddleware>(['call']);
|
||||
|
||||
serverUtilities = {
|
||||
pluginHook: jasmine.createSpy('pluginHook'),
|
||||
express: jasmine.createSpy('express'),
|
||||
expressSession: jasmine.createSpy('expressSession'),
|
||||
httpServerFactory: jasmine.createSpy('httpServerFactory'),
|
||||
passport: jasmine.createSpyObj<PassportStatic>(['initialize', 'session']),
|
||||
postgraphile: jasmine.createSpy('postgraphile')
|
||||
};
|
||||
|
||||
const rawConfig: object = { exampleOption: 'example value' };
|
||||
|
||||
configParser = jasmine.createSpy('configParser');
|
||||
configParser.and.returnValue(rawConfig);
|
||||
|
||||
expressSessionHandler = jasmine.createSpy('expressSessionHandler');
|
||||
passportInitializer = jasmine.createSpy('passportInitializer');
|
||||
passportSessionHandler = jasmine.createSpy('passportSessionHandler');
|
||||
|
||||
(serverUtilities.postgraphile as jasmine.Spy)
|
||||
.and.returnValue(postgraphileMiddleware);
|
||||
(serverUtilities.expressSession as jasmine.Spy)
|
||||
.and.returnValue(expressSessionHandler);
|
||||
(serverUtilities.passport.initialize as jasmine.Spy)
|
||||
.and.returnValue(passportInitializer);
|
||||
(serverUtilities.passport.session as jasmine.Spy)
|
||||
.and.returnValue(passportSessionHandler);
|
||||
|
||||
serverConfig = buildServerConfig(
|
||||
serverUtilities, databaseConfig, undefined);
|
||||
});
|
||||
|
||||
it('provides the Postgraphile options', () => {
|
||||
expect(serverConfig.options).not.toBeNull();
|
||||
});
|
||||
|
||||
it('it adds the express session handler as the first middleware', () => {
|
||||
expect(serverConfig.options.webSocketMiddlewares[0])
|
||||
.toBe(expressSessionHandler);
|
||||
});
|
||||
|
||||
it('it adds the passport initializer as the second middleware', () => {
|
||||
expect(serverConfig.options.webSocketMiddlewares[1])
|
||||
.toBe(passportInitializer);
|
||||
});
|
||||
|
||||
it('it adds the passport session handler as the third middleware', () => {
|
||||
expect(serverConfig.options.webSocketMiddlewares[2])
|
||||
.toBe(passportSessionHandler);
|
||||
});
|
||||
|
||||
it('provides the database config to Postgraphile', () => {
|
||||
expect(serverUtilities.postgraphile).toHaveBeenCalledWith(
|
||||
`${databaseConfig.host}/${databaseConfig.database}`,
|
||||
databaseConfig.schemas,
|
||||
jasmine.any(Object));
|
||||
});
|
||||
|
||||
it('provides the Postgraphile middleware', () => {
|
||||
expect(serverConfig.middleware).toBe(postgraphileMiddleware);
|
||||
});
|
||||
|
||||
it('sets the default server port', () => {
|
||||
expect(serverConfig.port).toEqual(3000);
|
||||
});
|
||||
|
||||
it('sets an explicit server port', () => {
|
||||
const serverConfigWithPort = buildServerConfig(
|
||||
serverUtilities, databaseConfig, '1234');
|
||||
|
||||
expect(serverConfigWithPort.port).toEqual(1234);
|
||||
});
|
||||
});
|
||||
@@ -1,66 +0,0 @@
|
||||
import { Express } from 'express';
|
||||
import { PassportStatic } from 'passport';
|
||||
import { Server } from 'http';
|
||||
|
||||
import { ServerUtilities, ServerConfig } from '../../src/server/interface';
|
||||
import { bootServer } from '../../src/server/runtime';
|
||||
import { PostgraphileMiddleware } from '../../src/adapters/postgraphile';
|
||||
|
||||
describe('bootServer', () => {
|
||||
let serverUtilities: ServerUtilities;
|
||||
let serverConfig: ServerConfig;
|
||||
let mockExpressApp: Express;
|
||||
let mockHttpServer: Server;
|
||||
|
||||
beforeEach(() => {
|
||||
serverUtilities = {
|
||||
pluginHook: jasmine.createSpy('pluginHook'),
|
||||
express: jasmine.createSpy('express'),
|
||||
expressSession: jasmine.createSpy('expressSession'),
|
||||
httpServerFactory: jasmine.createSpy('httpServerFactory'),
|
||||
passport: jasmine.createSpyObj<PassportStatic>(['initialize', 'session']),
|
||||
postgraphile: jasmine.createSpy('postgraphile')
|
||||
};
|
||||
|
||||
serverConfig = {
|
||||
middleware: jasmine.createSpyObj<PostgraphileMiddleware>(['call']),
|
||||
options: {
|
||||
appendPlugins: [],
|
||||
disableDefaultMutations: false,
|
||||
enableCors: true,
|
||||
exportGqlSchemaPath: '',
|
||||
graphiql: true,
|
||||
ignoreRBAC: false,
|
||||
ownerConnectionString: '',
|
||||
pluginHook: jasmine.createSpy('pluginHook'),
|
||||
watchPg: true,
|
||||
webSocketMiddlewares: [] },
|
||||
port: 5678
|
||||
};
|
||||
|
||||
mockExpressApp = jasmine.createSpyObj<Express>(['use']);
|
||||
(serverUtilities.express as jasmine.Spy)
|
||||
.and.returnValue(mockExpressApp);
|
||||
|
||||
mockHttpServer = jasmine.createSpyObj<Server>(['listen']);
|
||||
(serverUtilities.httpServerFactory as jasmine.Spy)
|
||||
.and.returnValue(mockHttpServer);
|
||||
|
||||
bootServer(serverUtilities, serverConfig);
|
||||
});
|
||||
|
||||
it('builds a new, Node HTTP server', () => {
|
||||
expect(serverUtilities.httpServerFactory)
|
||||
.toHaveBeenCalledWith(mockExpressApp);
|
||||
});
|
||||
|
||||
it('provides Postgraphile middleware to the Express app', () => {
|
||||
const useSpy = mockExpressApp.use as jasmine.Spy;
|
||||
expect(useSpy).toHaveBeenCalledWith(serverConfig.middleware);
|
||||
});
|
||||
|
||||
it('instructs the server to listen on the given port', () => {
|
||||
const listenSpy = mockHttpServer.listen as jasmine.Spy;
|
||||
expect(listenSpy).toHaveBeenCalledWith(serverConfig.port);
|
||||
});
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"spec_dir": "build/spec/",
|
||||
"spec_files": [
|
||||
"**/*[sS]pec.js"
|
||||
],
|
||||
"helpers": [
|
||||
"helpers/*.js"
|
||||
],
|
||||
"stopSpecOnExpectationFailure": false,
|
||||
"random": true
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import { URL } from 'url';
|
||||
|
||||
export type ReadFileSyncCallback = (
|
||||
path: string | number | Buffer | URL,
|
||||
options?: { encoding?: null | undefined; flag?: string | undefined; }
|
||||
| null
|
||||
| undefined
|
||||
) => string | Buffer;
|
||||
|
||||
export type TomlParseCallback
|
||||
= (fileContents: string) => { [key: string]: { [key: string]: any } };
|
||||
@@ -1,7 +0,0 @@
|
||||
import { IncomingMessage, ServerResponse, Server } from 'http';
|
||||
|
||||
export type RequestListenerCallback
|
||||
= (request: IncomingMessage, response: ServerResponse) => void;
|
||||
|
||||
export type CreateHttpServerCallback
|
||||
= (requestListener?: RequestListenerCallback) => Server;
|
||||
@@ -1,32 +0,0 @@
|
||||
import { RequestHandler } from 'express';
|
||||
import {PluginHookFn } from 'postgraphile/build/postgraphile/pluginHook';
|
||||
import {Plugin} from 'postgraphile';
|
||||
|
||||
// NOTE: Shape of the middleware is not
|
||||
// currently important to this application, but if a need arises,
|
||||
// any needed shape can be assigned from a custom type here. For
|
||||
// the time being, this is a named stub to provide clarity.
|
||||
export interface PostgraphileMiddleware extends RequestHandler {}
|
||||
|
||||
export interface PostgraphileOptions {
|
||||
appendPlugins: Plugin[];
|
||||
disableDefaultMutations: boolean;
|
||||
enableCors: boolean;
|
||||
exportGqlSchemaPath: string;
|
||||
graphiql: boolean;
|
||||
ignoreRBAC: boolean;
|
||||
ownerConnectionString: string;
|
||||
pluginHook: PluginHookFn;
|
||||
watchPg: boolean;
|
||||
// NOTE Shape of the middlewares is not
|
||||
// currently important to this application, but if a need arises,
|
||||
// any needed shape can be assigned from a custom type here.
|
||||
webSocketMiddlewares: object[];
|
||||
}
|
||||
|
||||
export type PostgraphileInitCallback = (
|
||||
databaseUrl: string,
|
||||
schemas: string[],
|
||||
options: PostgraphileOptions
|
||||
) => PostgraphileMiddleware;
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import { SessionOptions } from 'express-session';
|
||||
import { Express, RequestHandler, Handler } from 'express';
|
||||
|
||||
export type ExpressInitCallback = () => Express;
|
||||
|
||||
export type ExpressSessionInitCallback
|
||||
= (options?: SessionOptions) => RequestHandler ;
|
||||
|
||||
export type PassportInitCallback = (
|
||||
options?: { userProperty: string; } | undefined
|
||||
) => Handler;
|
||||
|
||||
export type PassportSessionCallback = (
|
||||
options?: { pauseStream: boolean; } | undefined
|
||||
) => Handler;
|
||||
|
||||
export interface StaticPassportProvider {
|
||||
initialize: PassportInitCallback;
|
||||
session: PassportSessionCallback;
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
import { CONFIG_PATH_KEY } from '../server/config';
|
||||
import { DatabaseConfig } from '../server/interface';
|
||||
import { ReadFileSyncCallback, TomlParseCallback } from '../adapters/fs';
|
||||
|
||||
export const MISSING_PATH_MESSAGE = `No path to config toml file provided, `
|
||||
+ `please check the value of ${CONFIG_PATH_KEY} in your environment`;
|
||||
|
||||
export const MISSING_HOST_MESSAGE = 'No database host provided in config toml';
|
||||
export const MISSING_USER_MESSAGE = 'No database user & password '
|
||||
+ 'provided in config toml';
|
||||
export const MISSING_DATABASE_MESSAGE = 'No database name provided '
|
||||
+ 'in config toml';
|
||||
|
||||
export function parseConfig(
|
||||
readCallback: ReadFileSyncCallback,
|
||||
tomlParseCallback: TomlParseCallback,
|
||||
configPath?: string
|
||||
): DatabaseConfig {
|
||||
let host = '';
|
||||
let port = '';
|
||||
let database = '';
|
||||
let user = '';
|
||||
let password = '';
|
||||
let gqSchemas = ['public'];
|
||||
let gqUser = '';
|
||||
let gqPassword = '';
|
||||
let disableDefaultMutations = true;
|
||||
|
||||
if (configPath) {
|
||||
const tomlContents = readCallback(`${configPath}`).toString();
|
||||
const parsedToml = tomlParseCallback(tomlContents);
|
||||
|
||||
host = parsedToml['database']['hostname'];
|
||||
port = parsedToml['database']['port'];
|
||||
database = parsedToml['database']['name'];
|
||||
user = parsedToml['database']['user'];
|
||||
password = parsedToml['database']['password'];
|
||||
gqSchemas = parsedToml['database']['gq_schemas'];
|
||||
gqUser = parsedToml['database']['gq_user'] || gqUser;
|
||||
gqPassword = parsedToml['database']['gq_password'] || gqPassword;
|
||||
disableDefaultMutations = parsedToml['database']['disable_default_mutations'] === undefined
|
||||
? true
|
||||
: parsedToml['database']['disable_default_mutations'];
|
||||
}
|
||||
|
||||
// Overwrite config values with env. vars if such are set
|
||||
host = process.env.DATABASE_HOST || host;
|
||||
port = process.env.DATABASE_PORT || port;
|
||||
database = process.env.DATABASE_NAME || database;
|
||||
user = process.env.DATABASE_USER || user;
|
||||
password = process.env.DATABASE_PASSWORD || password;
|
||||
gqSchemas = process.env.GQ_SCHEMAS
|
||||
? process.env.GQ_SCHEMAS.split(',')
|
||||
: gqSchemas;
|
||||
gqUser = process.env.GQ_USER || gqUser;
|
||||
gqPassword = process.env.GQ_PASSWORD || gqPassword;
|
||||
|
||||
if (!host) {
|
||||
throw new Error(MISSING_HOST_MESSAGE);
|
||||
}
|
||||
|
||||
if (!database) {
|
||||
throw new Error(MISSING_DATABASE_MESSAGE);
|
||||
}
|
||||
|
||||
if (!user || !password) {
|
||||
throw new Error(MISSING_USER_MESSAGE);
|
||||
}
|
||||
|
||||
return {
|
||||
host: gqUser && gqPassword
|
||||
? `postgres://${gqUser}:${gqPassword}@${host}:${port}`
|
||||
: `postgres://${user}:${password}@${host}:${port}`,
|
||||
database,
|
||||
schemas: gqSchemas,
|
||||
ownerConnectionString: `postgres://${user}:${password}@${host}:${port}/${database}`,
|
||||
disableDefaultMutations
|
||||
};
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import { createServer } from 'http';
|
||||
import { postgraphile, makePluginHook } from 'postgraphile';
|
||||
import { readFileSync } from 'fs';
|
||||
|
||||
import express = require('express');
|
||||
import passport = require('passport');
|
||||
import session = require('express-session');
|
||||
import toml = require('toml');
|
||||
|
||||
const pluginHook = makePluginHook([]);
|
||||
|
||||
import { ServerUtilities } from './server/interface';
|
||||
import { bootServer } from './server/runtime';
|
||||
import { parseConfig } from './config/parse';
|
||||
|
||||
import {
|
||||
buildServerConfig,
|
||||
CONFIG_PATH_KEY,
|
||||
SERVER_PORT_KEY
|
||||
} from './server/config';
|
||||
|
||||
const configPath = process.env[CONFIG_PATH_KEY];
|
||||
const serverPort = process.env[SERVER_PORT_KEY];
|
||||
|
||||
const serverUtilities: ServerUtilities = {
|
||||
express,
|
||||
expressSession: session,
|
||||
httpServerFactory: createServer,
|
||||
passport,
|
||||
postgraphile,
|
||||
pluginHook
|
||||
};
|
||||
|
||||
const databaseConfig = parseConfig(readFileSync, toml.parse, configPath);
|
||||
const serverConfig = buildServerConfig(
|
||||
serverUtilities, databaseConfig, serverPort);
|
||||
|
||||
bootServer(serverUtilities, serverConfig);
|
||||
@@ -1,52 +0,0 @@
|
||||
import { ServerUtilities, DatabaseConfig, ServerConfig } from './interface';
|
||||
|
||||
import {
|
||||
PostgraphileMiddleware,
|
||||
PostgraphileOptions
|
||||
} from '../adapters/postgraphile';
|
||||
|
||||
export const CONFIG_PATH_KEY = 'POSTGRAPHILE_CONFIG_PATH';
|
||||
export const SERVER_PORT_KEY = 'SERVER_PORT';
|
||||
|
||||
const DEFAULT_SERVER_PORT = '3000';
|
||||
|
||||
export function buildServerConfig(
|
||||
utilities: ServerUtilities,
|
||||
databaseConfig: DatabaseConfig,
|
||||
port?: string
|
||||
): ServerConfig {
|
||||
if (!port || port.length < 1) {
|
||||
port = DEFAULT_SERVER_PORT;
|
||||
}
|
||||
|
||||
const expressSessionHandler = utilities.expressSession();
|
||||
const passportInitializer = utilities.passport.initialize();
|
||||
const passportSessionHandler = utilities.passport.session();
|
||||
const pluginHook = utilities.pluginHook;
|
||||
const PgSimplifyInflectorPlugin = require('@graphile-contrib/pg-simplify-inflector');
|
||||
|
||||
const options: PostgraphileOptions = {
|
||||
appendPlugins: [PgSimplifyInflectorPlugin],
|
||||
disableDefaultMutations: databaseConfig.disableDefaultMutations,
|
||||
enableCors: true,
|
||||
exportGqlSchemaPath: 'schema.graphql',
|
||||
graphiql: true,
|
||||
ignoreRBAC: false,
|
||||
ownerConnectionString: databaseConfig.ownerConnectionString,
|
||||
pluginHook: pluginHook,
|
||||
watchPg: true,
|
||||
webSocketMiddlewares: [
|
||||
expressSessionHandler,
|
||||
passportInitializer,
|
||||
passportSessionHandler
|
||||
]
|
||||
};
|
||||
|
||||
const middleware: PostgraphileMiddleware = utilities.postgraphile(
|
||||
`${databaseConfig.host}/${databaseConfig.database}`,
|
||||
databaseConfig.schemas,
|
||||
options
|
||||
);
|
||||
|
||||
return { middleware, options, port: parseInt(port, 10) };
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import { CreateHttpServerCallback } from '../adapters/http';
|
||||
|
||||
import {
|
||||
ExpressInitCallback,
|
||||
ExpressSessionInitCallback,
|
||||
StaticPassportProvider
|
||||
} from '../adapters/session';
|
||||
|
||||
import {
|
||||
PostgraphileInitCallback,
|
||||
PostgraphileMiddleware,
|
||||
PostgraphileOptions
|
||||
} from '../adapters/postgraphile';
|
||||
import { PluginHookFn } from 'postgraphile/build/postgraphile/pluginHook';
|
||||
|
||||
export interface DatabaseConfig {
|
||||
host: string;
|
||||
database: string;
|
||||
schemas: string[];
|
||||
ownerConnectionString: string;
|
||||
disableDefaultMutations: boolean;
|
||||
}
|
||||
|
||||
export interface ServerConfig {
|
||||
middleware: PostgraphileMiddleware;
|
||||
options: PostgraphileOptions;
|
||||
port: number;
|
||||
}
|
||||
|
||||
export interface ServerUtilities {
|
||||
express: ExpressInitCallback;
|
||||
expressSession: ExpressSessionInitCallback;
|
||||
httpServerFactory: CreateHttpServerCallback;
|
||||
passport: StaticPassportProvider;
|
||||
postgraphile: PostgraphileInitCallback;
|
||||
pluginHook: PluginHookFn;
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { ServerUtilities, ServerConfig } from './interface';
|
||||
|
||||
export function bootServer(
|
||||
utilities: ServerUtilities,
|
||||
config: ServerConfig
|
||||
): void {
|
||||
const expressApp = utilities.express();
|
||||
expressApp.use(config.middleware);
|
||||
|
||||
const httpServer = utilities.httpServerFactory(expressApp);
|
||||
|
||||
httpServer.listen(config.port);
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Basic Options */
|
||||
"target": "es2017", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */
|
||||
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
|
||||
"lib": ["esnext"], /* Specify library files to be included in the compilation: */
|
||||
// "allowJs": true, /* Allow javascript files to be compiled. */
|
||||
// "checkJs": true, /* Report errors in .js files. */
|
||||
"jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
|
||||
// "declaration": true, /* Generates corresponding '.d.ts' file. */
|
||||
"sourceMap": true, /* Generates corresponding '.map' file. */
|
||||
// "outFile": "./", /* Concatenate and emit output to single file. */
|
||||
"outDir": "build/dist", /* Redirect output structure to the directory. */
|
||||
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
||||
"removeComments": true, /* Do not emit comments to output. */
|
||||
// "noEmit": true, /* Do not emit outputs. */
|
||||
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
|
||||
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
|
||||
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
||||
|
||||
/* Strict Type-Checking Options */
|
||||
"strict": true, /* Enable all strict type-checking options. */
|
||||
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
|
||||
// "strictNullChecks": true, /* Enable strict null checks. */
|
||||
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
|
||||
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
|
||||
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
|
||||
|
||||
/* Additional Checks */
|
||||
// "noUnusedLocals": true, /* Report errors on unused locals. */
|
||||
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||
|
||||
/* Module Resolution Options */
|
||||
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
||||
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
|
||||
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
||||
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
||||
// "typeRoots": [], /* List of folders to include type definitions from. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
|
||||
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
||||
|
||||
/* Source Map Options */
|
||||
// "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
|
||||
// "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
|
||||
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
|
||||
|
||||
/* Experimental Options */
|
||||
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Basic Options */
|
||||
"target": "es2017", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */
|
||||
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
|
||||
"lib": ["esnext"], /* Specify library files to be included in the compilation: */
|
||||
// "allowJs": true, /* Allow javascript files to be compiled. */
|
||||
// "checkJs": true, /* Report errors in .js files. */
|
||||
"jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
|
||||
// "declaration": true, /* Generates corresponding '.d.ts' file. */
|
||||
"sourceMap": true, /* Generates corresponding '.map' file. */
|
||||
// "outFile": "./", /* Concatenate and emit output to single file. */
|
||||
"outDir": "build/spec", /* Redirect output structure to the directory. */
|
||||
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
||||
"removeComments": true, /* Do not emit comments to output. */
|
||||
// "noEmit": true, /* Do not emit outputs. */
|
||||
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
|
||||
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
|
||||
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
||||
|
||||
/* Strict Type-Checking Options */
|
||||
"strict": true, /* Enable all strict type-checking options. */
|
||||
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
|
||||
// "strictNullChecks": true, /* Enable strict null checks. */
|
||||
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
|
||||
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
|
||||
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
|
||||
|
||||
/* Additional Checks */
|
||||
// "noUnusedLocals": true, /* Report errors on unused locals. */
|
||||
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||
|
||||
/* Module Resolution Options */
|
||||
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
||||
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
|
||||
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
||||
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
||||
// "typeRoots": [], /* List of folders to include type definitions from. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
|
||||
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
||||
|
||||
/* Source Map Options */
|
||||
// "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
|
||||
// "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
|
||||
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
|
||||
|
||||
/* Experimental Options */
|
||||
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
{
|
||||
"extends": [
|
||||
"tslint-eslint-rules"
|
||||
],
|
||||
"rules": {
|
||||
"object-curly-spacing": true,
|
||||
"ter-indent": [true, 2],
|
||||
"array-bracket-spacing": ["error", "never"],
|
||||
"class-name": true,
|
||||
"comment-format": [
|
||||
true,
|
||||
"check-space"
|
||||
],
|
||||
"curly": [true, "ignore-same-line"],
|
||||
"eofline": true,
|
||||
"forin": true,
|
||||
"indent": [
|
||||
true,
|
||||
"spaces"
|
||||
],
|
||||
"label-position": true,
|
||||
"max-line-length": [
|
||||
true,
|
||||
80
|
||||
],
|
||||
"member-access": [true, "no-public"],
|
||||
"member-ordering": [true, { "order": "statics-first" }],
|
||||
"no-arg": true,
|
||||
"no-bitwise": true,
|
||||
"no-console": [
|
||||
true,
|
||||
"debug",
|
||||
"info",
|
||||
"time",
|
||||
"timeEnd",
|
||||
"trace"
|
||||
],
|
||||
"no-consecutive-blank-lines": true,
|
||||
"no-construct": true,
|
||||
"no-debugger": true,
|
||||
"no-duplicate-variable": true,
|
||||
"no-empty": false,
|
||||
"no-eval": true,
|
||||
"no-inferrable-types": true,
|
||||
"no-shadowed-variable": false,
|
||||
"no-string-literal": false,
|
||||
"no-switch-case-fall-through": true,
|
||||
"no-trailing-whitespace": false,
|
||||
"no-unused-expression": false,
|
||||
"no-use-before-declare": true,
|
||||
"no-var-keyword": false,
|
||||
"object-literal-sort-keys": false,
|
||||
"one-line": [
|
||||
true,
|
||||
"check-open-brace",
|
||||
"check-whitespace"
|
||||
],
|
||||
"quotemark": [
|
||||
true,
|
||||
"single"
|
||||
],
|
||||
"radix": true,
|
||||
"semicolon": true,
|
||||
"triple-equals": [
|
||||
true,
|
||||
"allow-null-check"
|
||||
],
|
||||
"typedef-whitespace": [
|
||||
true,
|
||||
{
|
||||
"call-signature": "nospace",
|
||||
"index-signature": "nospace",
|
||||
"parameter": "nospace",
|
||||
"property-declaration": "nospace",
|
||||
"variable-declaration": "nospace"
|
||||
}
|
||||
],
|
||||
"variable-name": false,
|
||||
"whitespace": [
|
||||
true,
|
||||
"check-branch",
|
||||
"check-decl",
|
||||
"check-operator",
|
||||
"check-separator",
|
||||
"check-type"
|
||||
],
|
||||
|
||||
"use-host-property-decorator": false,
|
||||
"no-access-missing-member": false,
|
||||
"templates-use-public": false,
|
||||
"brace-style": false
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
module.exports = {
|
||||
entry: "./src/index.ts",
|
||||
mode: "production",
|
||||
devtool: "source-map",
|
||||
target: 'node',
|
||||
output: {
|
||||
filename: "vulcanize-postgraphile-server.js",
|
||||
path: __dirname + "/build/dist/",
|
||||
publicPath: "build/dist/"
|
||||
},
|
||||
resolve: {
|
||||
extensions: [".ts", ".tsx", ".mjs", ".js", ".json", ".css", ".png"]
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{ test: /\.ts$/, loader: "awesome-typescript-loader" },
|
||||
{ enforce: "pre", test: /\.js$/, loader: "source-map-loader" }
|
||||
]
|
||||
}
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,17 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Travis doesn't support postgres 10
|
||||
# https://github.com/travis-ci/travis-ci/issues/8537
|
||||
|
||||
set -ex
|
||||
|
||||
echo "Installing Postgres 10"
|
||||
sudo service postgresql stop
|
||||
sudo apt-get remove -q 'postgresql-*'
|
||||
sudo apt-get update -q
|
||||
sudo apt-get install -q postgresql-10 postgresql-client-10
|
||||
sudo cp /etc/postgresql/{9.6,10}/main/pg_hba.conf
|
||||
|
||||
echo "Restarting Postgres 10"
|
||||
sudo service postgresql restart
|
||||
|
||||
sudo psql -c 'CREATE ROLE travis SUPERUSER LOGIN CREATEDB;' -U postgres
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -ex
|
||||
|
||||
echo "Installing Postgres 11"
|
||||
sudo service postgresql stop
|
||||
sudo apt-get remove -q 'postgresql-*'
|
||||
sudo apt-get update -q
|
||||
sudo apt-get install -q postgresql-11 postgresql-client-11
|
||||
sudo cp /etc/postgresql/{9.6,11}/main/pg_hba.conf
|
||||
|
||||
echo "Restarting Postgres 11"
|
||||
sudo service postgresql restart
|
||||
|
||||
sudo psql -c 'CREATE ROLE travis SUPERUSER LOGIN CREATEDB;' -U postgres
|
||||
+2
-2
@@ -31,7 +31,7 @@ import (
|
||||
func LoadPostgres(database config.Database, node core.Node) postgres.DB {
|
||||
db, err := postgres.NewDB(database, node)
|
||||
if err != nil {
|
||||
log.Fatalf("Error loading postgres\n%v", err)
|
||||
log.Fatal("Error loading postgres: ", err)
|
||||
}
|
||||
return *db
|
||||
}
|
||||
@@ -65,7 +65,7 @@ func GetAbi(abiFilepath string, contractHash string, network string) string {
|
||||
}
|
||||
_, err := geth.ParseAbi(contractAbiString)
|
||||
if err != nil {
|
||||
log.Fatalln("Invalid ABI")
|
||||
log.Fatalln("Invalid ABI: ", err)
|
||||
}
|
||||
return contractAbiString
|
||||
}
|
||||
|
||||
Generated
+24
@@ -0,0 +1,24 @@
|
||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
_testmain.go
|
||||
|
||||
*.exe
|
||||
*.test
|
||||
*.prof
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
language: go
|
||||
go_import_path: github.com/pkg/errors
|
||||
go:
|
||||
- 1.4.x
|
||||
- 1.5.x
|
||||
- 1.6.x
|
||||
- 1.7.x
|
||||
- 1.8.x
|
||||
- 1.9.x
|
||||
- 1.10.x
|
||||
- 1.11.x
|
||||
- tip
|
||||
|
||||
script:
|
||||
- go test -v ./...
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
Copyright (c) 2015, Dave Cheney <dave@cheney.net>
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
# errors [](https://travis-ci.org/pkg/errors) [](https://ci.appveyor.com/project/davecheney/errors/branch/master) [](http://godoc.org/github.com/pkg/errors) [](https://goreportcard.com/report/github.com/pkg/errors) [](https://sourcegraph.com/github.com/pkg/errors?badge)
|
||||
|
||||
Package errors provides simple error handling primitives.
|
||||
|
||||
`go get github.com/pkg/errors`
|
||||
|
||||
The traditional error handling idiom in Go is roughly akin to
|
||||
```go
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
```
|
||||
which applied recursively up the call stack results in error reports without context or debugging information. The errors package allows programmers to add context to the failure path in their code in a way that does not destroy the original value of the error.
|
||||
|
||||
## Adding context to an error
|
||||
|
||||
The errors.Wrap function returns a new error that adds context to the original error. For example
|
||||
```go
|
||||
_, err := ioutil.ReadAll(r)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "read failed")
|
||||
}
|
||||
```
|
||||
## Retrieving the cause of an error
|
||||
|
||||
Using `errors.Wrap` constructs a stack of errors, adding context to the preceding error. Depending on the nature of the error it may be necessary to reverse the operation of errors.Wrap to retrieve the original error for inspection. Any error value which implements this interface can be inspected by `errors.Cause`.
|
||||
```go
|
||||
type causer interface {
|
||||
Cause() error
|
||||
}
|
||||
```
|
||||
`errors.Cause` will recursively retrieve the topmost error which does not implement `causer`, which is assumed to be the original cause. For example:
|
||||
```go
|
||||
switch err := errors.Cause(err).(type) {
|
||||
case *MyError:
|
||||
// handle specifically
|
||||
default:
|
||||
// unknown error
|
||||
}
|
||||
```
|
||||
|
||||
[Read the package documentation for more information](https://godoc.org/github.com/pkg/errors).
|
||||
|
||||
## Contributing
|
||||
|
||||
We welcome pull requests, bug fixes and issue reports. With that said, the bar for adding new symbols to this package is intentionally set high.
|
||||
|
||||
Before proposing a change, please discuss your change by raising an issue.
|
||||
|
||||
## License
|
||||
|
||||
BSD-2-Clause
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
version: build-{build}.{branch}
|
||||
|
||||
clone_folder: C:\gopath\src\github.com\pkg\errors
|
||||
shallow_clone: true # for startup speed
|
||||
|
||||
environment:
|
||||
GOPATH: C:\gopath
|
||||
|
||||
platform:
|
||||
- x64
|
||||
|
||||
# http://www.appveyor.com/docs/installed-software
|
||||
install:
|
||||
# some helpful output for debugging builds
|
||||
- go version
|
||||
- go env
|
||||
# pre-installed MinGW at C:\MinGW is 32bit only
|
||||
# but MSYS2 at C:\msys64 has mingw64
|
||||
- set PATH=C:\msys64\mingw64\bin;%PATH%
|
||||
- gcc --version
|
||||
- g++ --version
|
||||
|
||||
build_script:
|
||||
- go install -v ./...
|
||||
|
||||
test_script:
|
||||
- set PATH=C:\gopath\bin;%PATH%
|
||||
- go test -v ./...
|
||||
|
||||
#artifacts:
|
||||
# - path: '%GOPATH%\bin\*.exe'
|
||||
deploy: off
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
// +build go1.7
|
||||
|
||||
package errors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
stderrors "errors"
|
||||
)
|
||||
|
||||
func noErrors(at, depth int) error {
|
||||
if at >= depth {
|
||||
return stderrors.New("no error")
|
||||
}
|
||||
return noErrors(at+1, depth)
|
||||
}
|
||||
|
||||
func yesErrors(at, depth int) error {
|
||||
if at >= depth {
|
||||
return New("ye error")
|
||||
}
|
||||
return yesErrors(at+1, depth)
|
||||
}
|
||||
|
||||
// GlobalE is an exported global to store the result of benchmark results,
|
||||
// preventing the compiler from optimising the benchmark functions away.
|
||||
var GlobalE error
|
||||
|
||||
func BenchmarkErrors(b *testing.B) {
|
||||
type run struct {
|
||||
stack int
|
||||
std bool
|
||||
}
|
||||
runs := []run{
|
||||
{10, false},
|
||||
{10, true},
|
||||
{100, false},
|
||||
{100, true},
|
||||
{1000, false},
|
||||
{1000, true},
|
||||
}
|
||||
for _, r := range runs {
|
||||
part := "pkg/errors"
|
||||
if r.std {
|
||||
part = "errors"
|
||||
}
|
||||
name := fmt.Sprintf("%s-stack-%d", part, r.stack)
|
||||
b.Run(name, func(b *testing.B) {
|
||||
var err error
|
||||
f := yesErrors
|
||||
if r.std {
|
||||
f = noErrors
|
||||
}
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
err = f(0, r.stack)
|
||||
}
|
||||
b.StopTimer()
|
||||
GlobalE = err
|
||||
})
|
||||
}
|
||||
}
|
||||
+282
@@ -0,0 +1,282 @@
|
||||
// Package errors provides simple error handling primitives.
|
||||
//
|
||||
// The traditional error handling idiom in Go is roughly akin to
|
||||
//
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
//
|
||||
// which when applied recursively up the call stack results in error reports
|
||||
// without context or debugging information. The errors package allows
|
||||
// programmers to add context to the failure path in their code in a way
|
||||
// that does not destroy the original value of the error.
|
||||
//
|
||||
// Adding context to an error
|
||||
//
|
||||
// The errors.Wrap function returns a new error that adds context to the
|
||||
// original error by recording a stack trace at the point Wrap is called,
|
||||
// together with the supplied message. For example
|
||||
//
|
||||
// _, err := ioutil.ReadAll(r)
|
||||
// if err != nil {
|
||||
// return errors.Wrap(err, "read failed")
|
||||
// }
|
||||
//
|
||||
// If additional control is required, the errors.WithStack and
|
||||
// errors.WithMessage functions destructure errors.Wrap into its component
|
||||
// operations: annotating an error with a stack trace and with a message,
|
||||
// respectively.
|
||||
//
|
||||
// Retrieving the cause of an error
|
||||
//
|
||||
// Using errors.Wrap constructs a stack of errors, adding context to the
|
||||
// preceding error. Depending on the nature of the error it may be necessary
|
||||
// to reverse the operation of errors.Wrap to retrieve the original error
|
||||
// for inspection. Any error value which implements this interface
|
||||
//
|
||||
// type causer interface {
|
||||
// Cause() error
|
||||
// }
|
||||
//
|
||||
// can be inspected by errors.Cause. errors.Cause will recursively retrieve
|
||||
// the topmost error that does not implement causer, which is assumed to be
|
||||
// the original cause. For example:
|
||||
//
|
||||
// switch err := errors.Cause(err).(type) {
|
||||
// case *MyError:
|
||||
// // handle specifically
|
||||
// default:
|
||||
// // unknown error
|
||||
// }
|
||||
//
|
||||
// Although the causer interface is not exported by this package, it is
|
||||
// considered a part of its stable public interface.
|
||||
//
|
||||
// Formatted printing of errors
|
||||
//
|
||||
// All error values returned from this package implement fmt.Formatter and can
|
||||
// be formatted by the fmt package. The following verbs are supported:
|
||||
//
|
||||
// %s print the error. If the error has a Cause it will be
|
||||
// printed recursively.
|
||||
// %v see %s
|
||||
// %+v extended format. Each Frame of the error's StackTrace will
|
||||
// be printed in detail.
|
||||
//
|
||||
// Retrieving the stack trace of an error or wrapper
|
||||
//
|
||||
// New, Errorf, Wrap, and Wrapf record a stack trace at the point they are
|
||||
// invoked. This information can be retrieved with the following interface:
|
||||
//
|
||||
// type stackTracer interface {
|
||||
// StackTrace() errors.StackTrace
|
||||
// }
|
||||
//
|
||||
// The returned errors.StackTrace type is defined as
|
||||
//
|
||||
// type StackTrace []Frame
|
||||
//
|
||||
// The Frame type represents a call site in the stack trace. Frame supports
|
||||
// the fmt.Formatter interface that can be used for printing information about
|
||||
// the stack trace of this error. For example:
|
||||
//
|
||||
// if err, ok := err.(stackTracer); ok {
|
||||
// for _, f := range err.StackTrace() {
|
||||
// fmt.Printf("%+s:%d", f)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Although the stackTracer interface is not exported by this package, it is
|
||||
// considered a part of its stable public interface.
|
||||
//
|
||||
// See the documentation for Frame.Format for more details.
|
||||
package errors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
// New returns an error with the supplied message.
|
||||
// New also records the stack trace at the point it was called.
|
||||
func New(message string) error {
|
||||
return &fundamental{
|
||||
msg: message,
|
||||
stack: callers(),
|
||||
}
|
||||
}
|
||||
|
||||
// Errorf formats according to a format specifier and returns the string
|
||||
// as a value that satisfies error.
|
||||
// Errorf also records the stack trace at the point it was called.
|
||||
func Errorf(format string, args ...interface{}) error {
|
||||
return &fundamental{
|
||||
msg: fmt.Sprintf(format, args...),
|
||||
stack: callers(),
|
||||
}
|
||||
}
|
||||
|
||||
// fundamental is an error that has a message and a stack, but no caller.
|
||||
type fundamental struct {
|
||||
msg string
|
||||
*stack
|
||||
}
|
||||
|
||||
func (f *fundamental) Error() string { return f.msg }
|
||||
|
||||
func (f *fundamental) Format(s fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 'v':
|
||||
if s.Flag('+') {
|
||||
io.WriteString(s, f.msg)
|
||||
f.stack.Format(s, verb)
|
||||
return
|
||||
}
|
||||
fallthrough
|
||||
case 's':
|
||||
io.WriteString(s, f.msg)
|
||||
case 'q':
|
||||
fmt.Fprintf(s, "%q", f.msg)
|
||||
}
|
||||
}
|
||||
|
||||
// WithStack annotates err with a stack trace at the point WithStack was called.
|
||||
// If err is nil, WithStack returns nil.
|
||||
func WithStack(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return &withStack{
|
||||
err,
|
||||
callers(),
|
||||
}
|
||||
}
|
||||
|
||||
type withStack struct {
|
||||
error
|
||||
*stack
|
||||
}
|
||||
|
||||
func (w *withStack) Cause() error { return w.error }
|
||||
|
||||
func (w *withStack) Format(s fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 'v':
|
||||
if s.Flag('+') {
|
||||
fmt.Fprintf(s, "%+v", w.Cause())
|
||||
w.stack.Format(s, verb)
|
||||
return
|
||||
}
|
||||
fallthrough
|
||||
case 's':
|
||||
io.WriteString(s, w.Error())
|
||||
case 'q':
|
||||
fmt.Fprintf(s, "%q", w.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Wrap returns an error annotating err with a stack trace
|
||||
// at the point Wrap is called, and the supplied message.
|
||||
// If err is nil, Wrap returns nil.
|
||||
func Wrap(err error, message string) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
err = &withMessage{
|
||||
cause: err,
|
||||
msg: message,
|
||||
}
|
||||
return &withStack{
|
||||
err,
|
||||
callers(),
|
||||
}
|
||||
}
|
||||
|
||||
// Wrapf returns an error annotating err with a stack trace
|
||||
// at the point Wrapf is called, and the format specifier.
|
||||
// If err is nil, Wrapf returns nil.
|
||||
func Wrapf(err error, format string, args ...interface{}) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
err = &withMessage{
|
||||
cause: err,
|
||||
msg: fmt.Sprintf(format, args...),
|
||||
}
|
||||
return &withStack{
|
||||
err,
|
||||
callers(),
|
||||
}
|
||||
}
|
||||
|
||||
// WithMessage annotates err with a new message.
|
||||
// If err is nil, WithMessage returns nil.
|
||||
func WithMessage(err error, message string) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return &withMessage{
|
||||
cause: err,
|
||||
msg: message,
|
||||
}
|
||||
}
|
||||
|
||||
// WithMessagef annotates err with the format specifier.
|
||||
// If err is nil, WithMessagef returns nil.
|
||||
func WithMessagef(err error, format string, args ...interface{}) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return &withMessage{
|
||||
cause: err,
|
||||
msg: fmt.Sprintf(format, args...),
|
||||
}
|
||||
}
|
||||
|
||||
type withMessage struct {
|
||||
cause error
|
||||
msg string
|
||||
}
|
||||
|
||||
func (w *withMessage) Error() string { return w.msg + ": " + w.cause.Error() }
|
||||
func (w *withMessage) Cause() error { return w.cause }
|
||||
|
||||
func (w *withMessage) Format(s fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 'v':
|
||||
if s.Flag('+') {
|
||||
fmt.Fprintf(s, "%+v\n", w.Cause())
|
||||
io.WriteString(s, w.msg)
|
||||
return
|
||||
}
|
||||
fallthrough
|
||||
case 's', 'q':
|
||||
io.WriteString(s, w.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Cause returns the underlying cause of the error, if possible.
|
||||
// An error value has a cause if it implements the following
|
||||
// interface:
|
||||
//
|
||||
// type causer interface {
|
||||
// Cause() error
|
||||
// }
|
||||
//
|
||||
// If the error does not implement Cause, the original error will
|
||||
// be returned. If the error is nil, nil will be returned without further
|
||||
// investigation.
|
||||
func Cause(err error) error {
|
||||
type causer interface {
|
||||
Cause() error
|
||||
}
|
||||
|
||||
for err != nil {
|
||||
cause, ok := err.(causer)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
err = cause.Cause()
|
||||
}
|
||||
return err
|
||||
}
|
||||
+251
@@ -0,0 +1,251 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
tests := []struct {
|
||||
err string
|
||||
want error
|
||||
}{
|
||||
{"", fmt.Errorf("")},
|
||||
{"foo", fmt.Errorf("foo")},
|
||||
{"foo", New("foo")},
|
||||
{"string with format specifiers: %v", errors.New("string with format specifiers: %v")},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := New(tt.err)
|
||||
if got.Error() != tt.want.Error() {
|
||||
t.Errorf("New.Error(): got: %q, want %q", got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrapNil(t *testing.T) {
|
||||
got := Wrap(nil, "no error")
|
||||
if got != nil {
|
||||
t.Errorf("Wrap(nil, \"no error\"): got %#v, expected nil", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrap(t *testing.T) {
|
||||
tests := []struct {
|
||||
err error
|
||||
message string
|
||||
want string
|
||||
}{
|
||||
{io.EOF, "read error", "read error: EOF"},
|
||||
{Wrap(io.EOF, "read error"), "client error", "client error: read error: EOF"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := Wrap(tt.err, tt.message).Error()
|
||||
if got != tt.want {
|
||||
t.Errorf("Wrap(%v, %q): got: %v, want %v", tt.err, tt.message, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type nilError struct{}
|
||||
|
||||
func (nilError) Error() string { return "nil error" }
|
||||
|
||||
func TestCause(t *testing.T) {
|
||||
x := New("error")
|
||||
tests := []struct {
|
||||
err error
|
||||
want error
|
||||
}{{
|
||||
// nil error is nil
|
||||
err: nil,
|
||||
want: nil,
|
||||
}, {
|
||||
// explicit nil error is nil
|
||||
err: (error)(nil),
|
||||
want: nil,
|
||||
}, {
|
||||
// typed nil is nil
|
||||
err: (*nilError)(nil),
|
||||
want: (*nilError)(nil),
|
||||
}, {
|
||||
// uncaused error is unaffected
|
||||
err: io.EOF,
|
||||
want: io.EOF,
|
||||
}, {
|
||||
// caused error returns cause
|
||||
err: Wrap(io.EOF, "ignored"),
|
||||
want: io.EOF,
|
||||
}, {
|
||||
err: x, // return from errors.New
|
||||
want: x,
|
||||
}, {
|
||||
WithMessage(nil, "whoops"),
|
||||
nil,
|
||||
}, {
|
||||
WithMessage(io.EOF, "whoops"),
|
||||
io.EOF,
|
||||
}, {
|
||||
WithStack(nil),
|
||||
nil,
|
||||
}, {
|
||||
WithStack(io.EOF),
|
||||
io.EOF,
|
||||
}}
|
||||
|
||||
for i, tt := range tests {
|
||||
got := Cause(tt.err)
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("test %d: got %#v, want %#v", i+1, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrapfNil(t *testing.T) {
|
||||
got := Wrapf(nil, "no error")
|
||||
if got != nil {
|
||||
t.Errorf("Wrapf(nil, \"no error\"): got %#v, expected nil", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrapf(t *testing.T) {
|
||||
tests := []struct {
|
||||
err error
|
||||
message string
|
||||
want string
|
||||
}{
|
||||
{io.EOF, "read error", "read error: EOF"},
|
||||
{Wrapf(io.EOF, "read error without format specifiers"), "client error", "client error: read error without format specifiers: EOF"},
|
||||
{Wrapf(io.EOF, "read error with %d format specifier", 1), "client error", "client error: read error with 1 format specifier: EOF"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := Wrapf(tt.err, tt.message).Error()
|
||||
if got != tt.want {
|
||||
t.Errorf("Wrapf(%v, %q): got: %v, want %v", tt.err, tt.message, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorf(t *testing.T) {
|
||||
tests := []struct {
|
||||
err error
|
||||
want string
|
||||
}{
|
||||
{Errorf("read error without format specifiers"), "read error without format specifiers"},
|
||||
{Errorf("read error with %d format specifier", 1), "read error with 1 format specifier"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := tt.err.Error()
|
||||
if got != tt.want {
|
||||
t.Errorf("Errorf(%v): got: %q, want %q", tt.err, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithStackNil(t *testing.T) {
|
||||
got := WithStack(nil)
|
||||
if got != nil {
|
||||
t.Errorf("WithStack(nil): got %#v, expected nil", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithStack(t *testing.T) {
|
||||
tests := []struct {
|
||||
err error
|
||||
want string
|
||||
}{
|
||||
{io.EOF, "EOF"},
|
||||
{WithStack(io.EOF), "EOF"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := WithStack(tt.err).Error()
|
||||
if got != tt.want {
|
||||
t.Errorf("WithStack(%v): got: %v, want %v", tt.err, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithMessageNil(t *testing.T) {
|
||||
got := WithMessage(nil, "no error")
|
||||
if got != nil {
|
||||
t.Errorf("WithMessage(nil, \"no error\"): got %#v, expected nil", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithMessage(t *testing.T) {
|
||||
tests := []struct {
|
||||
err error
|
||||
message string
|
||||
want string
|
||||
}{
|
||||
{io.EOF, "read error", "read error: EOF"},
|
||||
{WithMessage(io.EOF, "read error"), "client error", "client error: read error: EOF"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := WithMessage(tt.err, tt.message).Error()
|
||||
if got != tt.want {
|
||||
t.Errorf("WithMessage(%v, %q): got: %q, want %q", tt.err, tt.message, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithMessagefNil(t *testing.T) {
|
||||
got := WithMessagef(nil, "no error")
|
||||
if got != nil {
|
||||
t.Errorf("WithMessage(nil, \"no error\"): got %#v, expected nil", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithMessagef(t *testing.T) {
|
||||
tests := []struct {
|
||||
err error
|
||||
message string
|
||||
want string
|
||||
}{
|
||||
{io.EOF, "read error", "read error: EOF"},
|
||||
{WithMessagef(io.EOF, "read error without format specifier"), "client error", "client error: read error without format specifier: EOF"},
|
||||
{WithMessagef(io.EOF, "read error with %d format specifier", 1), "client error", "client error: read error with 1 format specifier: EOF"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := WithMessagef(tt.err, tt.message).Error()
|
||||
if got != tt.want {
|
||||
t.Errorf("WithMessage(%v, %q): got: %q, want %q", tt.err, tt.message, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// errors.New, etc values are not expected to be compared by value
|
||||
// but the change in errors#27 made them incomparable. Assert that
|
||||
// various kinds of errors have a functional equality operator, even
|
||||
// if the result of that equality is always false.
|
||||
func TestErrorEquality(t *testing.T) {
|
||||
vals := []error{
|
||||
nil,
|
||||
io.EOF,
|
||||
errors.New("EOF"),
|
||||
New("EOF"),
|
||||
Errorf("EOF"),
|
||||
Wrap(io.EOF, "EOF"),
|
||||
Wrapf(io.EOF, "EOF%d", 2),
|
||||
WithMessage(nil, "whoops"),
|
||||
WithMessage(io.EOF, "whoops"),
|
||||
WithStack(io.EOF),
|
||||
WithStack(nil),
|
||||
}
|
||||
|
||||
for i := range vals {
|
||||
for j := range vals {
|
||||
_ = vals[i] == vals[j] // mustn't panic
|
||||
}
|
||||
}
|
||||
}
|
||||
+205
@@ -0,0 +1,205 @@
|
||||
package errors_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func ExampleNew() {
|
||||
err := errors.New("whoops")
|
||||
fmt.Println(err)
|
||||
|
||||
// Output: whoops
|
||||
}
|
||||
|
||||
func ExampleNew_printf() {
|
||||
err := errors.New("whoops")
|
||||
fmt.Printf("%+v", err)
|
||||
|
||||
// Example output:
|
||||
// whoops
|
||||
// github.com/pkg/errors_test.ExampleNew_printf
|
||||
// /home/dfc/src/github.com/pkg/errors/example_test.go:17
|
||||
// testing.runExample
|
||||
// /home/dfc/go/src/testing/example.go:114
|
||||
// testing.RunExamples
|
||||
// /home/dfc/go/src/testing/example.go:38
|
||||
// testing.(*M).Run
|
||||
// /home/dfc/go/src/testing/testing.go:744
|
||||
// main.main
|
||||
// /github.com/pkg/errors/_test/_testmain.go:106
|
||||
// runtime.main
|
||||
// /home/dfc/go/src/runtime/proc.go:183
|
||||
// runtime.goexit
|
||||
// /home/dfc/go/src/runtime/asm_amd64.s:2059
|
||||
}
|
||||
|
||||
func ExampleWithMessage() {
|
||||
cause := errors.New("whoops")
|
||||
err := errors.WithMessage(cause, "oh noes")
|
||||
fmt.Println(err)
|
||||
|
||||
// Output: oh noes: whoops
|
||||
}
|
||||
|
||||
func ExampleWithStack() {
|
||||
cause := errors.New("whoops")
|
||||
err := errors.WithStack(cause)
|
||||
fmt.Println(err)
|
||||
|
||||
// Output: whoops
|
||||
}
|
||||
|
||||
func ExampleWithStack_printf() {
|
||||
cause := errors.New("whoops")
|
||||
err := errors.WithStack(cause)
|
||||
fmt.Printf("%+v", err)
|
||||
|
||||
// Example Output:
|
||||
// whoops
|
||||
// github.com/pkg/errors_test.ExampleWithStack_printf
|
||||
// /home/fabstu/go/src/github.com/pkg/errors/example_test.go:55
|
||||
// testing.runExample
|
||||
// /usr/lib/go/src/testing/example.go:114
|
||||
// testing.RunExamples
|
||||
// /usr/lib/go/src/testing/example.go:38
|
||||
// testing.(*M).Run
|
||||
// /usr/lib/go/src/testing/testing.go:744
|
||||
// main.main
|
||||
// github.com/pkg/errors/_test/_testmain.go:106
|
||||
// runtime.main
|
||||
// /usr/lib/go/src/runtime/proc.go:183
|
||||
// runtime.goexit
|
||||
// /usr/lib/go/src/runtime/asm_amd64.s:2086
|
||||
// github.com/pkg/errors_test.ExampleWithStack_printf
|
||||
// /home/fabstu/go/src/github.com/pkg/errors/example_test.go:56
|
||||
// testing.runExample
|
||||
// /usr/lib/go/src/testing/example.go:114
|
||||
// testing.RunExamples
|
||||
// /usr/lib/go/src/testing/example.go:38
|
||||
// testing.(*M).Run
|
||||
// /usr/lib/go/src/testing/testing.go:744
|
||||
// main.main
|
||||
// github.com/pkg/errors/_test/_testmain.go:106
|
||||
// runtime.main
|
||||
// /usr/lib/go/src/runtime/proc.go:183
|
||||
// runtime.goexit
|
||||
// /usr/lib/go/src/runtime/asm_amd64.s:2086
|
||||
}
|
||||
|
||||
func ExampleWrap() {
|
||||
cause := errors.New("whoops")
|
||||
err := errors.Wrap(cause, "oh noes")
|
||||
fmt.Println(err)
|
||||
|
||||
// Output: oh noes: whoops
|
||||
}
|
||||
|
||||
func fn() error {
|
||||
e1 := errors.New("error")
|
||||
e2 := errors.Wrap(e1, "inner")
|
||||
e3 := errors.Wrap(e2, "middle")
|
||||
return errors.Wrap(e3, "outer")
|
||||
}
|
||||
|
||||
func ExampleCause() {
|
||||
err := fn()
|
||||
fmt.Println(err)
|
||||
fmt.Println(errors.Cause(err))
|
||||
|
||||
// Output: outer: middle: inner: error
|
||||
// error
|
||||
}
|
||||
|
||||
func ExampleWrap_extended() {
|
||||
err := fn()
|
||||
fmt.Printf("%+v\n", err)
|
||||
|
||||
// Example output:
|
||||
// error
|
||||
// github.com/pkg/errors_test.fn
|
||||
// /home/dfc/src/github.com/pkg/errors/example_test.go:47
|
||||
// github.com/pkg/errors_test.ExampleCause_printf
|
||||
// /home/dfc/src/github.com/pkg/errors/example_test.go:63
|
||||
// testing.runExample
|
||||
// /home/dfc/go/src/testing/example.go:114
|
||||
// testing.RunExamples
|
||||
// /home/dfc/go/src/testing/example.go:38
|
||||
// testing.(*M).Run
|
||||
// /home/dfc/go/src/testing/testing.go:744
|
||||
// main.main
|
||||
// /github.com/pkg/errors/_test/_testmain.go:104
|
||||
// runtime.main
|
||||
// /home/dfc/go/src/runtime/proc.go:183
|
||||
// runtime.goexit
|
||||
// /home/dfc/go/src/runtime/asm_amd64.s:2059
|
||||
// github.com/pkg/errors_test.fn
|
||||
// /home/dfc/src/github.com/pkg/errors/example_test.go:48: inner
|
||||
// github.com/pkg/errors_test.fn
|
||||
// /home/dfc/src/github.com/pkg/errors/example_test.go:49: middle
|
||||
// github.com/pkg/errors_test.fn
|
||||
// /home/dfc/src/github.com/pkg/errors/example_test.go:50: outer
|
||||
}
|
||||
|
||||
func ExampleWrapf() {
|
||||
cause := errors.New("whoops")
|
||||
err := errors.Wrapf(cause, "oh noes #%d", 2)
|
||||
fmt.Println(err)
|
||||
|
||||
// Output: oh noes #2: whoops
|
||||
}
|
||||
|
||||
func ExampleErrorf_extended() {
|
||||
err := errors.Errorf("whoops: %s", "foo")
|
||||
fmt.Printf("%+v", err)
|
||||
|
||||
// Example output:
|
||||
// whoops: foo
|
||||
// github.com/pkg/errors_test.ExampleErrorf
|
||||
// /home/dfc/src/github.com/pkg/errors/example_test.go:101
|
||||
// testing.runExample
|
||||
// /home/dfc/go/src/testing/example.go:114
|
||||
// testing.RunExamples
|
||||
// /home/dfc/go/src/testing/example.go:38
|
||||
// testing.(*M).Run
|
||||
// /home/dfc/go/src/testing/testing.go:744
|
||||
// main.main
|
||||
// /github.com/pkg/errors/_test/_testmain.go:102
|
||||
// runtime.main
|
||||
// /home/dfc/go/src/runtime/proc.go:183
|
||||
// runtime.goexit
|
||||
// /home/dfc/go/src/runtime/asm_amd64.s:2059
|
||||
}
|
||||
|
||||
func Example_stackTrace() {
|
||||
type stackTracer interface {
|
||||
StackTrace() errors.StackTrace
|
||||
}
|
||||
|
||||
err, ok := errors.Cause(fn()).(stackTracer)
|
||||
if !ok {
|
||||
panic("oops, err does not implement stackTracer")
|
||||
}
|
||||
|
||||
st := err.StackTrace()
|
||||
fmt.Printf("%+v", st[0:2]) // top two frames
|
||||
|
||||
// Example output:
|
||||
// github.com/pkg/errors_test.fn
|
||||
// /home/dfc/src/github.com/pkg/errors/example_test.go:47
|
||||
// github.com/pkg/errors_test.Example_stackTrace
|
||||
// /home/dfc/src/github.com/pkg/errors/example_test.go:127
|
||||
}
|
||||
|
||||
func ExampleCause_printf() {
|
||||
err := errors.Wrap(func() error {
|
||||
return func() error {
|
||||
return errors.Errorf("hello %s", fmt.Sprintf("world"))
|
||||
}()
|
||||
}(), "failed")
|
||||
|
||||
fmt.Printf("%v", err)
|
||||
|
||||
// Output: failed: hello world
|
||||
}
|
||||
+535
@@ -0,0 +1,535 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFormatNew(t *testing.T) {
|
||||
tests := []struct {
|
||||
error
|
||||
format string
|
||||
want string
|
||||
}{{
|
||||
New("error"),
|
||||
"%s",
|
||||
"error",
|
||||
}, {
|
||||
New("error"),
|
||||
"%v",
|
||||
"error",
|
||||
}, {
|
||||
New("error"),
|
||||
"%+v",
|
||||
"error\n" +
|
||||
"github.com/pkg/errors.TestFormatNew\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:26",
|
||||
}, {
|
||||
New("error"),
|
||||
"%q",
|
||||
`"error"`,
|
||||
}}
|
||||
|
||||
for i, tt := range tests {
|
||||
testFormatRegexp(t, i, tt.error, tt.format, tt.want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatErrorf(t *testing.T) {
|
||||
tests := []struct {
|
||||
error
|
||||
format string
|
||||
want string
|
||||
}{{
|
||||
Errorf("%s", "error"),
|
||||
"%s",
|
||||
"error",
|
||||
}, {
|
||||
Errorf("%s", "error"),
|
||||
"%v",
|
||||
"error",
|
||||
}, {
|
||||
Errorf("%s", "error"),
|
||||
"%+v",
|
||||
"error\n" +
|
||||
"github.com/pkg/errors.TestFormatErrorf\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:56",
|
||||
}}
|
||||
|
||||
for i, tt := range tests {
|
||||
testFormatRegexp(t, i, tt.error, tt.format, tt.want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatWrap(t *testing.T) {
|
||||
tests := []struct {
|
||||
error
|
||||
format string
|
||||
want string
|
||||
}{{
|
||||
Wrap(New("error"), "error2"),
|
||||
"%s",
|
||||
"error2: error",
|
||||
}, {
|
||||
Wrap(New("error"), "error2"),
|
||||
"%v",
|
||||
"error2: error",
|
||||
}, {
|
||||
Wrap(New("error"), "error2"),
|
||||
"%+v",
|
||||
"error\n" +
|
||||
"github.com/pkg/errors.TestFormatWrap\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:82",
|
||||
}, {
|
||||
Wrap(io.EOF, "error"),
|
||||
"%s",
|
||||
"error: EOF",
|
||||
}, {
|
||||
Wrap(io.EOF, "error"),
|
||||
"%v",
|
||||
"error: EOF",
|
||||
}, {
|
||||
Wrap(io.EOF, "error"),
|
||||
"%+v",
|
||||
"EOF\n" +
|
||||
"error\n" +
|
||||
"github.com/pkg/errors.TestFormatWrap\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:96",
|
||||
}, {
|
||||
Wrap(Wrap(io.EOF, "error1"), "error2"),
|
||||
"%+v",
|
||||
"EOF\n" +
|
||||
"error1\n" +
|
||||
"github.com/pkg/errors.TestFormatWrap\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:103\n",
|
||||
}, {
|
||||
Wrap(New("error with space"), "context"),
|
||||
"%q",
|
||||
`"context: error with space"`,
|
||||
}}
|
||||
|
||||
for i, tt := range tests {
|
||||
testFormatRegexp(t, i, tt.error, tt.format, tt.want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatWrapf(t *testing.T) {
|
||||
tests := []struct {
|
||||
error
|
||||
format string
|
||||
want string
|
||||
}{{
|
||||
Wrapf(io.EOF, "error%d", 2),
|
||||
"%s",
|
||||
"error2: EOF",
|
||||
}, {
|
||||
Wrapf(io.EOF, "error%d", 2),
|
||||
"%v",
|
||||
"error2: EOF",
|
||||
}, {
|
||||
Wrapf(io.EOF, "error%d", 2),
|
||||
"%+v",
|
||||
"EOF\n" +
|
||||
"error2\n" +
|
||||
"github.com/pkg/errors.TestFormatWrapf\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:134",
|
||||
}, {
|
||||
Wrapf(New("error"), "error%d", 2),
|
||||
"%s",
|
||||
"error2: error",
|
||||
}, {
|
||||
Wrapf(New("error"), "error%d", 2),
|
||||
"%v",
|
||||
"error2: error",
|
||||
}, {
|
||||
Wrapf(New("error"), "error%d", 2),
|
||||
"%+v",
|
||||
"error\n" +
|
||||
"github.com/pkg/errors.TestFormatWrapf\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:149",
|
||||
}}
|
||||
|
||||
for i, tt := range tests {
|
||||
testFormatRegexp(t, i, tt.error, tt.format, tt.want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatWithStack(t *testing.T) {
|
||||
tests := []struct {
|
||||
error
|
||||
format string
|
||||
want []string
|
||||
}{{
|
||||
WithStack(io.EOF),
|
||||
"%s",
|
||||
[]string{"EOF"},
|
||||
}, {
|
||||
WithStack(io.EOF),
|
||||
"%v",
|
||||
[]string{"EOF"},
|
||||
}, {
|
||||
WithStack(io.EOF),
|
||||
"%+v",
|
||||
[]string{"EOF",
|
||||
"github.com/pkg/errors.TestFormatWithStack\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:175"},
|
||||
}, {
|
||||
WithStack(New("error")),
|
||||
"%s",
|
||||
[]string{"error"},
|
||||
}, {
|
||||
WithStack(New("error")),
|
||||
"%v",
|
||||
[]string{"error"},
|
||||
}, {
|
||||
WithStack(New("error")),
|
||||
"%+v",
|
||||
[]string{"error",
|
||||
"github.com/pkg/errors.TestFormatWithStack\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:189",
|
||||
"github.com/pkg/errors.TestFormatWithStack\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:189"},
|
||||
}, {
|
||||
WithStack(WithStack(io.EOF)),
|
||||
"%+v",
|
||||
[]string{"EOF",
|
||||
"github.com/pkg/errors.TestFormatWithStack\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:197",
|
||||
"github.com/pkg/errors.TestFormatWithStack\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:197"},
|
||||
}, {
|
||||
WithStack(WithStack(Wrapf(io.EOF, "message"))),
|
||||
"%+v",
|
||||
[]string{"EOF",
|
||||
"message",
|
||||
"github.com/pkg/errors.TestFormatWithStack\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:205",
|
||||
"github.com/pkg/errors.TestFormatWithStack\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:205",
|
||||
"github.com/pkg/errors.TestFormatWithStack\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:205"},
|
||||
}, {
|
||||
WithStack(Errorf("error%d", 1)),
|
||||
"%+v",
|
||||
[]string{"error1",
|
||||
"github.com/pkg/errors.TestFormatWithStack\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:216",
|
||||
"github.com/pkg/errors.TestFormatWithStack\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:216"},
|
||||
}}
|
||||
|
||||
for i, tt := range tests {
|
||||
testFormatCompleteCompare(t, i, tt.error, tt.format, tt.want, true)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatWithMessage(t *testing.T) {
|
||||
tests := []struct {
|
||||
error
|
||||
format string
|
||||
want []string
|
||||
}{{
|
||||
WithMessage(New("error"), "error2"),
|
||||
"%s",
|
||||
[]string{"error2: error"},
|
||||
}, {
|
||||
WithMessage(New("error"), "error2"),
|
||||
"%v",
|
||||
[]string{"error2: error"},
|
||||
}, {
|
||||
WithMessage(New("error"), "error2"),
|
||||
"%+v",
|
||||
[]string{
|
||||
"error",
|
||||
"github.com/pkg/errors.TestFormatWithMessage\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:244",
|
||||
"error2"},
|
||||
}, {
|
||||
WithMessage(io.EOF, "addition1"),
|
||||
"%s",
|
||||
[]string{"addition1: EOF"},
|
||||
}, {
|
||||
WithMessage(io.EOF, "addition1"),
|
||||
"%v",
|
||||
[]string{"addition1: EOF"},
|
||||
}, {
|
||||
WithMessage(io.EOF, "addition1"),
|
||||
"%+v",
|
||||
[]string{"EOF", "addition1"},
|
||||
}, {
|
||||
WithMessage(WithMessage(io.EOF, "addition1"), "addition2"),
|
||||
"%v",
|
||||
[]string{"addition2: addition1: EOF"},
|
||||
}, {
|
||||
WithMessage(WithMessage(io.EOF, "addition1"), "addition2"),
|
||||
"%+v",
|
||||
[]string{"EOF", "addition1", "addition2"},
|
||||
}, {
|
||||
Wrap(WithMessage(io.EOF, "error1"), "error2"),
|
||||
"%+v",
|
||||
[]string{"EOF", "error1", "error2",
|
||||
"github.com/pkg/errors.TestFormatWithMessage\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:272"},
|
||||
}, {
|
||||
WithMessage(Errorf("error%d", 1), "error2"),
|
||||
"%+v",
|
||||
[]string{"error1",
|
||||
"github.com/pkg/errors.TestFormatWithMessage\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:278",
|
||||
"error2"},
|
||||
}, {
|
||||
WithMessage(WithStack(io.EOF), "error"),
|
||||
"%+v",
|
||||
[]string{
|
||||
"EOF",
|
||||
"github.com/pkg/errors.TestFormatWithMessage\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:285",
|
||||
"error"},
|
||||
}, {
|
||||
WithMessage(Wrap(WithStack(io.EOF), "inside-error"), "outside-error"),
|
||||
"%+v",
|
||||
[]string{
|
||||
"EOF",
|
||||
"github.com/pkg/errors.TestFormatWithMessage\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:293",
|
||||
"inside-error",
|
||||
"github.com/pkg/errors.TestFormatWithMessage\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:293",
|
||||
"outside-error"},
|
||||
}}
|
||||
|
||||
for i, tt := range tests {
|
||||
testFormatCompleteCompare(t, i, tt.error, tt.format, tt.want, true)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatGeneric(t *testing.T) {
|
||||
starts := []struct {
|
||||
err error
|
||||
want []string
|
||||
}{
|
||||
{New("new-error"), []string{
|
||||
"new-error",
|
||||
"github.com/pkg/errors.TestFormatGeneric\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:315"},
|
||||
}, {Errorf("errorf-error"), []string{
|
||||
"errorf-error",
|
||||
"github.com/pkg/errors.TestFormatGeneric\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:319"},
|
||||
}, {errors.New("errors-new-error"), []string{
|
||||
"errors-new-error"},
|
||||
},
|
||||
}
|
||||
|
||||
wrappers := []wrapper{
|
||||
{
|
||||
func(err error) error { return WithMessage(err, "with-message") },
|
||||
[]string{"with-message"},
|
||||
}, {
|
||||
func(err error) error { return WithStack(err) },
|
||||
[]string{
|
||||
"github.com/pkg/errors.(func·002|TestFormatGeneric.func2)\n\t" +
|
||||
".+/github.com/pkg/errors/format_test.go:333",
|
||||
},
|
||||
}, {
|
||||
func(err error) error { return Wrap(err, "wrap-error") },
|
||||
[]string{
|
||||
"wrap-error",
|
||||
"github.com/pkg/errors.(func·003|TestFormatGeneric.func3)\n\t" +
|
||||
".+/github.com/pkg/errors/format_test.go:339",
|
||||
},
|
||||
}, {
|
||||
func(err error) error { return Wrapf(err, "wrapf-error%d", 1) },
|
||||
[]string{
|
||||
"wrapf-error1",
|
||||
"github.com/pkg/errors.(func·004|TestFormatGeneric.func4)\n\t" +
|
||||
".+/github.com/pkg/errors/format_test.go:346",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for s := range starts {
|
||||
err := starts[s].err
|
||||
want := starts[s].want
|
||||
testFormatCompleteCompare(t, s, err, "%+v", want, false)
|
||||
testGenericRecursive(t, err, want, wrappers, 3)
|
||||
}
|
||||
}
|
||||
|
||||
func testFormatRegexp(t *testing.T, n int, arg interface{}, format, want string) {
|
||||
got := fmt.Sprintf(format, arg)
|
||||
gotLines := strings.SplitN(got, "\n", -1)
|
||||
wantLines := strings.SplitN(want, "\n", -1)
|
||||
|
||||
if len(wantLines) > len(gotLines) {
|
||||
t.Errorf("test %d: wantLines(%d) > gotLines(%d):\n got: %q\nwant: %q", n+1, len(wantLines), len(gotLines), got, want)
|
||||
return
|
||||
}
|
||||
|
||||
for i, w := range wantLines {
|
||||
match, err := regexp.MatchString(w, gotLines[i])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !match {
|
||||
t.Errorf("test %d: line %d: fmt.Sprintf(%q, err):\n got: %q\nwant: %q", n+1, i+1, format, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var stackLineR = regexp.MustCompile(`\.`)
|
||||
|
||||
// parseBlocks parses input into a slice, where:
|
||||
// - incase entry contains a newline, its a stacktrace
|
||||
// - incase entry contains no newline, its a solo line.
|
||||
//
|
||||
// Detecting stack boundaries only works incase the WithStack-calls are
|
||||
// to be found on the same line, thats why it is optionally here.
|
||||
//
|
||||
// Example use:
|
||||
//
|
||||
// for _, e := range blocks {
|
||||
// if strings.ContainsAny(e, "\n") {
|
||||
// // Match as stack
|
||||
// } else {
|
||||
// // Match as line
|
||||
// }
|
||||
// }
|
||||
//
|
||||
func parseBlocks(input string, detectStackboundaries bool) ([]string, error) {
|
||||
var blocks []string
|
||||
|
||||
stack := ""
|
||||
wasStack := false
|
||||
lines := map[string]bool{} // already found lines
|
||||
|
||||
for _, l := range strings.Split(input, "\n") {
|
||||
isStackLine := stackLineR.MatchString(l)
|
||||
|
||||
switch {
|
||||
case !isStackLine && wasStack:
|
||||
blocks = append(blocks, stack, l)
|
||||
stack = ""
|
||||
lines = map[string]bool{}
|
||||
case isStackLine:
|
||||
if wasStack {
|
||||
// Detecting two stacks after another, possible cause lines match in
|
||||
// our tests due to WithStack(WithStack(io.EOF)) on same line.
|
||||
if detectStackboundaries {
|
||||
if lines[l] {
|
||||
if len(stack) == 0 {
|
||||
return nil, errors.New("len of block must not be zero here")
|
||||
}
|
||||
|
||||
blocks = append(blocks, stack)
|
||||
stack = l
|
||||
lines = map[string]bool{l: true}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
stack = stack + "\n" + l
|
||||
} else {
|
||||
stack = l
|
||||
}
|
||||
lines[l] = true
|
||||
case !isStackLine && !wasStack:
|
||||
blocks = append(blocks, l)
|
||||
default:
|
||||
return nil, errors.New("must not happen")
|
||||
}
|
||||
|
||||
wasStack = isStackLine
|
||||
}
|
||||
|
||||
// Use up stack
|
||||
if stack != "" {
|
||||
blocks = append(blocks, stack)
|
||||
}
|
||||
return blocks, nil
|
||||
}
|
||||
|
||||
func testFormatCompleteCompare(t *testing.T, n int, arg interface{}, format string, want []string, detectStackBoundaries bool) {
|
||||
gotStr := fmt.Sprintf(format, arg)
|
||||
|
||||
got, err := parseBlocks(gotStr, detectStackBoundaries)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("test %d: fmt.Sprintf(%s, err) -> wrong number of blocks: got(%d) want(%d)\n got: %s\nwant: %s\ngotStr: %q",
|
||||
n+1, format, len(got), len(want), prettyBlocks(got), prettyBlocks(want), gotStr)
|
||||
}
|
||||
|
||||
for i := range got {
|
||||
if strings.ContainsAny(want[i], "\n") {
|
||||
// Match as stack
|
||||
match, err := regexp.MatchString(want[i], got[i])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !match {
|
||||
t.Fatalf("test %d: block %d: fmt.Sprintf(%q, err):\ngot:\n%q\nwant:\n%q\nall-got:\n%s\nall-want:\n%s\n",
|
||||
n+1, i+1, format, got[i], want[i], prettyBlocks(got), prettyBlocks(want))
|
||||
}
|
||||
} else {
|
||||
// Match as message
|
||||
if got[i] != want[i] {
|
||||
t.Fatalf("test %d: fmt.Sprintf(%s, err) at block %d got != want:\n got: %q\nwant: %q", n+1, format, i+1, got[i], want[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type wrapper struct {
|
||||
wrap func(err error) error
|
||||
want []string
|
||||
}
|
||||
|
||||
func prettyBlocks(blocks []string) string {
|
||||
var out []string
|
||||
|
||||
for _, b := range blocks {
|
||||
out = append(out, fmt.Sprintf("%v", b))
|
||||
}
|
||||
|
||||
return " " + strings.Join(out, "\n ")
|
||||
}
|
||||
|
||||
func testGenericRecursive(t *testing.T, beforeErr error, beforeWant []string, list []wrapper, maxDepth int) {
|
||||
if len(beforeWant) == 0 {
|
||||
panic("beforeWant must not be empty")
|
||||
}
|
||||
for _, w := range list {
|
||||
if len(w.want) == 0 {
|
||||
panic("want must not be empty")
|
||||
}
|
||||
|
||||
err := w.wrap(beforeErr)
|
||||
|
||||
// Copy required cause append(beforeWant, ..) modified beforeWant subtly.
|
||||
beforeCopy := make([]string, len(beforeWant))
|
||||
copy(beforeCopy, beforeWant)
|
||||
|
||||
beforeWant := beforeCopy
|
||||
last := len(beforeWant) - 1
|
||||
var want []string
|
||||
|
||||
// Merge two stacks behind each other.
|
||||
if strings.ContainsAny(beforeWant[last], "\n") && strings.ContainsAny(w.want[0], "\n") {
|
||||
want = append(beforeWant[:last], append([]string{beforeWant[last] + "((?s).*)" + w.want[0]}, w.want[1:]...)...)
|
||||
} else {
|
||||
want = append(beforeWant, w.want...)
|
||||
}
|
||||
|
||||
testFormatCompleteCompare(t, maxDepth, err, "%+v", want, false)
|
||||
if maxDepth > 0 {
|
||||
testGenericRecursive(t, err, want, list, maxDepth-1)
|
||||
}
|
||||
}
|
||||
}
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"path"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Frame represents a program counter inside a stack frame.
|
||||
type Frame uintptr
|
||||
|
||||
// pc returns the program counter for this frame;
|
||||
// multiple frames may have the same PC value.
|
||||
func (f Frame) pc() uintptr { return uintptr(f) - 1 }
|
||||
|
||||
// file returns the full path to the file that contains the
|
||||
// function for this Frame's pc.
|
||||
func (f Frame) file() string {
|
||||
fn := runtime.FuncForPC(f.pc())
|
||||
if fn == nil {
|
||||
return "unknown"
|
||||
}
|
||||
file, _ := fn.FileLine(f.pc())
|
||||
return file
|
||||
}
|
||||
|
||||
// line returns the line number of source code of the
|
||||
// function for this Frame's pc.
|
||||
func (f Frame) line() int {
|
||||
fn := runtime.FuncForPC(f.pc())
|
||||
if fn == nil {
|
||||
return 0
|
||||
}
|
||||
_, line := fn.FileLine(f.pc())
|
||||
return line
|
||||
}
|
||||
|
||||
// Format formats the frame according to the fmt.Formatter interface.
|
||||
//
|
||||
// %s source file
|
||||
// %d source line
|
||||
// %n function name
|
||||
// %v equivalent to %s:%d
|
||||
//
|
||||
// Format accepts flags that alter the printing of some verbs, as follows:
|
||||
//
|
||||
// %+s function name and path of source file relative to the compile time
|
||||
// GOPATH separated by \n\t (<funcname>\n\t<path>)
|
||||
// %+v equivalent to %+s:%d
|
||||
func (f Frame) Format(s fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 's':
|
||||
switch {
|
||||
case s.Flag('+'):
|
||||
pc := f.pc()
|
||||
fn := runtime.FuncForPC(pc)
|
||||
if fn == nil {
|
||||
io.WriteString(s, "unknown")
|
||||
} else {
|
||||
file, _ := fn.FileLine(pc)
|
||||
fmt.Fprintf(s, "%s\n\t%s", fn.Name(), file)
|
||||
}
|
||||
default:
|
||||
io.WriteString(s, path.Base(f.file()))
|
||||
}
|
||||
case 'd':
|
||||
fmt.Fprintf(s, "%d", f.line())
|
||||
case 'n':
|
||||
name := runtime.FuncForPC(f.pc()).Name()
|
||||
io.WriteString(s, funcname(name))
|
||||
case 'v':
|
||||
f.Format(s, 's')
|
||||
io.WriteString(s, ":")
|
||||
f.Format(s, 'd')
|
||||
}
|
||||
}
|
||||
|
||||
// StackTrace is stack of Frames from innermost (newest) to outermost (oldest).
|
||||
type StackTrace []Frame
|
||||
|
||||
// Format formats the stack of Frames according to the fmt.Formatter interface.
|
||||
//
|
||||
// %s lists source files for each Frame in the stack
|
||||
// %v lists the source file and line number for each Frame in the stack
|
||||
//
|
||||
// Format accepts flags that alter the printing of some verbs, as follows:
|
||||
//
|
||||
// %+v Prints filename, function, and line number for each Frame in the stack.
|
||||
func (st StackTrace) Format(s fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 'v':
|
||||
switch {
|
||||
case s.Flag('+'):
|
||||
for _, f := range st {
|
||||
fmt.Fprintf(s, "\n%+v", f)
|
||||
}
|
||||
case s.Flag('#'):
|
||||
fmt.Fprintf(s, "%#v", []Frame(st))
|
||||
default:
|
||||
fmt.Fprintf(s, "%v", []Frame(st))
|
||||
}
|
||||
case 's':
|
||||
fmt.Fprintf(s, "%s", []Frame(st))
|
||||
}
|
||||
}
|
||||
|
||||
// stack represents a stack of program counters.
|
||||
type stack []uintptr
|
||||
|
||||
func (s *stack) Format(st fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 'v':
|
||||
switch {
|
||||
case st.Flag('+'):
|
||||
for _, pc := range *s {
|
||||
f := Frame(pc)
|
||||
fmt.Fprintf(st, "\n%+v", f)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *stack) StackTrace() StackTrace {
|
||||
f := make([]Frame, len(*s))
|
||||
for i := 0; i < len(f); i++ {
|
||||
f[i] = Frame((*s)[i])
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
func callers() *stack {
|
||||
const depth = 32
|
||||
var pcs [depth]uintptr
|
||||
n := runtime.Callers(3, pcs[:])
|
||||
var st stack = pcs[0:n]
|
||||
return &st
|
||||
}
|
||||
|
||||
// funcname removes the path prefix component of a function's name reported by func.Name().
|
||||
func funcname(name string) string {
|
||||
i := strings.LastIndex(name, "/")
|
||||
name = name[i+1:]
|
||||
i = strings.Index(name, ".")
|
||||
return name[i+1:]
|
||||
}
|
||||
+274
@@ -0,0 +1,274 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var initpc, _, _, _ = runtime.Caller(0)
|
||||
|
||||
func TestFrameLine(t *testing.T) {
|
||||
var tests = []struct {
|
||||
Frame
|
||||
want int
|
||||
}{{
|
||||
Frame(initpc),
|
||||
9,
|
||||
}, {
|
||||
func() Frame {
|
||||
var pc, _, _, _ = runtime.Caller(0)
|
||||
return Frame(pc)
|
||||
}(),
|
||||
20,
|
||||
}, {
|
||||
func() Frame {
|
||||
var pc, _, _, _ = runtime.Caller(1)
|
||||
return Frame(pc)
|
||||
}(),
|
||||
28,
|
||||
}, {
|
||||
Frame(0), // invalid PC
|
||||
0,
|
||||
}}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := tt.Frame.line()
|
||||
want := tt.want
|
||||
if want != got {
|
||||
t.Errorf("Frame(%v): want: %v, got: %v", uintptr(tt.Frame), want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type X struct{}
|
||||
|
||||
func (x X) val() Frame {
|
||||
var pc, _, _, _ = runtime.Caller(0)
|
||||
return Frame(pc)
|
||||
}
|
||||
|
||||
func (x *X) ptr() Frame {
|
||||
var pc, _, _, _ = runtime.Caller(0)
|
||||
return Frame(pc)
|
||||
}
|
||||
|
||||
func TestFrameFormat(t *testing.T) {
|
||||
var tests = []struct {
|
||||
Frame
|
||||
format string
|
||||
want string
|
||||
}{{
|
||||
Frame(initpc),
|
||||
"%s",
|
||||
"stack_test.go",
|
||||
}, {
|
||||
Frame(initpc),
|
||||
"%+s",
|
||||
"github.com/pkg/errors.init\n" +
|
||||
"\t.+/github.com/pkg/errors/stack_test.go",
|
||||
}, {
|
||||
Frame(0),
|
||||
"%s",
|
||||
"unknown",
|
||||
}, {
|
||||
Frame(0),
|
||||
"%+s",
|
||||
"unknown",
|
||||
}, {
|
||||
Frame(initpc),
|
||||
"%d",
|
||||
"9",
|
||||
}, {
|
||||
Frame(0),
|
||||
"%d",
|
||||
"0",
|
||||
}, {
|
||||
Frame(initpc),
|
||||
"%n",
|
||||
"init",
|
||||
}, {
|
||||
func() Frame {
|
||||
var x X
|
||||
return x.ptr()
|
||||
}(),
|
||||
"%n",
|
||||
`\(\*X\).ptr`,
|
||||
}, {
|
||||
func() Frame {
|
||||
var x X
|
||||
return x.val()
|
||||
}(),
|
||||
"%n",
|
||||
"X.val",
|
||||
}, {
|
||||
Frame(0),
|
||||
"%n",
|
||||
"",
|
||||
}, {
|
||||
Frame(initpc),
|
||||
"%v",
|
||||
"stack_test.go:9",
|
||||
}, {
|
||||
Frame(initpc),
|
||||
"%+v",
|
||||
"github.com/pkg/errors.init\n" +
|
||||
"\t.+/github.com/pkg/errors/stack_test.go:9",
|
||||
}, {
|
||||
Frame(0),
|
||||
"%v",
|
||||
"unknown:0",
|
||||
}}
|
||||
|
||||
for i, tt := range tests {
|
||||
testFormatRegexp(t, i, tt.Frame, tt.format, tt.want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFuncname(t *testing.T) {
|
||||
tests := []struct {
|
||||
name, want string
|
||||
}{
|
||||
{"", ""},
|
||||
{"runtime.main", "main"},
|
||||
{"github.com/pkg/errors.funcname", "funcname"},
|
||||
{"funcname", "funcname"},
|
||||
{"io.copyBuffer", "copyBuffer"},
|
||||
{"main.(*R).Write", "(*R).Write"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := funcname(tt.name)
|
||||
want := tt.want
|
||||
if got != want {
|
||||
t.Errorf("funcname(%q): want: %q, got %q", tt.name, want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStackTrace(t *testing.T) {
|
||||
tests := []struct {
|
||||
err error
|
||||
want []string
|
||||
}{{
|
||||
New("ooh"), []string{
|
||||
"github.com/pkg/errors.TestStackTrace\n" +
|
||||
"\t.+/github.com/pkg/errors/stack_test.go:154",
|
||||
},
|
||||
}, {
|
||||
Wrap(New("ooh"), "ahh"), []string{
|
||||
"github.com/pkg/errors.TestStackTrace\n" +
|
||||
"\t.+/github.com/pkg/errors/stack_test.go:159", // this is the stack of Wrap, not New
|
||||
},
|
||||
}, {
|
||||
Cause(Wrap(New("ooh"), "ahh")), []string{
|
||||
"github.com/pkg/errors.TestStackTrace\n" +
|
||||
"\t.+/github.com/pkg/errors/stack_test.go:164", // this is the stack of New
|
||||
},
|
||||
}, {
|
||||
func() error { return New("ooh") }(), []string{
|
||||
`github.com/pkg/errors.(func·009|TestStackTrace.func1)` +
|
||||
"\n\t.+/github.com/pkg/errors/stack_test.go:169", // this is the stack of New
|
||||
"github.com/pkg/errors.TestStackTrace\n" +
|
||||
"\t.+/github.com/pkg/errors/stack_test.go:169", // this is the stack of New's caller
|
||||
},
|
||||
}, {
|
||||
Cause(func() error {
|
||||
return func() error {
|
||||
return Errorf("hello %s", fmt.Sprintf("world"))
|
||||
}()
|
||||
}()), []string{
|
||||
`github.com/pkg/errors.(func·010|TestStackTrace.func2.1)` +
|
||||
"\n\t.+/github.com/pkg/errors/stack_test.go:178", // this is the stack of Errorf
|
||||
`github.com/pkg/errors.(func·011|TestStackTrace.func2)` +
|
||||
"\n\t.+/github.com/pkg/errors/stack_test.go:179", // this is the stack of Errorf's caller
|
||||
"github.com/pkg/errors.TestStackTrace\n" +
|
||||
"\t.+/github.com/pkg/errors/stack_test.go:180", // this is the stack of Errorf's caller's caller
|
||||
},
|
||||
}}
|
||||
for i, tt := range tests {
|
||||
x, ok := tt.err.(interface {
|
||||
StackTrace() StackTrace
|
||||
})
|
||||
if !ok {
|
||||
t.Errorf("expected %#v to implement StackTrace() StackTrace", tt.err)
|
||||
continue
|
||||
}
|
||||
st := x.StackTrace()
|
||||
for j, want := range tt.want {
|
||||
testFormatRegexp(t, i, st[j], "%+v", want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func stackTrace() StackTrace {
|
||||
const depth = 8
|
||||
var pcs [depth]uintptr
|
||||
n := runtime.Callers(1, pcs[:])
|
||||
var st stack = pcs[0:n]
|
||||
return st.StackTrace()
|
||||
}
|
||||
|
||||
func TestStackTraceFormat(t *testing.T) {
|
||||
tests := []struct {
|
||||
StackTrace
|
||||
format string
|
||||
want string
|
||||
}{{
|
||||
nil,
|
||||
"%s",
|
||||
`\[\]`,
|
||||
}, {
|
||||
nil,
|
||||
"%v",
|
||||
`\[\]`,
|
||||
}, {
|
||||
nil,
|
||||
"%+v",
|
||||
"",
|
||||
}, {
|
||||
nil,
|
||||
"%#v",
|
||||
`\[\]errors.Frame\(nil\)`,
|
||||
}, {
|
||||
make(StackTrace, 0),
|
||||
"%s",
|
||||
`\[\]`,
|
||||
}, {
|
||||
make(StackTrace, 0),
|
||||
"%v",
|
||||
`\[\]`,
|
||||
}, {
|
||||
make(StackTrace, 0),
|
||||
"%+v",
|
||||
"",
|
||||
}, {
|
||||
make(StackTrace, 0),
|
||||
"%#v",
|
||||
`\[\]errors.Frame{}`,
|
||||
}, {
|
||||
stackTrace()[:2],
|
||||
"%s",
|
||||
`\[stack_test.go stack_test.go\]`,
|
||||
}, {
|
||||
stackTrace()[:2],
|
||||
"%v",
|
||||
`\[stack_test.go:207 stack_test.go:254\]`,
|
||||
}, {
|
||||
stackTrace()[:2],
|
||||
"%+v",
|
||||
"\n" +
|
||||
"github.com/pkg/errors.stackTrace\n" +
|
||||
"\t.+/github.com/pkg/errors/stack_test.go:207\n" +
|
||||
"github.com/pkg/errors.TestStackTraceFormat\n" +
|
||||
"\t.+/github.com/pkg/errors/stack_test.go:258",
|
||||
}, {
|
||||
stackTrace()[:2],
|
||||
"%#v",
|
||||
`\[\]errors.Frame{stack_test.go:207, stack_test.go:266}`,
|
||||
}}
|
||||
|
||||
for i, tt := range tests {
|
||||
testFormatRegexp(t, i, tt.StackTrace, tt.format, tt.want)
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
.DS_Store
|
||||
cmd/goose/goose*
|
||||
*.swp
|
||||
*.test
|
||||
|
||||
# Files output by tests
|
||||
bin
|
||||
go.db
|
||||
sql.db
|
||||
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
sudo: false
|
||||
language: go
|
||||
go:
|
||||
- 1.8
|
||||
|
||||
install:
|
||||
- go get github.com/golang/dep/cmd/dep
|
||||
- dep ensure
|
||||
|
||||
script:
|
||||
- go test
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
[[constraint]]
|
||||
name = "github.com/go-sql-driver/mysql"
|
||||
version = "^1.3.0"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/lib/pq"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/mattn/go-sqlite3"
|
||||
version = "^1.2.0"
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
Original work Copyright (c) 2012 Liam Staskawicz
|
||||
Modified work Copyright (c) 2016 Vojtech Vitek
|
||||
|
||||
MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
.PHONY: dist
|
||||
dist:
|
||||
@mkdir -p ./bin
|
||||
@rm -f ./bin/*
|
||||
GOOS=darwin GOARCH=amd64 go build -o ./bin/goose-darwin64 ./cmd/goose
|
||||
GOOS=linux GOARCH=amd64 go build -o ./bin/goose-linux64 ./cmd/goose
|
||||
GOOS=linux GOARCH=386 go build -o ./bin/goose-linux386 ./cmd/goose
|
||||
GOOS=windows GOARCH=amd64 go build -o ./bin/goose-windows64.exe ./cmd/goose
|
||||
GOOS=windows GOARCH=386 go build -o ./bin/goose-windows386.exe ./cmd/goose
|
||||
+256
@@ -0,0 +1,256 @@
|
||||
# goose
|
||||
|
||||
Goose is a database migration tool. Manage your database schema by creating incremental SQL changes or Go functions.
|
||||
|
||||
[![GoDoc Widget]][GoDoc] [![Travis Widget]][Travis]
|
||||
|
||||
### Goals of this fork
|
||||
|
||||
`github.com/pressly/goose` is a fork of `bitbucket.org/liamstask/goose` with the following changes:
|
||||
- No config files
|
||||
- [Default goose binary](./cmd/goose/main.go) can migrate SQL files only
|
||||
- Go migrations:
|
||||
- We don't `go build` Go migrations functions on-the-fly
|
||||
from within the goose binary
|
||||
- Instead, we let you
|
||||
[create your own custom goose binary](examples/go-migrations),
|
||||
register your Go migration functions explicitly and run complex
|
||||
migrations with your own `*sql.DB` connection
|
||||
- Go migration functions let you run your code within
|
||||
an SQL transaction, if you use the `*sql.Tx` argument
|
||||
- The goose pkg is decoupled from the binary:
|
||||
- goose pkg doesn't register any SQL drivers anymore,
|
||||
thus no driver `panic()` conflict within your codebase!
|
||||
- goose pkg doesn't have any vendor dependencies anymore
|
||||
- We use timestamped migrations by default but recommend a hybrid approach of using timestamps in the development process and sequential versions in production.
|
||||
|
||||
# Install
|
||||
|
||||
$ go get -u github.com/pressly/goose/cmd/goose
|
||||
|
||||
This will install the `goose` binary to your `$GOPATH/bin` directory.
|
||||
|
||||
For a lite version of the binary without DB connection dependent commands, use the exclusive build tags:
|
||||
|
||||
$ go build -tags='no_mysql no_sqlite no_psql' -i -o goose ./cmd/goose
|
||||
|
||||
|
||||
# Usage
|
||||
|
||||
```
|
||||
Usage: goose [OPTIONS] DRIVER DBSTRING COMMAND
|
||||
|
||||
Drivers:
|
||||
postgres
|
||||
mysql
|
||||
sqlite3
|
||||
redshift
|
||||
|
||||
Commands:
|
||||
up Migrate the DB to the most recent version available
|
||||
up-to VERSION Migrate the DB to a specific VERSION
|
||||
down Roll back the version by 1
|
||||
down-to VERSION Roll back to a specific VERSION
|
||||
redo Re-run the latest migration
|
||||
status Dump the migration status for the current DB
|
||||
version Print the current version of the database
|
||||
create NAME [sql|go] Creates new migration file with the current timestamp
|
||||
|
||||
Options:
|
||||
-dir string
|
||||
directory with migration files (default ".")
|
||||
|
||||
Examples:
|
||||
goose sqlite3 ./foo.db status
|
||||
goose sqlite3 ./foo.db create init sql
|
||||
goose sqlite3 ./foo.db create add_some_column sql
|
||||
goose sqlite3 ./foo.db create fetch_user_data go
|
||||
goose sqlite3 ./foo.db up
|
||||
|
||||
goose postgres "user=postgres dbname=postgres sslmode=disable" status
|
||||
goose mysql "user:password@/dbname?parseTime=true" status
|
||||
goose redshift "postgres://user:password@qwerty.us-east-1.redshift.amazonaws.com:5439/db" status
|
||||
goose tidb "user:password@/dbname?parseTime=true" status
|
||||
```
|
||||
## create
|
||||
|
||||
Create a new SQL migration.
|
||||
|
||||
$ goose create add_some_column sql
|
||||
$ Created new file: 20170506082420_add_some_column.sql
|
||||
|
||||
Edit the newly created file to define the behavior of your migration.
|
||||
|
||||
You can also create a Go migration, if you then invoke it with [your own goose binary](#go-migrations):
|
||||
|
||||
$ goose create fetch_user_data go
|
||||
$ Created new file: 20170506082421_fetch_user_data.go
|
||||
|
||||
## up
|
||||
|
||||
Apply all available migrations.
|
||||
|
||||
$ goose up
|
||||
$ OK 001_basics.sql
|
||||
$ OK 002_next.sql
|
||||
$ OK 003_and_again.go
|
||||
|
||||
## up-to
|
||||
|
||||
Migrate up to a specific version.
|
||||
|
||||
$ goose up-to 20170506082420
|
||||
$ OK 20170506082420_create_table.sql
|
||||
|
||||
## down
|
||||
|
||||
Roll back a single migration from the current version.
|
||||
|
||||
$ goose down
|
||||
$ OK 003_and_again.go
|
||||
|
||||
## down-to
|
||||
|
||||
Roll back migrations to a specific version.
|
||||
|
||||
$ goose down-to 20170506082527
|
||||
$ OK 20170506082527_alter_column.sql
|
||||
|
||||
## redo
|
||||
|
||||
Roll back the most recently applied migration, then run it again.
|
||||
|
||||
$ goose redo
|
||||
$ OK 003_and_again.go
|
||||
$ OK 003_and_again.go
|
||||
|
||||
## status
|
||||
|
||||
Print the status of all migrations:
|
||||
|
||||
$ goose status
|
||||
$ Applied At Migration
|
||||
$ =======================================
|
||||
$ Sun Jan 6 11:25:03 2013 -- 001_basics.sql
|
||||
$ Sun Jan 6 11:25:03 2013 -- 002_next.sql
|
||||
$ Pending -- 003_and_again.go
|
||||
|
||||
Note: for MySQL [parseTime flag](https://github.com/go-sql-driver/mysql#parsetime) must be enabled.
|
||||
|
||||
## version
|
||||
|
||||
Print the current version of the database:
|
||||
|
||||
$ goose version
|
||||
$ goose: version 002
|
||||
|
||||
# Migrations
|
||||
|
||||
goose supports migrations written in SQL or in Go.
|
||||
|
||||
## SQL Migrations
|
||||
|
||||
A sample SQL migration looks like:
|
||||
|
||||
```sql
|
||||
-- +goose Up
|
||||
CREATE TABLE post (
|
||||
id int NOT NULL,
|
||||
title text,
|
||||
body text,
|
||||
PRIMARY KEY(id)
|
||||
);
|
||||
|
||||
-- +goose Down
|
||||
DROP TABLE post;
|
||||
```
|
||||
|
||||
Notice the annotations in the comments. Any statements following `-- +goose Up` will be executed as part of a forward migration, and any statements following `-- +goose Down` will be executed as part of a rollback.
|
||||
|
||||
By default, all migrations are run within a transaction. Some statements like `CREATE DATABASE`, however, cannot be run within a transaction. You may optionally add `-- +goose NO TRANSACTION` to the top of your migration
|
||||
file in order to skip transactions within that specific migration file. Both Up and Down migrations within this file will be run without transactions.
|
||||
|
||||
By default, SQL statements are delimited by semicolons - in fact, query statements must end with a semicolon to be properly recognized by goose.
|
||||
|
||||
More complex statements (PL/pgSQL) that have semicolons within them must be annotated with `-- +goose StatementBegin` and `-- +goose StatementEnd` to be properly recognized. For example:
|
||||
|
||||
```sql
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
CREATE OR REPLACE FUNCTION histories_partition_creation( DATE, DATE )
|
||||
returns void AS $$
|
||||
DECLARE
|
||||
create_query text;
|
||||
BEGIN
|
||||
FOR create_query IN SELECT
|
||||
'CREATE TABLE IF NOT EXISTS histories_'
|
||||
|| TO_CHAR( d, 'YYYY_MM' )
|
||||
|| ' ( CHECK( created_at >= timestamp '''
|
||||
|| TO_CHAR( d, 'YYYY-MM-DD 00:00:00' )
|
||||
|| ''' AND created_at < timestamp '''
|
||||
|| TO_CHAR( d + INTERVAL '1 month', 'YYYY-MM-DD 00:00:00' )
|
||||
|| ''' ) ) inherits ( histories );'
|
||||
FROM generate_series( $1, $2, '1 month' ) AS d
|
||||
LOOP
|
||||
EXECUTE create_query;
|
||||
END LOOP; -- LOOP END
|
||||
END; -- FUNCTION END
|
||||
$$
|
||||
language plpgsql;
|
||||
-- +goose StatementEnd
|
||||
```
|
||||
|
||||
## Go Migrations
|
||||
|
||||
1. Create your own goose binary, see [example](./examples/go-migrations)
|
||||
2. Import `github.com/pressly/goose`
|
||||
3. Register your migration functions
|
||||
4. Run goose command, ie. `goose.Up(db *sql.DB, dir string)`
|
||||
|
||||
A [sample Go migration 00002_users_add_email.go file](./example/migrations-go/00002_rename_root.go) looks like:
|
||||
|
||||
```go
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/pressly/goose"
|
||||
)
|
||||
|
||||
func init() {
|
||||
goose.AddMigration(Up, Down)
|
||||
}
|
||||
|
||||
func Up(tx *sql.Tx) error {
|
||||
_, err := tx.Exec("UPDATE users SET username='admin' WHERE username='root';")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Down(tx *sql.Tx) error {
|
||||
_, err := tx.Exec("UPDATE users SET username='root' WHERE username='admin';")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
# Hybrid Versioning
|
||||
Please, read the [versioning problem](https://github.com/pressly/goose/issues/63#issuecomment-428681694) first.
|
||||
|
||||
We strongly recommend adopting a hybrid versioning approach, using both timestamps and sequential numbers. Migrations created during the development process are timestamped and sequential versions are ran on production. We believe this method will prevent the problem of conflicting versions when writing software in a team environment.
|
||||
|
||||
To help you adopt this approach, `create` will use the current timestamp as the migration version. When you're ready to deploy your migrations in a production environment, we also provide a helpful `fix` command to convert your migrations into sequential order, while preserving the timestamp ordering. We recommend running `fix` in the CI pipeline, and only when the migrations are ready for production.
|
||||
|
||||
## License
|
||||
|
||||
Licensed under [MIT License](./LICENSE)
|
||||
|
||||
[GoDoc]: https://godoc.org/github.com/pressly/goose
|
||||
[GoDoc Widget]: https://godoc.org/github.com/pressly/goose?status.svg
|
||||
[Travis]: https://travis-ci.org/pressly/goose
|
||||
[Travis Widget]: https://travis-ci.org/pressly/goose.svg?branch=master
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
// +build !no_mysql
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
_ "github.com/ziutek/mymysql/godrv"
|
||||
)
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
// +build !no_pq
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "github.com/lib/pq"
|
||||
)
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
// +build !no_sqlite
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/pressly/goose"
|
||||
)
|
||||
|
||||
var (
|
||||
flags = flag.NewFlagSet("goose", flag.ExitOnError)
|
||||
dir = flags.String("dir", ".", "directory with migration files")
|
||||
verbose = flags.Bool("v", false, "enable verbose mode")
|
||||
help = flags.Bool("h", false, "print help")
|
||||
version = flags.Bool("version", false, "print version")
|
||||
)
|
||||
|
||||
func main() {
|
||||
flags.Usage = usage
|
||||
flags.Parse(os.Args[1:])
|
||||
|
||||
if *version {
|
||||
fmt.Println(goose.VERSION)
|
||||
return
|
||||
}
|
||||
if *verbose {
|
||||
goose.SetVerbose(true)
|
||||
}
|
||||
|
||||
args := flags.Args()
|
||||
if len(args) == 0 || *help {
|
||||
flags.Usage()
|
||||
return
|
||||
}
|
||||
|
||||
switch args[0] {
|
||||
case "create":
|
||||
if err := goose.Run("create", nil, *dir, args[1:]...); err != nil {
|
||||
log.Fatalf("goose run: %v", err)
|
||||
}
|
||||
return
|
||||
case "fix":
|
||||
if err := goose.Run("fix", nil, *dir); err != nil {
|
||||
log.Fatalf("goose run: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if len(args) < 3 {
|
||||
flags.Usage()
|
||||
return
|
||||
}
|
||||
|
||||
driver, dbstring, command := args[0], args[1], args[2]
|
||||
|
||||
if err := goose.SetDialect(driver); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
switch driver {
|
||||
case "redshift":
|
||||
driver = "postgres"
|
||||
case "tidb":
|
||||
driver = "mysql"
|
||||
}
|
||||
|
||||
switch dbstring {
|
||||
case "":
|
||||
log.Fatalf("-dbstring=%q not supported\n", dbstring)
|
||||
default:
|
||||
}
|
||||
|
||||
db, err := sql.Open(driver, dbstring)
|
||||
if err != nil {
|
||||
log.Fatalf("-dbstring=%q: %v\n", dbstring, err)
|
||||
}
|
||||
|
||||
arguments := []string{}
|
||||
if len(args) > 3 {
|
||||
arguments = append(arguments, args[3:]...)
|
||||
}
|
||||
|
||||
if err := goose.Run(command, db, *dir, arguments...); err != nil {
|
||||
log.Fatalf("goose run: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func usage() {
|
||||
fmt.Println(usagePrefix)
|
||||
flags.PrintDefaults()
|
||||
fmt.Println(usageCommands)
|
||||
}
|
||||
|
||||
var (
|
||||
usagePrefix = `Usage: goose [OPTIONS] DRIVER DBSTRING COMMAND
|
||||
|
||||
Drivers:
|
||||
postgres
|
||||
mysql
|
||||
sqlite3
|
||||
redshift
|
||||
|
||||
Examples:
|
||||
goose sqlite3 ./foo.db status
|
||||
goose sqlite3 ./foo.db create init sql
|
||||
goose sqlite3 ./foo.db create add_some_column sql
|
||||
goose sqlite3 ./foo.db create fetch_user_data go
|
||||
goose sqlite3 ./foo.db up
|
||||
|
||||
goose postgres "user=postgres dbname=postgres sslmode=disable" status
|
||||
goose mysql "user:password@/dbname?parseTime=true" status
|
||||
goose redshift "postgres://user:password@qwerty.us-east-1.redshift.amazonaws.com:5439/db" status
|
||||
goose tidb "user:password@/dbname?parseTime=true" status
|
||||
|
||||
Options:
|
||||
`
|
||||
|
||||
usageCommands = `
|
||||
Commands:
|
||||
up Migrate the DB to the most recent version available
|
||||
up-to VERSION Migrate the DB to a specific VERSION
|
||||
down Roll back the version by 1
|
||||
down-to VERSION Roll back to a specific VERSION
|
||||
redo Re-run the latest migration
|
||||
reset Roll back all migrations
|
||||
status Dump the migration status for the current DB
|
||||
version Print the current version of the database
|
||||
create NAME [sql|go] Creates new migration file with the current timestamp
|
||||
fix Apply sequential ordering to migrations
|
||||
`
|
||||
)
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
package goose
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"text/template"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Create writes a new blank migration file.
|
||||
func CreateWithTemplate(db *sql.DB, dir string, migrationTemplate *template.Template, name, migrationType string) error {
|
||||
version := time.Now().Format(timestampFormat)
|
||||
filename := fmt.Sprintf("%v_%v.%v", version, name, migrationType)
|
||||
|
||||
fpath := filepath.Join(dir, filename)
|
||||
|
||||
tmpl := sqlMigrationTemplate
|
||||
if migrationType == "go" {
|
||||
tmpl = goSQLMigrationTemplate
|
||||
}
|
||||
|
||||
if migrationTemplate != nil {
|
||||
tmpl = migrationTemplate
|
||||
}
|
||||
|
||||
path, err := writeTemplateToFile(fpath, tmpl, version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("Created new file: %s\n", path)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create writes a new blank migration file.
|
||||
func Create(db *sql.DB, dir, name, migrationType string) error {
|
||||
return CreateWithTemplate(db, dir, nil, name, migrationType)
|
||||
}
|
||||
|
||||
func writeTemplateToFile(path string, t *template.Template, version string) (string, error) {
|
||||
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
||||
return "", fmt.Errorf("failed to create file: %v already exists", path)
|
||||
}
|
||||
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
err = t.Execute(f, version)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return f.Name(), nil
|
||||
}
|
||||
|
||||
var sqlMigrationTemplate = template.Must(template.New("goose.sql-migration").Parse(`-- +goose Up
|
||||
-- SQL in this section is executed when the migration is applied.
|
||||
|
||||
-- +goose Down
|
||||
-- SQL in this section is executed when the migration is rolled back.
|
||||
`))
|
||||
|
||||
var goSQLMigrationTemplate = template.Must(template.New("goose.go-migration").Parse(`package migration
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"github.com/pressly/goose"
|
||||
)
|
||||
|
||||
func init() {
|
||||
goose.AddMigration(Up{{.}}, Down{{.}})
|
||||
}
|
||||
|
||||
func Up{{.}}(tx *sql.Tx) error {
|
||||
// This code is executed when the migration is applied.
|
||||
return nil
|
||||
}
|
||||
|
||||
func Down{{.}}(tx *sql.Tx) error {
|
||||
// This code is executed when the migration is rolled back.
|
||||
return nil
|
||||
}
|
||||
`))
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user