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 5 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
280 changes: 280 additions & 0 deletions rfcs/0003-mutations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
# RFC-0003: Mutations

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

<dt>RFC pull request</dt>
<dd><a href="TODO">URL</a></dd>
dOrgJelli marked this conversation as resolved.
Show resolved Hide resolved

<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 you to add executable functions to your 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. Going forward we'll describe how Mutations can be 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

Currently, 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.
dOrgJelli marked this conversation as resolved.
Show resolved Hide resolved

Protocol developers that currently use a subgraph still often publish a Javascript wrapper library for their dApp developers. 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.
dOrgJelli marked this conversation as resolved.
Show resolved Hide resolved

## Urgency

From a developer experience point of view I see this as urgent because it eliminates the need for protocol developers to manaually wrap their graphql query interfaces alongside user-friendly write functions. Additionally from a user experience point of view, mutations provide a solution for optimistic UI updates, which is something dApp developers have been wanting for a long time. Lastly with the whole protocol now defined in GraphQL, existing application layer code generators can now be used to hasten dApp development.
dOrgJelli marked this conversation as resolved.
Show resolved Hide resolved
dOrgJelli marked this conversation as resolved.
Show resolved Hide resolved
dOrgJelli marked this conversation as resolved.
Show resolved Hide resolved
dOrgJelli marked this conversation as resolved.
Show resolved Hide resolved
dOrgJelli marked this conversation as resolved.
Show resolved Hide resolved

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

* _Mutation_: GraphQL Mutation.
dOrgJelli marked this conversation as resolved.
Show resolved Hide resolved
* _Resolver_: Resolver function that's mapped to a mutation.
dOrgJelli marked this conversation as resolved.
Show resolved Hide resolved
* _Resolver State_: The resolver function's state (transactions sent, data logged, etc).
dOrgJelli marked this conversation as resolved.
Show resolved Hide resolved
* _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
### Mutation Manifest

`subgraph.yaml`
```yaml
specVersion: ...
...
mutations:
specVersion: 0.0.1
dOrgJelli marked this conversation as resolved.
Show resolved Hide resolved
repository: https://npmjs.com/package/...
schema:
file: ./mutations/schema.graphql
resolvers:
kind: javascript
dOrgJelli marked this conversation as resolved.
Show resolved Hide resolved
file: ./mutations/index.js
dataSources: ...
...
```

Alternatively, you can store the mutations manifest externally 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
specVersion: 0.0.1
repository: https://npmjs.com/package/...
dOrgJelli marked this conversation as resolved.
Show resolved Hide resolved
schema:
file: ./schema.graphql
resolvers:
kind: javascript
dOrgJelli marked this conversation as resolved.
Show resolved Hide resolved
file: ./index.js
```

### Mutation Schema
`schema.graphql`
dOrgJelli marked this conversation as resolved.
Show resolved Hide resolved
```graphql
type MyEntity @entity {
id: ID!
name: String!
value: BigInt!
}
```

`mutations/schema.graphql`
```graphql
input MyEntityOptions {
name: String!
value: BigInt!
}

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

setEnityName(
dOrgJelli marked this conversation as resolved.
Show resolved Hide resolved
entity: MyEntity!
name: String!
): MyEntity!
}
```
**NOTE:** GraphQL types from the subgraph's schema.graphql are automatically included in this file.
dOrgJelli marked this conversation as resolved.
Show resolved Hide resolved

### Mutation Resolvers
`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 configuration properties created by the
// config generator functions below
const { ethereum, ipfs } = context.graph.config
dOrgJelli marked this conversation as resolved.
Show resolved Hide resolved

// 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 { mutationState } = context.graph
mutationState.addTransaction("tx_hash")
dOrgJelli marked this conversation as resolved.
Show resolved Hide resolved

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

// Configuration Setters
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
}
}

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
// Configuration Getters
config: {
ethereum: async () => {
const { ethereum } = (window as any)
await ethereum.enable()
return ethereum
},
ipfs: "http://localhost:5001",
customProperty: 5
}
})

// 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 === resolver's state
dOrgJelli marked this conversation as resolved.
Show resolved Hide resolved
const [exec, { loading, state }] = useMutation(
CREATE_ENTITY,
{
client,
variables: {
options: { name: "...", value: 5 }
}
}
)

// We can also utilize an optimistic response
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?**
We've been operating under this assumption while developing the prototype. We have since scrapped this requirement as it has proven nearly impossible to successfully transpile our own source, along with all our dependencies, into a single monolithic module. If anyone has experience doing this I would love chat!
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You'd think that it's pretty common and can be achieved with a transpiler and bundler, but I don't have much experience with that either. Bundling everything together into a single file at least should be no problem. Maybe ES2015 / ES6 is the better target nowadays?

Another question is the module system to use for exporting from the bundle (CommonJS, UMD, ...)?

@nenadjaja may have more experience with transpiling to older JS versions and bundling.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://www.tutorialspoint.com/babeljs/babeljs_transpile_es6_modules_to_es5.htm looks promising (webpack + babel with a single output file). I bet there are better guides out there though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks so much for this link, it was able to help me find a configuration that worked.

I've run into one last problem: building for a node environment and the browser requires different "target" types in the webpack config. I'm looking into solutions now, but I have a feeling we may end up needing to support something along the lines of:

resolvers:
  - kind: javascript/es5/node
    file: ./bundle-node/index.js
  - kind: javascript/es5/web
    file: ./bundle-web/index.js

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the error I receive when trying to use a "node bundle" within the browser:
image

This is because...

When running the code in the browser, there is no builtin require function. If you want to flag a module as external in the browser, you have to provide some other way to load it (put a script tag, or integrate an AMD library like require.js). Otherwise this external module cannot be imported.

Taken from this response: liady/webpack-node-externals#17 (comment)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure why there even is a need to import anything. It's a bundle after all. I think I'll need some hands-on time with the bundling to figure something out.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh that's very true, will investigate further.

- **How should the dApp configure the resolvers module?**
The dApp knows best how to: connect to the various web3 networks, handle key signature requests, and all other user / dApp specific things. We need a way for the dApp to configure the resolvers in a specific way given the resolver's requirements (IPFS provider, Web3 provider, etc).
dOrgJelli marked this conversation as resolved.
Show resolved Hide resolved
- **What paradigm should the resolver state follow?**
One option is to have the resolver's call into a single interface that modifys 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.
dOrgJelli marked this conversation as resolved.
Show resolved Hide resolved
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)