Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mutations #10

Merged
merged 52 commits into from
Feb 3, 2020
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
2ee2686
init
dOrgJelli Dec 24, 2019
98f9039
newlines
dOrgJelli Dec 24, 2019
3441108
beta
dOrgJelli Dec 24, 2019
03dc4a9
resolver state question
dOrgJelli Dec 24, 2019
2b31f7d
useMutation modified
dOrgJelli Dec 24, 2019
ea84141
edits based on feedback
dOrgJelli Jan 10, 2020
20d975b
changes based on feedback
dOrgJelli Jan 11, 2020
31e3d30
remove config question
dOrgJelli Jan 11, 2020
feee73f
add subgraph name
dOrgJelli Jan 12, 2020
4c61778
our -> the
dOrgJelli Jan 20, 2020
f242ee3
mutations schema builds upon...
dOrgJelli Jan 20, 2020
f272b34
setEnity -> setEntity
dOrgJelli Jan 20, 2020
6272d9a
graph-cli description grammar
dOrgJelli Jan 20, 2020
7ed7b9c
executes the mutation query
dOrgJelli Jan 20, 2020
d9ceb40
javascript/es5
dOrgJelli Jan 20, 2020
6a14b59
inline code resolvers & config
dOrgJelli Jan 20, 2020
1c53960
remove datasource api
dOrgJelli Jan 21, 2020
4e96fa8
package split
dOrgJelli Jan 21, 2020
9ff22b0
remove node + subgraph
dOrgJelli Jan 21, 2020
473952f
apollo link comment
dOrgJelli Jan 21, 2020
67d7f0a
optimistic response update
dOrgJelli Jan 21, 2020
6c5c385
use mutation comment
dOrgJelli Jan 21, 2020
8cb2dec
definitions
dOrgJelli Jan 23, 2020
83adcd3
mutation resolvers module types + new state interface
dOrgJelli Jan 23, 2020
b64ad77
API types + typescript dApp
dOrgJelli Jan 23, 2020
3de5850
open questions
dOrgJelli Jan 23, 2020
10abcd1
types
dOrgJelli Jan 23, 2020
f1238af
dApp event handlers
dOrgJelli Jan 23, 2020
1f37afa
fix export
dOrgJelli Jan 23, 2020
6511624
await dispatch
dOrgJelli Jan 23, 2020
c298c6d
renaming
dOrgJelli Jan 23, 2020
6f7a1ef
more questions
dOrgJelli Jan 23, 2020
bd968b0
rename mutations-ts to mutations
dOrgJelli Jan 24, 2020
ca1fbde
reducer event: Event param
dOrgJelli Jan 24, 2020
68dc6b5
mutations.yaml specVersion
dOrgJelli Feb 1, 2020
6e36176
remove ES5 open question
dOrgJelli Feb 1, 2020
fbe9956
Web3 casing
dOrgJelli Feb 1, 2020
a9221ff
ext -> extended
dOrgJelli Feb 1, 2020
5c7c3ad
resolvers' -> resolvers
dOrgJelli Feb 1, 2020
b4c6035
remove datasources API
dOrgJelli Feb 1, 2020
9df0802
fix <Mutation .../> typings
dOrgJelli Feb 1, 2020
a55ccbb
grammar fixes
dOrgJelli Feb 1, 2020
aa8973d
grammar
dOrgJelli Feb 1, 2020
c3812d8
proposed steps implementation
dOrgJelli Feb 1, 2020
3a57d7b
typing updates
dOrgJelli Feb 1, 2020
b75e514
event type map fix
dOrgJelli Feb 1, 2020
4c81863
typing updates
dOrgJelli Feb 1, 2020
79fe623
add MutationContext type
dOrgJelli Feb 1, 2020
6d7fe02
update useMutation example & types w/ multi-state support
dOrgJelli Feb 1, 2020
113aab5
please refer to apollo's docs...
dOrgJelli Feb 1, 2020
b223207
fix Mutation prop type
dOrgJelli Feb 1, 2020
6d07744
require types file w/ Config type
dOrgJelli Feb 1, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
- [RFCs](./rfcs/index.md)
- [Approved RFCs](./rfcs/approved.md)
- [RFC-0002: Ethereum Tracing Cache](./rfcs/0002-ethereum-tracing-cache.md)
- [RFC-0003: Mutations](./rfcs/0003-mutations.md)
- [Obsolete RFCs](./rfcs/obsolete.md)
- [Rejected RFCs](./rfcs/rejected.md)
- [Engineering Plans](./engineering-plans/index.md)
Expand Down
317 changes: 317 additions & 0 deletions rfcs/0003-mutations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,317 @@
# RFC-0003: Mutations

<dl>
<dt>Author</dt>
<dd>dOrg: Jordan Ellis, Nestor Amesty</dd>

<dt>RFC pull request</dt>
<dd><a href="https://github.com/graphprotocol/rfcs/pull/10">URL</a></dd>

<dt>Date of submission</dt>
<dd>2019-12-20</dd>

<dt>Date of approval</dt>
<dd>YYYY-MM-DD</dd>

<dt>Approved by</dt>
<dd>First Person, Second Person</dd>
</dl>

## Contents

<!-- toc -->

## Summary

GraphQL mutations allow developers to add executable functions to their schema. Callers can invoke these functions using GraphQL queries. An introduction to how mutations are defined and work can be found [here](https://graphql.org/learn/queries/#mutations). This RFC will assume the reader understands how to use GraphQL mutations in a traditional Web2 application. This proposal describes how mutations are added to The Graph's toolchain, and used to replace web3 write operations the same way The Graph has replaced Web3 read operations.
dOrgJelli marked this conversation as resolved.
Show resolved Hide resolved

## Goals & Motivation

The Graph has created a read semantic layer that describes smart contract protocols, which has made it easier to build applications ontop of complex protocols. Since dApps have two primary interactions with web3 protocols (reading & writing), the next logical addition is write support.

Protocol developers that use a subgraph still often publish a Javascript wrapper library for their dApp developers (examples: [DAOstack](https://github.com/daostack/client), [ENS](https://github.com/ensdomains/ensjs), [LivePeer](https://github.com/livepeer/livepeerjs/tree/master/packages/sdk), [DAI](https://github.com/makerdao/dai.js/tree/dev/packages/dai), [Uniswap](https://github.com/Uniswap/uniswap-sdk)). This is done to help speed up dApp development and promote consistency with protocol usage patterns. With the addition of mutations to the Graph Protocol's GraphQL tooling, Web3 reading & writing can now both be invoked through GraphQL queries. dApp developers can now simply refer to a single GraphQL schema that defines the entire protocol.

## Urgency

This is urgent from a developer experience point of view. With this addition, it eliminates the need for protocol developers to manually wrap GraphQL query interfaces alongside developer-friendly write functions. Additionally, mutations provide a solution for optimistic UI updates, which is something dApp developers have been seeking for a long time (see [here](https://github.com/aragon/nest/issues/21)). Lastly with the whole protocol now defined in GraphQL, existing application layer code generators can now be used to hasten dApp development ([some examples](https://dev.to/graphqleditor/top-3-graphql-code-generators-1gnj)).

## Terminology
dOrgJelli marked this conversation as resolved.
Show resolved Hide resolved

* _Mutations_: Collection of mutations.
* _Mutation_: A GraphQL mutation.
* _Mutations Schema_: A GraphQL schema that defines a `type Mutation` that contains all mutations. Additionally, this schema can define other types to be used by the mutations, such as `input` and `interface` types.
* _Mutations Manifest_: A YAML manifest file that is used to add mutations to an existing subgraph manifest.
* _Mutation Resolvers_: Code module that contains all resolvers.
* _Resolver_: Function that is used to execute a mutation.
* _Mutation State_: The state of a mutation being executed. It's passed to the resolver through the mutation context.
* _Mutation Context_: A context object that's created for every mutation that's executed. It's passed as an argument to the resolver.
* _Config_: Collection of config properties required by the mutation resolvers.
* _Config Property_: A single property within the config (ex: ipfs, ethereum, etc).
* _Config Generator_: A function that takes a config value, and returns a config property. For example, "localhost:5001" as a config value gets turned into a new IPFS client by the config generator.
* _Config Value_: An initialization value that's passed into the config generator. This config value is provided by the dApp developer.
* _Optimistic Response_: A response given to the dApp that predicts what the outcome of the mutation's execution will be. If it is incorrect, it will be overwritten with the actual result.

## Detailed Design

Jannis marked this conversation as resolved.
Show resolved Hide resolved
The sections below illustrate how a developer would add mutations to an existing subgraph, and then add those mutations to a dApp.

### Mutations Manifest

The subgraph manifest (`subgraph.yaml`) now has an extra property named `mutations` which is the mutations manifest.

`subgraph.yaml`
```yaml
specVersion: ...
...
mutations:
repository: https://npmjs.com/package/...
schema:
file: ./mutations/schema.graphql
resolvers:
apiVersion: 0.0.1
kind: javascript/es5
file: ./mutations/index.js
dataSources: ...
...
```

Alternatively, the mutation manifest can be external like so:
`subgraph.yaml`
```yaml
specVersion: ...
dOrgJelli marked this conversation as resolved.
Show resolved Hide resolved
...
mutations:
file: ./mutations/mutations.yaml
dataSources: ...
...
```
`mutations/mutations.yaml`
```yaml
repository: https://npmjs.com/package/...
dOrgJelli marked this conversation as resolved.
Show resolved Hide resolved
schema:
file: ./schema.graphql
resolvers:
apiVersion: 0.0.1
kind: javascript/es5
file: ./index.js
```

### Mutations Schema

The mutations schema defines all of the mutations in our subgraph. The mutations schema is a super-set of the subgraph's schema. For example, starting from a base subgraph schema:
dOrgJelli marked this conversation as resolved.
Show resolved Hide resolved
dOrgJelli marked this conversation as resolved.
Show resolved Hide resolved
`schema.graphql`
dOrgJelli marked this conversation as resolved.
Show resolved Hide resolved
```graphql
type MyEntity @entity {
id: ID!
name: String!
value: BigInt!
}
```

Developers can define mutations that reference these subgraph schema types. Additionally new `input` and `interface` types can be defined for the mutations to use:
`mutations/schema.graphql`
```graphql
input MyEntityOptions {
name: String!
value: BigInt!
}

interface NewNameSet {
oldName: String!
newName: String!
}

type Mutation {
createEntity(
options: MyEntityOptions!
): MyEntity!

setEnityName(
dOrgJelli marked this conversation as resolved.
Show resolved Hide resolved
entity: MyEntity!
name: String!
): NewNameSet!
}
```

`graph-cli` handles the combining, parsing, and validating of these two schemas. The `graph-cli` verifies that the mutations schema defines a `type Mutation`, that all of the mutations within it are defined in the resolvers module (see next section).
dOrgJelli marked this conversation as resolved.
Show resolved Hide resolved

### Mutation Resolvers

Each mutation within the schema must have a corresponding resolver function defined. Resolvers will be invoked by whatever engine executes the query. They are executed locally within the client application.
dOrgJelli marked this conversation as resolved.
Show resolved Hide resolved

Mutation resolvers of kind `javascript` take the form of a javascript module. This module is expected to have a default export that contains the following properties:
dOrgJelli marked this conversation as resolved.
Show resolved Hide resolved
* resolvers - The mutation resolver functions.
* config - A collection of config generators.
dOrgJelli marked this conversation as resolved.
Show resolved Hide resolved

`mutations/index.js`
dOrgJelli marked this conversation as resolved.
Show resolved Hide resolved
```javascript
const resolvers = {
Mutation: {
async createEntity (_, args, context) {
// Extract mutation arguments
const { name, value } = args.options

// Use config properties created by the
// config generator functions
const { ethereum, ipfs } = context.graph.config

// Fetch datasource addresses & abis
const { MyContract } = context.graph.datasources
await MyContract.abi
await MyContract.address
dOrgJelli marked this conversation as resolved.
Show resolved Hide resolved

// Modify a state object, which relays updates back
// to the subscribed dApp
const { state } = context.graph
state.addTransaction("tx_hash")
dOrgJelli marked this conversation as resolved.
Show resolved Hide resolved

...
},
async setEntityName (_, args, context) {
...
}
}
}

// Config generators
const config = {
// These function arguments are passed in by the dApp
ethereum (provider) {
return new ethers.providers.Web3Provider(provider)
},
ipfs (provider) {
return new IPFS(provider)
},
customProperty (value) {
return value + 2
},
rootProperty: {
nestedProperty (value) {
...
}
}
}

export default {
resolvers,
config
}
```

### dApp Integration
```javascript
const {
createMutations,
createMutationsLink,
useMutation
} = require("@graphprotocol/mutations-ts")
dOrgJelli marked this conversation as resolved.
Show resolved Hide resolved
const myMutations = require("mutations-js-module")

const mutations = createMutations({
mutations: myMutations,
subgraph: "my-subgraph",
node: "http://localhost:8080",
dOrgJelli marked this conversation as resolved.
Show resolved Hide resolved
// Config values, which will be passed to the generators
config: {
ethereum: async () => {
const { ethereum } = (window as any)
await ethereum.enable()
return ethereum
},
ipfs: "http://localhost:5001",
customProperty: 5,
rootProperty: {
nestedProperty: "foo"
}
}
})

// Create an Apollo Links
dOrgJelli marked this conversation as resolved.
Show resolved Hide resolved
const mutationLink = createMutationLink({ mutations })
const queryLink = createHttpLink({
uri: "http://localhost:5001/subgraphs/name/my-subgraph"
})

const link = split(
({ query }) => {
const node = getMainDefinition(query);
return node.kind === "OperationDefinition" &&
node.operation === "mutation"
},
mutationLink,
queryLink
);

// Create Apollo Client
const client = new ApolloClient({
link,
cache: new InMemoryCache()
})

const CREATE_ENTITY = gql`
mutation createEntity($options: MyEntityOptions) {
createEntity(options: $options) {
id
name
value
}
}
`

// state === mutation state
const [exec, { loading, state }] = useMutation(
CREATE_ENTITY,
{
client,
variables: {
options: { name: "...", value: 5 }
}
}
)

// Optimistic responses can be used to update
// the UI before the execution has finished
const [exec, { loading, state }] = useMutation(
CREATE_ENTITY,
{
optimisticResponse: {
myEntity: {
dOrgJelli marked this conversation as resolved.
Show resolved Hide resolved
id: "...",
name: "...",
value: 5,
}
},
update(proxy, { data }) {
// result = data.myEntity
dOrgJelli marked this conversation as resolved.
Show resolved Hide resolved
},
onError(error) {
...
},
variables: {
options: { name: "...", value: 5 }
}
}
)
```

## Compatibility

No breaking changes will be introduced, as mutations are an optional add-on to a subgraph.

## Drawbacks and Risks

I have some thoughts but they are rather verbose and tangential. Would love some feedback on this from others first.
dOrgJelli marked this conversation as resolved.
Show resolved Hide resolved

## Alternatives

The existing alternative that protocol developers are creating for dApp developers has been described above.

## Open Questions
dOrgJelli marked this conversation as resolved.
Show resolved Hide resolved

- **Should the resolvers module be ES5 compliant?**
The prototype was originally developed under these conditions. ES5 compliance has since been abandoned as it has proven nearly impossible to successfully transpile all dependencies into a single monolithic module.
dOrgJelli marked this conversation as resolved.
Show resolved Hide resolved

- **What paradigm should the mutation state follow?**
One option is to have the resolver's call into a single interface that modifies the backing data. Whenever this data is modified, the entirety of it is passed to the dApp. The downside here is that the dApp doesn't know what has changed within the data, and is forced to represent it in its entirety in order to not miss anything.

Another option is to implement something similar to Redux, where the resolvers fire off events with corresponding payloads of data. These events map to reducers, which take in this payload of data and decide what to do with it. The dApp could implement these reducers, and choose how it would want to react to the various events.
1 change: 1 addition & 0 deletions rfcs/approved.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# Approved RFCs

- [RFC-0002: Ethereum Tracing Cache](./0002-ethereum-tracing-cache.md)
- [RFC-0003: Mutations](./0003-mutations.md)