From 2ee268674368eaac1d22756cc26c630e5d70d988 Mon Sep 17 00:00:00 2001 From: dOrgJelli Date: Mon, 23 Dec 2019 21:36:36 -0600 Subject: [PATCH 01/52] init --- SUMMARY.md | 1 + rfcs/0003-mutations.md | 71 ++++++++++++++++++++++++++++++++++++++++++ rfcs/approved.md | 1 + 3 files changed, 73 insertions(+) create mode 100644 rfcs/0003-mutations.md diff --git a/SUMMARY.md b/SUMMARY.md index 58bdea5..261fe62 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -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) diff --git a/rfcs/0003-mutations.md b/rfcs/0003-mutations.md new file mode 100644 index 0000000..a8f8e90 --- /dev/null +++ b/rfcs/0003-mutations.md @@ -0,0 +1,71 @@ +# RFC-0003: Mutations + +
+
Author
+
dOrg: Jordan Ellis, Nestor Amesty
+ +
RFC pull request
+
URL
+ +
Date of submission
+
2019-12-20
+ +
Date of approval
+
YYYY-MM-DD
+ +
Approved by
+
First Person, Second Person
+
+ +## Contents + + + +## 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. + +## 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. + +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. + +## 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. + +## Terminology + +_Mutation_: GraphQL Mutation. +_Resolver_: Resolver function that's mapped to a mutation. +_Resolver State_: The resolver function's state (transactions sent, data logged, etc). +_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 + +This is the main section of the RFC. What does the proposal include? What are the proposed interfaces/APIs? How are different affected parties, such as users, developers or node operators affected by the change and how are they going to use it? + +TODO: design the developer flow, below it have a detailed spec of each aspect. + +## Compatibility + +Is this proposal backwards-compatible or is it a breaking change? If it is breaking, how could this be mitigated (think: migrations, announcing ahead of time like with hard forks, etc.)? + +TODO: no breaking changes will be introduced. This is an optional add-on to existing and new subgraphs. + +## Drawbacks and Risks + +Why might we _not_ want to do this? What cost would implementing this proposal incur? What risks would be introduced by going down this path? + +TODO: compat issues (ES5) + +## Alternatives + +What other designs have been considered, if any? For what reasons have they not been chosen? Are there workarounds that make this change less necessary? + +TODO: talk about existing alternatives, and how they're ineffecient. + +## Open Questions + +What are unresolved questions? diff --git a/rfcs/approved.md b/rfcs/approved.md index dae899b..abff125 100644 --- a/rfcs/approved.md +++ b/rfcs/approved.md @@ -1,3 +1,4 @@ # Approved RFCs - [RFC-0002: Ethereum Tracing Cache](./0002-ethereum-tracing-cache.md) +- [RFC-0003: Mutations](./0003-mutations.md) From 98f90390fb193582c63ed3a60b27a13ba7123988 Mon Sep 17 00:00:00 2001 From: dOrgJelli Date: Mon, 23 Dec 2019 21:37:52 -0600 Subject: [PATCH 02/52] newlines --- rfcs/0003-mutations.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rfcs/0003-mutations.md b/rfcs/0003-mutations.md index a8f8e90..13709f8 100644 --- a/rfcs/0003-mutations.md +++ b/rfcs/0003-mutations.md @@ -37,10 +37,10 @@ From a developer experience point of view I see this as urgent because it elimin ## Terminology -_Mutation_: GraphQL Mutation. -_Resolver_: Resolver function that's mapped to a mutation. -_Resolver State_: The resolver function's state (transactions sent, data logged, etc). -_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. +_Mutation_: GraphQL Mutation. +_Resolver_: Resolver function that's mapped to a mutation. +_Resolver State_: The resolver function's state (transactions sent, data logged, etc). +_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 From 3441108e085350a90be9363377cf37e63a2e94e3 Mon Sep 17 00:00:00 2001 From: dOrgJelli Date: Mon, 23 Dec 2019 23:18:32 -0600 Subject: [PATCH 03/52] beta --- rfcs/0003-mutations.md | 239 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 223 insertions(+), 16 deletions(-) diff --git a/rfcs/0003-mutations.md b/rfcs/0003-mutations.md index 13709f8..30b5bad 100644 --- a/rfcs/0003-mutations.md +++ b/rfcs/0003-mutations.md @@ -37,35 +37,242 @@ From a developer experience point of view I see this as urgent because it elimin ## Terminology -_Mutation_: GraphQL Mutation. -_Resolver_: Resolver function that's mapped to a mutation. -_Resolver State_: The resolver function's state (transactions sent, data logged, etc). -_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. +* _Mutation_: GraphQL Mutation. +* _Resolver_: Resolver function that's mapped to a mutation. +* _Resolver State_: The resolver function's state (transactions sent, data logged, etc). +* _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 -This is the main section of the RFC. What does the proposal include? What are the proposed interfaces/APIs? How are different affected parties, such as users, developers or node operators affected by the change and how are they going to use it? +### Mutation Manifest -TODO: design the developer flow, below it have a detailed spec of each aspect. +`subgraph.yaml` +```yaml +specVersion: ... +... +mutations: + specVersion: 0.0.1 + repository: https://npmjs.com/package/... + schema: + file: ./mutations/schema.graphql + resolvers: + kind: javascript + file: ./mutations/index.js +dataSources: ... +... +``` -## Compatibility +Alternatively, you can store the mutations manifest externally like so: +`subgraph.yaml` +```yaml +specVersion: ... +... +mutations: + file: ./mutations/mutations.yaml +dataSources: ... +... +``` +`mutations/mutations.yaml` +```yaml +specVersion: 0.0.1 +repository: https://npmjs.com/package/... +schema: + file: ./schema.graphql +resolvers: + kind: javascript + file: ./index.js +``` -Is this proposal backwards-compatible or is it a breaking change? If it is breaking, how could this be mitigated (think: migrations, announcing ahead of time like with hard forks, etc.)? +### Mutation Schema +`schema.graphql` +```graphql +type MyEntity @entity { + id: ID! + name: String! + value: BigInt! +} +``` -TODO: no breaking changes will be introduced. This is an optional add-on to existing and new subgraphs. +`mutations/schema.graphql` +```graphql +input MyEntityOptions { + name: String! + value: BigInt! +} -## Drawbacks and Risks +type Mutation { + createEntity( + options: MyEntityOptions! + ): MyEntity! -Why might we _not_ want to do this? What cost would implementing this proposal incur? What risks would be introduced by going down this path? + setEnityName( + entity: MyEntity! + name: String! + ): MyEntity! +} +``` +**NOTE:** GraphQL types from the subgraph's schema.graphql are automatically included in this file. -TODO: compat issues (ES5) +### Mutation Resolvers +`mutations/index.js` +```javascript +const resolvers = { + Mutation: { + async createEntity (_, args, context) { + // Extract mutation arguments + const { name, value } = args.options -## Alternatives + // Use configuration properties created by the + // config generator functions below + const { ethereum, ipfs } = context.graph.config + + // Fetch datasource addresses & abis + const { MyContract } = context.graph.datasources + await MyContract.abi + await MyContract.address + + // Modify a state object, which relays updates back + // to the subscribed dApp + const { mutationState } = context.graph + mutationState.addTransaction("tx_hash") + + ... + }, + 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 +} = require("@graphprotocol/mutations-ts") +const myMutations = require("mutations-js-module") + +const mutations = createMutations({ + mutations: myMutations, + subgraph: "my-subgraph", + node: "http://localhost:8080", + // 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 +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 +); -What other designs have been considered, if any? For what reasons have they not been chosen? Are there workarounds that make this change less necessary? +// 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 + } + } +` + +const [exec] = useMutation( + CREATE_ENTITY, + { + client, + variables: { + options: { name: "...", value: 5 } + } + } +) + +// Or alternatively, we can subscribe to resolver +// state updates, and also utilize an optimistic response +const [exec, { loading, state }] = useMutationAndSubscribe( + CREATE_ENTITY, + { + optimisticResponse: { + myEntity: { + id: "...", + name: "...", + value: 5, + } + }, + update(proxy, { data }) { + // result = data.myEntity + }, + onError(error) { + ... + }, + variables: { + options: { name: "...", value: 5 } + } + } +) + +// loading === boolean +// state === resolver's state +``` + +## 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. + +## Alternatives -TODO: talk about existing alternatives, and how they're ineffecient. +The existing alternative that protocol developers are creating for dApp developers has been described above. ## Open Questions -What are unresolved questions? +- **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! +- **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). From 03dc4a9d111a9c6672dea0053ce8aa483596d8fa Mon Sep 17 00:00:00 2001 From: dOrgJelli Date: Mon, 23 Dec 2019 23:27:45 -0600 Subject: [PATCH 04/52] resolver state question --- rfcs/0003-mutations.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rfcs/0003-mutations.md b/rfcs/0003-mutations.md index 30b5bad..0f44faf 100644 --- a/rfcs/0003-mutations.md +++ b/rfcs/0003-mutations.md @@ -276,3 +276,7 @@ The existing alternative that protocol developers are creating for dApp develope 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! - **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). +- **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. \ No newline at end of file From 2b31f7d56ad773c40c898251ea00503f923704fe Mon Sep 17 00:00:00 2001 From: dOrgJelli Date: Tue, 24 Dec 2019 01:41:51 -0600 Subject: [PATCH 05/52] useMutation modified --- rfcs/0003-mutations.md | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/rfcs/0003-mutations.md b/rfcs/0003-mutations.md index 0f44faf..9e028b3 100644 --- a/rfcs/0003-mutations.md +++ b/rfcs/0003-mutations.md @@ -168,7 +168,8 @@ export default { ```javascript const { createMutations, - createMutationsLink + createMutationsLink, + useMutation } = require("@graphprotocol/mutations-ts") const myMutations = require("mutations-js-module") @@ -220,7 +221,8 @@ const CREATE_ENTITY = gql` } ` -const [exec] = useMutation( +// state === resolver's state +const [exec, { loading, state }] = useMutation( CREATE_ENTITY, { client, @@ -230,9 +232,8 @@ const [exec] = useMutation( } ) -// Or alternatively, we can subscribe to resolver -// state updates, and also utilize an optimistic response -const [exec, { loading, state }] = useMutationAndSubscribe( +// We can also utilize an optimistic response +const [exec, { loading, state }] = useMutation( CREATE_ENTITY, { optimisticResponse: { @@ -253,9 +254,6 @@ const [exec, { loading, state }] = useMutationAndSubscribe( } } ) - -// loading === boolean -// state === resolver's state ``` ## Compatibility From ea8414113d57b25c3d473d121081ab9abd2cffc2 Mon Sep 17 00:00:00 2001 From: dOrgJelli Date: Fri, 10 Jan 2020 01:12:19 -0500 Subject: [PATCH 06/52] edits based on feedback --- rfcs/0003-mutations.md | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/rfcs/0003-mutations.md b/rfcs/0003-mutations.md index 9e028b3..07e0fe3 100644 --- a/rfcs/0003-mutations.md +++ b/rfcs/0003-mutations.md @@ -5,7 +5,7 @@
dOrg: Jordan Ellis, Nestor Amesty
RFC pull request
-
URL
+
URL
Date of submission
2019-12-20
@@ -23,23 +23,23 @@ ## 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. +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. ## 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. +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 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. +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 -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. +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 -* _Mutation_: GraphQL Mutation. -* _Resolver_: Resolver function that's mapped to a mutation. -* _Resolver State_: The resolver function's state (transactions sent, data logged, etc). +* _Mutation_: A GraphQL mutation. +* _Resolver_: Function that is used to execute a mutation. +* _Mutation State_: The state of a mutation being executed. * _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 @@ -62,7 +62,7 @@ dataSources: ... ... ``` -Alternatively, you can store the mutations manifest externally like so: +Alternatively, the mutation manifest can be external like so: `subgraph.yaml` ```yaml specVersion: ... @@ -232,7 +232,8 @@ const [exec, { loading, state }] = useMutation( } ) -// We can also utilize an optimistic response +// Optimistic responses can be used to update +// the UI before the execution has finished const [exec, { loading, state }] = useMutation( CREATE_ENTITY, { @@ -271,10 +272,10 @@ The existing alternative that protocol developers are creating for dApp develope ## Open Questions - **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! + 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. - **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). -- **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. + The dApp knows best how to: connect to the various web3 networks, handle key signature requests, and all other user / dApp specific things. Therefore a way for the dApp to configure the resolvers in a specific way is required. The resolvers are still responsible for defining what configuration options are required by the dApp (IPFS provider, Web3 provider, etc). +- **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. \ No newline at end of file From 20d975b673a0fd8025ee30f065ba3b82eb708cd6 Mon Sep 17 00:00:00 2001 From: dOrgJelli Date: Sat, 11 Jan 2020 01:06:28 -0500 Subject: [PATCH 07/52] changes based on feedback --- rfcs/0003-mutations.md | 71 ++++++++++++++++++++++++++++++++---------- 1 file changed, 54 insertions(+), 17 deletions(-) diff --git a/rfcs/0003-mutations.md b/rfcs/0003-mutations.md index 07e0fe3..95ec628 100644 --- a/rfcs/0003-mutations.md +++ b/rfcs/0003-mutations.md @@ -37,26 +37,39 @@ This is urgent from a developer experience point of view. With this addition, it ## Terminology +* _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. +* _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 -### Mutation Manifest +The sections below illustrate how a developer would add mutations to an existing subgraph, and add the mutations to a dApp. + +### Mutations Manifest + +The subgraph manifest (`subgraph.yaml`) now has an extra property `mutations` which is the mutations manifest. `subgraph.yaml` ```yaml specVersion: ... ... mutations: - specVersion: 0.0.1 repository: https://npmjs.com/package/... schema: file: ./mutations/schema.graphql resolvers: - kind: javascript + apiVersion: 0.0.1 + kind: javascript/es5 file: ./mutations/index.js dataSources: ... ... @@ -74,16 +87,18 @@ dataSources: ... ``` `mutations/mutations.yaml` ```yaml -specVersion: 0.0.1 repository: https://npmjs.com/package/... schema: file: ./schema.graphql resolvers: - kind: javascript + apiVersion: 0.0.1 + kind: javascript/es5 file: ./index.js ``` -### Mutation Schema +### 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 a base subgraph schema: `schema.graphql` ```graphql type MyEntity @entity { @@ -93,6 +108,7 @@ type MyEntity @entity { } ``` +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 { @@ -100,6 +116,11 @@ input MyEntityOptions { value: BigInt! } +interface NewNameSet { + oldName: String! + newName: String! +} + type Mutation { createEntity( options: MyEntityOptions! @@ -108,12 +129,20 @@ type Mutation { setEnityName( entity: MyEntity! name: String! - ): MyEntity! + ): NewNameSet! } ``` -**NOTE:** GraphQL types from the subgraph's schema.graphql are automatically included in this file. + +`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). ### 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. + +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: + * resolvers - The mutation resolver functions. + * config - A collection of config generators. + `mutations/index.js` ```javascript const resolvers = { @@ -122,8 +151,8 @@ const resolvers = { // Extract mutation arguments const { name, value } = args.options - // Use configuration properties created by the - // config generator functions below + // Use config properties created by the + // config generator functions const { ethereum, ipfs } = context.graph.config // Fetch datasource addresses & abis @@ -133,8 +162,8 @@ const resolvers = { // Modify a state object, which relays updates back // to the subscribed dApp - const { mutationState } = context.graph - mutationState.addTransaction("tx_hash") + const { state } = context.graph + state.addTransaction("tx_hash") ... }, @@ -144,7 +173,7 @@ const resolvers = { } } -// Configuration Setters +// Config generators const config = { // These function arguments are passed in by the dApp ethereum (provider) { @@ -155,6 +184,11 @@ const config = { }, customProperty (value) { return value + 2 + }, + rootProperty: { + nestedProperty (value) { + ... + } } } @@ -177,7 +211,7 @@ const mutations = createMutations({ mutations: myMutations, subgraph: "my-subgraph", node: "http://localhost:8080", - // Configuration Getters + // Config values, which will be passed to the generators config: { ethereum: async () => { const { ethereum } = (window as any) @@ -185,7 +219,10 @@ const mutations = createMutations({ return ethereum }, ipfs: "http://localhost:5001", - customProperty: 5 + customProperty: 5, + rootProperty: { + nestedProperty: "foo" + } } }) @@ -221,7 +258,7 @@ const CREATE_ENTITY = gql` } ` -// state === resolver's state +// state === mutation state const [exec, { loading, state }] = useMutation( CREATE_ENTITY, { From 31e3d30e56eb69a0a9098c978bb82a6f44612468 Mon Sep 17 00:00:00 2001 From: dOrgJelli Date: Sat, 11 Jan 2020 18:24:56 -0500 Subject: [PATCH 08/52] remove config question --- rfcs/0003-mutations.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/rfcs/0003-mutations.md b/rfcs/0003-mutations.md index 95ec628..adc5c6a 100644 --- a/rfcs/0003-mutations.md +++ b/rfcs/0003-mutations.md @@ -310,8 +310,7 @@ The existing alternative that protocol developers are creating for dApp develope - **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. -- **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. Therefore a way for the dApp to configure the resolvers in a specific way is required. The resolvers are still responsible for defining what configuration options are required by the dApp (IPFS provider, Web3 provider, etc). + - **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. From feee73fca83ab5614da346730c10dfcab45ec0b1 Mon Sep 17 00:00:00 2001 From: dOrgJelli Date: Sat, 11 Jan 2020 23:03:16 -0500 Subject: [PATCH 09/52] add subgraph name --- rfcs/0003-mutations.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rfcs/0003-mutations.md b/rfcs/0003-mutations.md index adc5c6a..3a12492 100644 --- a/rfcs/0003-mutations.md +++ b/rfcs/0003-mutations.md @@ -53,11 +53,11 @@ This is urgent from a developer experience point of view. With this addition, it ## Detailed Design -The sections below illustrate how a developer would add mutations to an existing subgraph, and add the mutations to a dApp. +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 `mutations` which is the mutations manifest. +The subgraph manifest (`subgraph.yaml`) now has an extra property named `mutations` which is the mutations manifest. `subgraph.yaml` ```yaml @@ -98,7 +98,7 @@ resolvers: ### 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 a base subgraph 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: `schema.graphql` ```graphql type MyEntity @entity { @@ -108,7 +108,7 @@ type MyEntity @entity { } ``` -Developers can define mutations that reference these subgraph schema types. Additionally new `input` and `interface` types can be defined for the mutations to use: +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 { From 4c61778737fe64fa78cc8e15170bf27d4deb36dd Mon Sep 17 00:00:00 2001 From: dOrgJelli Date: Mon, 20 Jan 2020 00:58:53 -0500 Subject: [PATCH 10/52] our -> the --- rfcs/0003-mutations.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rfcs/0003-mutations.md b/rfcs/0003-mutations.md index 3a12492..6414626 100644 --- a/rfcs/0003-mutations.md +++ b/rfcs/0003-mutations.md @@ -98,7 +98,7 @@ resolvers: ### 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: +The mutations schema defines all of the mutations in the subgraph. The mutations schema is a super-set of the subgraph's schema. For example, starting from a base subgraph schema: `schema.graphql` ```graphql type MyEntity @entity { From f242ee3a16be57b9eb1761c8c7934ba891e17d55 Mon Sep 17 00:00:00 2001 From: dOrgJelli Date: Mon, 20 Jan 2020 01:01:05 -0500 Subject: [PATCH 11/52] mutations schema builds upon... --- rfcs/0003-mutations.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rfcs/0003-mutations.md b/rfcs/0003-mutations.md index 6414626..4adc601 100644 --- a/rfcs/0003-mutations.md +++ b/rfcs/0003-mutations.md @@ -98,7 +98,7 @@ resolvers: ### Mutations Schema -The mutations schema defines all of the mutations in the subgraph. The mutations schema is a super-set of the subgraph's schema. For example, starting from a base subgraph schema: +The mutations schema defines all of the mutations in the subgraph. The mutations schema builds on the subgraph schema, allowing the use of types from the subgraph schema, as well as defining new types that are used only in the context of mutations. For example, starting from a base subgraph schema: `schema.graphql` ```graphql type MyEntity @entity { From f272b34b5ef5db2ac74c5d8b8e875f8f5069f0f0 Mon Sep 17 00:00:00 2001 From: dOrgJelli Date: Mon, 20 Jan 2020 01:02:08 -0500 Subject: [PATCH 12/52] setEnity -> setEntity --- rfcs/0003-mutations.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rfcs/0003-mutations.md b/rfcs/0003-mutations.md index 4adc601..35c147c 100644 --- a/rfcs/0003-mutations.md +++ b/rfcs/0003-mutations.md @@ -126,7 +126,7 @@ type Mutation { options: MyEntityOptions! ): MyEntity! - setEnityName( + setEntityName( entity: MyEntity! name: String! ): NewNameSet! From 6272d9a7d8abd33ace96d6d0bc7b9973afd939be Mon Sep 17 00:00:00 2001 From: dOrgJelli Date: Mon, 20 Jan 2020 01:03:46 -0500 Subject: [PATCH 13/52] graph-cli description grammar --- rfcs/0003-mutations.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rfcs/0003-mutations.md b/rfcs/0003-mutations.md index 35c147c..d3d0143 100644 --- a/rfcs/0003-mutations.md +++ b/rfcs/0003-mutations.md @@ -133,7 +133,7 @@ type Mutation { } ``` -`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). +`graph-cli` handles the parsing and validating of these two schemas. It verifies that the mutations schema defines a `type Mutation` and that all of the mutations within it are defined in the resolvers module (see next section). ### Mutation Resolvers From 7ed7b9c4e591229459fd7b8fb396ec3149151222 Mon Sep 17 00:00:00 2001 From: dOrgJelli Date: Mon, 20 Jan 2020 01:04:22 -0500 Subject: [PATCH 14/52] executes the mutation query --- rfcs/0003-mutations.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rfcs/0003-mutations.md b/rfcs/0003-mutations.md index d3d0143..a53ba1b 100644 --- a/rfcs/0003-mutations.md +++ b/rfcs/0003-mutations.md @@ -137,7 +137,7 @@ type Mutation { ### 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. +Each mutation within the schema must have a corresponding resolver function defined. Resolvers will be invoked by whatever engine executes the mutation queries. They are executed locally within the client application. 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: * resolvers - The mutation resolver functions. From d9ceb40ee41f5f6d4aafab9ab3afb2f3b4a8fbfb Mon Sep 17 00:00:00 2001 From: dOrgJelli Date: Mon, 20 Jan 2020 01:05:24 -0500 Subject: [PATCH 15/52] javascript/es5 --- rfcs/0003-mutations.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rfcs/0003-mutations.md b/rfcs/0003-mutations.md index a53ba1b..42f89f9 100644 --- a/rfcs/0003-mutations.md +++ b/rfcs/0003-mutations.md @@ -139,7 +139,7 @@ type Mutation { Each mutation within the schema must have a corresponding resolver function defined. Resolvers will be invoked by whatever engine executes the mutation queries. They are executed locally within the client application. -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: +Mutation resolvers of kind `javascript/es5` take the form of an ES5 javascript module. This module is expected to have a default export that contains the following properties: * resolvers - The mutation resolver functions. * config - A collection of config generators. From 6a14b59149ad4e2e5071409ce17471e52bee2d37 Mon Sep 17 00:00:00 2001 From: dOrgJelli Date: Mon, 20 Jan 2020 01:06:07 -0500 Subject: [PATCH 16/52] inline code resolvers & config --- rfcs/0003-mutations.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rfcs/0003-mutations.md b/rfcs/0003-mutations.md index 42f89f9..9fe7dae 100644 --- a/rfcs/0003-mutations.md +++ b/rfcs/0003-mutations.md @@ -140,8 +140,8 @@ type Mutation { Each mutation within the schema must have a corresponding resolver function defined. Resolvers will be invoked by whatever engine executes the mutation queries. They are executed locally within the client application. Mutation resolvers of kind `javascript/es5` take the form of an ES5 javascript module. This module is expected to have a default export that contains the following properties: - * resolvers - The mutation resolver functions. - * config - A collection of config generators. + * `resolvers` - The mutation resolver functions. + * `config` - A collection of config generators. `mutations/index.js` ```javascript From 1c53960fab39157198ae4590bf15c5a292c9f2d7 Mon Sep 17 00:00:00 2001 From: dOrgJelli Date: Mon, 20 Jan 2020 22:01:47 -0500 Subject: [PATCH 17/52] remove datasource api --- rfcs/0003-mutations.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/rfcs/0003-mutations.md b/rfcs/0003-mutations.md index 9fe7dae..e9818d3 100644 --- a/rfcs/0003-mutations.md +++ b/rfcs/0003-mutations.md @@ -155,11 +155,6 @@ const resolvers = { // config generator functions const { ethereum, ipfs } = context.graph.config - // Fetch datasource addresses & abis - const { MyContract } = context.graph.datasources - await MyContract.abi - await MyContract.address - // Modify a state object, which relays updates back // to the subscribed dApp const { state } = context.graph From 4e96fa8b73ee1b3e5142733981dcf8b7a0511456 Mon Sep 17 00:00:00 2001 From: dOrgJelli Date: Mon, 20 Jan 2020 22:03:39 -0500 Subject: [PATCH 18/52] package split --- rfcs/0003-mutations.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rfcs/0003-mutations.md b/rfcs/0003-mutations.md index e9818d3..1e82c46 100644 --- a/rfcs/0003-mutations.md +++ b/rfcs/0003-mutations.md @@ -197,9 +197,9 @@ export default { ```javascript const { createMutations, - createMutationsLink, - useMutation + createMutationsLink } = require("@graphprotocol/mutations-ts") +const { useMutation } = require("@graphprotocol/mutations-apollo-react") const myMutations = require("mutations-js-module") const mutations = createMutations({ From 9ff22b0662e2216ca3a9a36b3f9ac9639511f85c Mon Sep 17 00:00:00 2001 From: dOrgJelli Date: Mon, 20 Jan 2020 22:04:51 -0500 Subject: [PATCH 19/52] remove node + subgraph --- rfcs/0003-mutations.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/rfcs/0003-mutations.md b/rfcs/0003-mutations.md index 1e82c46..22a1ec6 100644 --- a/rfcs/0003-mutations.md +++ b/rfcs/0003-mutations.md @@ -204,8 +204,6 @@ const myMutations = require("mutations-js-module") const mutations = createMutations({ mutations: myMutations, - subgraph: "my-subgraph", - node: "http://localhost:8080", // Config values, which will be passed to the generators config: { ethereum: async () => { From 473952f72d04a102692e6eb1c25fbd2e51ac06c7 Mon Sep 17 00:00:00 2001 From: dOrgJelli Date: Mon, 20 Jan 2020 22:05:50 -0500 Subject: [PATCH 20/52] apollo link comment --- rfcs/0003-mutations.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rfcs/0003-mutations.md b/rfcs/0003-mutations.md index 22a1ec6..bb5ec90 100644 --- a/rfcs/0003-mutations.md +++ b/rfcs/0003-mutations.md @@ -219,7 +219,7 @@ const mutations = createMutations({ } }) -// Create an Apollo Links +// Create Apollo links to handle queries and mutation queries const mutationLink = createMutationLink({ mutations }) const queryLink = createHttpLink({ uri: "http://localhost:5001/subgraphs/name/my-subgraph" From 67d7f0ab5047dc6d81b4e5fc2eb03d8d9cf60841 Mon Sep 17 00:00:00 2001 From: dOrgJelli Date: Tue, 21 Jan 2020 13:08:35 -0500 Subject: [PATCH 21/52] optimistic response update --- rfcs/0003-mutations.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/rfcs/0003-mutations.md b/rfcs/0003-mutations.md index bb5ec90..52a998d 100644 --- a/rfcs/0003-mutations.md +++ b/rfcs/0003-mutations.md @@ -263,23 +263,23 @@ const [exec, { loading, state }] = useMutation( ) // Optimistic responses can be used to update -// the UI before the execution has finished +// the UI before the execution has finished. +// More information can be found here: +// https://www.apollographql.com/docs/react/performance/optimistic-ui/ const [exec, { loading, state }] = useMutation( CREATE_ENTITY, { optimisticResponse: { - myEntity: { - id: "...", + __typename: "Mutation", + createEntity: { + __typename: "MyEntity", name: "...", value: 5, + // NOTE: ID must be known so the + // final response can be correlated + id: "id" } }, - update(proxy, { data }) { - // result = data.myEntity - }, - onError(error) { - ... - }, variables: { options: { name: "...", value: 5 } } From 6c5c3853abe3bb0c847af874034449dcba8733a6 Mon Sep 17 00:00:00 2001 From: dOrgJelli Date: Tue, 21 Jan 2020 13:11:34 -0500 Subject: [PATCH 22/52] use mutation comment --- rfcs/0003-mutations.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rfcs/0003-mutations.md b/rfcs/0003-mutations.md index 52a998d..7a8e880 100644 --- a/rfcs/0003-mutations.md +++ b/rfcs/0003-mutations.md @@ -251,7 +251,9 @@ const CREATE_ENTITY = gql` } ` -// state === mutation state +// exec: execution function for the mutation query +// loading: https://www.apollographql.com/docs/react/data/mutations/#tracking-mutation-status +// state: mutation state instance const [exec, { loading, state }] = useMutation( CREATE_ENTITY, { From 8cb2decd7208fa388d4edd1eba7b8a4e47d73fc1 Mon Sep 17 00:00:00 2001 From: dOrgJelli Date: Wed, 22 Jan 2020 19:43:53 -0500 Subject: [PATCH 23/52] definitions --- rfcs/0003-mutations.md | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/rfcs/0003-mutations.md b/rfcs/0003-mutations.md index 7a8e880..00a345b 100644 --- a/rfcs/0003-mutations.md +++ b/rfcs/0003-mutations.md @@ -27,7 +27,7 @@ GraphQL mutations allow developers to add executable functions to their schema. ## 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. +The Graph has created a read semantic layer that describes smart contract protocols, which has made it easier to build applications on top 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. @@ -39,13 +39,27 @@ This is urgent from a developer experience point of view. With this addition, it * _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. +* _Mutations Schema_: A GraphQL schema that defines a `type Mutation`, which 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. This manifest can be stored in an external YAML file, or within the subgraph manifest's YAML file under the `mutations` property. * _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. +* _Resolver_: Function that is used to execute a mutation's logic. +* _Mutation Context_: A context object that's created for every mutation that's executed. It's passed as the 3rd argument to the resolver function. +* _Mutation States_: A collection of mutation states. One is created for each mutation being executed in a given query. +* _Mutation State_: The state of a mutation being executed. Also referred to in this document as "_State_". It is an aggregate of the core & ext states (see below). dApp developers can subscribe to the mutation's state upon execution of the mutation query. See the `useMutation` examples below. +* _Core State_: Default properties present within every mutation state. Some examples: `events: Event[]`, `uuid: string`, and `progress: number`. +* _Ext State_: Properties the mutation developer defines. These are added alongside the core state properties in the mutation state. There are no bounds to what a developer can define here. See examples below. +* _State Events_: Events emitted by mutation resolvers. Also referred to in this document as "_Events_". Events are defined by a `key: string` and a `payload: any`. These events, once emitted, are given to reducer functions which then update the state accordingly. +* _Core Events_: Default events available to all mutations. Some examples: `PROGRESS_UPDATE`, `TRANSACTION_CREATED`, `TRANSACTION_COMPLETED`. +* _Ext Events_: Events the mutation developer defines. See examples below. +* _State Reducers_: A collection of state reducer functions. +* _State Reducer_: Reducers are responsible for translating events into state updates. They take the form of a function that has the inputs [event, current state], and returns the new state post-event. Also referred to in this document as "_Reducer(s)_". +* _Core Reducers_: Default reducers that handle the processing of the core events. +* _Ext Reducers_: Reducers the mutation developer defines. These reducers can be defined for any event, core or ext. The core & ext reducers are run one after another if both are defined for a given core event. See examples below. +* _State Updater_: The state updater object is used by the resolvers to dispatch events. It's passed to the resolvers through the mutation context like so: `context.graph.state`. +* _State Builder_: An object responsible for (1) initializing the state with initial values and (2) defining reducers for events. +* _Core State Builder_: A state builder that's defined by default. It's responsible for initializing the core state properties, and processing the core events with its reducers. +* _Ext State Builder_: A state builder defined by the mutation developer. It's responsible for initializing the ext state properties, and processing the ext events with its reducers. +* _Mutations Config_: Collection of config properties required by the mutation resolvers. Also referred to in this document as "_Config_". All resolvers share the same config. It's passed to the resolver through the mutation context like so: `context.graph.config`. * _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. @@ -53,7 +67,7 @@ This is urgent from a developer experience point of view. With this addition, it ## Detailed Design -The sections below illustrate how a developer would add mutations to an existing subgraph, and then add those mutations to a dApp. +The sections below illustrate how a developer would add mutations to an existing subgraph, and then add those mutations to a dApp. ### Mutations Manifest From 83adcd36e7919a3192e09e16e29410fc12856977 Mon Sep 17 00:00:00 2001 From: dOrgJelli Date: Wed, 22 Jan 2020 22:01:22 -0500 Subject: [PATCH 24/52] mutation resolvers module types + new state interface --- rfcs/0003-mutations.md | 196 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 176 insertions(+), 20 deletions(-) diff --git a/rfcs/0003-mutations.md b/rfcs/0003-mutations.md index 00a345b..e9f8ac9 100644 --- a/rfcs/0003-mutations.md +++ b/rfcs/0003-mutations.md @@ -85,6 +85,7 @@ mutations: apiVersion: 0.0.1 kind: javascript/es5 file: ./mutations/index.js + types: ./mutations/index.d.ts dataSources: ... ... ``` @@ -108,8 +109,11 @@ resolvers: apiVersion: 0.0.1 kind: javascript/es5 file: ./index.js + types: ./index.d.ts ``` +NOTE: `resolvers.types` is optional, and is only required if the resolvers' module exports a state builder. More on this below. + ### Mutations Schema The mutations schema defines all of the mutations in the subgraph. The mutations schema builds on the subgraph schema, allowing the use of types from the subgraph schema, as well as defining new types that are used only in the context of mutations. For example, starting from a base subgraph schema: @@ -151,17 +155,99 @@ type Mutation { ### Mutation Resolvers -Each mutation within the schema must have a corresponding resolver function defined. Resolvers will be invoked by whatever engine executes the mutation queries. They are executed locally within the client application. +Each mutation within the schema must have a corresponding resolver function defined. Resolvers will be invoked by whatever engine executes the mutation queries (ex: Apollo Client). They are executed locally within the client application. Mutation resolvers of kind `javascript/es5` take the form of an ES5 javascript module. This module is expected to have a default export that contains the following properties: - * `resolvers` - The mutation resolver functions. - * `config` - A collection of config generators. + * `resolvers` - The mutation resolver functions. The shape of this object must match the shape of the `type Mutation` defined above. See the example below for demonstration of this. Resolvers have the following prototype, [as defined in graphql-js](https://github.com/graphql/graphql-js/blob/9dba58eeb6e28031bec7594b6df34c4fd74459b0/src/type/definition.js#L906): + ```typescript + type GraphQLFieldResolver< + TSource, + TContext, + TArgs = { [argument: string]: any, ... }, + > = ( + source: TSource, + args: TArgs, + context: TContext, + info: GraphQLResolveInfo, + ) => mixed; + + interface MutationResolvers { + Mutation: { + [field: string]: GraphQLFieldResolver; + }; + } + ``` + * `config` - A collection of config generators. The config object is made up of properties, that can be nested, but all terminate in the form of a function with the prototype: + ```typescript + type ConfigGenerator = (value: TInput) => TOutput + ``` + See the example below for a demonstration of this. + + * `stateBuilder` (optional) - A state builder interface responsible for (1) initializing ext state properties and (2) reducing ext state events. State builders implement the following interface: + ```typescript + type MutationState = CoreState & TState + type MutationEvents = CoreEvents & TEventMap + + interface StateBuilder { + getInitialState(uuid: string): TState, + // Event Specific Reducers + reducers?: { + [TEvent in keyof MutationEvents]?: ( + state: MutationState, + payload: InferEventPayload + ) => Promise> + }, + // Catch-All Reducer + reducer?: ( + state: MutationState, + event: string, + payload: any + ) => Promise>, + } + + interface EventPayload { } + + interface Event { + name: string + payload: EventPayload + } + + interface EventTypeMap { + [name: string]: EventPayload + } + + type InferEventPayload< + TEvent extends keyof TEvents, + TEvents extends EventTypeMap + > = + TEvent extends keyof TEvents ? TEvents[TEvent] : + any + ``` + See the example below for a demonstration of this. +For example: `mutations/index.js` -```javascript -const resolvers = { +```typescript +import gql from "graphql-tag" +import { + ethers, + AsyncSendable, + Web3Provider +} from "ethers" +import IPFS from "ipfs" +import { + MutationResolvers, + ConfigGenerators, + Event, + EventPayload, + EventTypeMap, + ProgressUpdateEvent +} from "@graphprotocol/mutations-ts" + +/// Mutation Resolvers +const resolvers: MutationResolvers = { Mutation: { - async createEntity (_, args, context) { + async createEntity (source: any, args: any, context: any) { // Extract mutation arguments const { name, value } = args.options @@ -169,44 +255,114 @@ const resolvers = { // config generator functions const { ethereum, ipfs } = context.graph.config - // Modify a state object, which relays updates back - // to the subscribed dApp + // Create ethereum transactions... + // Fetch & upload to ipfs... + + // Dispatch a state event through the state updater const { state } = context.graph - state.addTransaction("tx_hash") + state.dispatch("PROGRESS_UPDATE", { progress: 0.5 }) + + // Dispatch a custom ext event + state.dispatch("MY_EVENT", { myValue: "..." }) + + // Send another query using the same client. + // This query would result in the graph-node's + // entity store being fetched from. You could also + // execute another mutation here if desired. + const { client } = context + await client.query({ + query: gql` + myEntity (id: "${id}") { + id + name + value + } + }` + }) ... }, - async setEntityName (_, args, context) { + async setEntityName (source: any, args: any, context: any) { ... } } } -// Config generators -const config = { +/// Config Generators +const config: ConfigGenerators = { // These function arguments are passed in by the dApp - ethereum (provider) { + ethereum (provider: AsyncSendable): Web3Provider { return new ethers.providers.Web3Provider(provider) }, - ipfs (provider) { + ipfs (provider: string): IPFS { return new IPFS(provider) }, - customProperty (value) { - return value + 2 + // Example of a custom config property + property: { + // Generators can be nested + a: (value: string) => { }, + b: (value: string) => { } + } +} + +/// (optional) Ext State, Events, and State Builder + +// Ext State +interface State { + myValue: string +} + +// Ext Events +interface MyEvent extends EventPayload { + myValue: string +} + +interface EventMap extends EventTypeMap { + "MY_EVENT": MyEvent +} + +// Ext State Builder +const stateBuilder: StateBuilder = { + getInitialState(): State { + return { + myValue: "" + } + }, + reducers: { + "MY_EVENT": (state: MutationState, payload: MyEvent) => { + return { + myValue: payload.myValue + } + }, + "PROGRESS_UPDATE": (state: MutationState, payload: ProgressUpdateEvent) => { + // Do something custom... + } }, - rootProperty: { - nestedProperty (value) { - ... + // Catch-all reducer... + reducer: (state: MutationState, event: Event) => { + switch (event.name) { + case "TRANSACTION_CREATED": + // Do something custom... + break; } } } export default { resolvers, - config + config, + State, + MyEvent, + EventMap, + stateBuilder } ``` +NOTE: If the resolvers module exports a `stateBuilder`, it's expected that the mutations manifest has a `resolvers.types` file defined. The following types are expected to be defined in the .d.ts type definition file: + - `State` + - `EventMap` + - Any `EventPayload` interfaces defined within the `EventMap` + ### dApp Integration ```javascript const { From b64ad77dc8f19186a8e4000215eb86f52ef5ac1e Mon Sep 17 00:00:00 2001 From: dOrgJelli Date: Thu, 23 Jan 2020 00:20:18 -0500 Subject: [PATCH 25/52] API types + typescript dApp --- rfcs/0003-mutations.md | 148 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 133 insertions(+), 15 deletions(-) diff --git a/rfcs/0003-mutations.md b/rfcs/0003-mutations.md index e9f8ac9..f5636ba 100644 --- a/rfcs/0003-mutations.md +++ b/rfcs/0003-mutations.md @@ -265,6 +265,9 @@ const resolvers: MutationResolvers = { // Dispatch a custom ext event state.dispatch("MY_EVENT", { myValue: "..." }) + // Get a copy of the current state + const currentState = state.current + // Send another query using the same client. // This query would result in the graph-node's // entity store being fetched from. You could also @@ -280,6 +283,11 @@ const resolvers: MutationResolvers = { }` }) + // Fetch datasource addresses & abis from the subgraph manifest + const { MyContract } = context.graph.datasources + await MyContract.abi + await MyContract.address + ... }, async setEntityName (source: any, args: any, context: any) { @@ -364,37 +372,138 @@ NOTE: If the resolvers module exports a `stateBuilder`, it's expected that the m - Any `EventPayload` interfaces defined within the `EventMap` ### dApp Integration -```javascript -const { + +In addition to the resolvers module defined above, the dApp has access to a run-time API to help with the instantiation and execution of mutations. This package is called `@graphprotocol/mutations-ts` and is defined like so: + - `createMutations` - Create a mutations interface which enables the user to `execute` a mutation query and `configure` the mutation module. + ```typescript + interface CreateMutationsOptions< + TState, + TEventMap extends EventTypeMap, + TConfig extends ConfigGenerators + > { + mutations: MutationsModule, + subgraph: string, + node: string, + config: ConfigGetters + mutationExecutor?: MutationExecutor + } + + interface Mutations { + execute: (query: MutationQuery) => Promise + configure: (config: ConfigGetters) => void + } + + const createMutations = < + TState, + TEventMap extends EventTypeMap, + TConfig extends ConfigGenerators + >( + options: CreateMutationsOptions + ): Mutations => { ... } + ``` + + - `createMutationsLink` - wrap the mutations created above in an ApolloLink. + ```typescript + const createMutationsLink = ( + options: { mutations: Mutations } + ): ApolloLink => { ... } + ``` + +For applications using Apollo and React, a run-time API is available which mimics commonly used hooks and components for executing mutations, with the addition of having the mutation state available to the caller. This package is called `@graphprotocol/mutations-apollo-react` and is defined like so: + - `useMutation` - see https://www.apollographql.com/docs/react/data/mutations/#executing-a-mutation + ```typescript + import { DocumentNode } from "graphql" + import { + ExecutionResult, + MutationFunctionOptions, + MutationResult, + OperationVariables + } from "@apollo/react-common" + import { MutationHookOptions } from "@apollo/react-hooks" + import { CoreState } from "@graphprotocol/mutations-ts" + + interface MutationResultWithState extends MutationResult { + state: TState + } + + type MutationTupleWithState = [ + ( + options?: MutationFunctionOptions + ) => Promise>, + MutationResultWithState + ]; + + const useMutation = < + TState = CoreState, + TData = any, + TVariables = OperationVariables + >( + mutation: DocumentNode, + mutationOptions: MutationHookOptions + ): MutationTupleWithState => { ... } + ``` + - `Mutation` - see https://www.howtographql.com/react-apollo/3-mutations-creating-links/ + ```typescript + interface MutationComponentOptions< + TData = any, + TVariables = OperationVariables + > extends BaseMutationOptions { + mutation: DocumentNode + children: ( + mutateFunction: MutationFunction, + result: MutationResult + ) => JSX.Element | null + } + + const Mutation = ( + props: MutationComponentOptions + ): JSX.Element | null => { ... } + ``` + +For example: +`dApp/src/App.tsx` +```typescript +import { createMutations, - createMutationsLink -} = require("@graphprotocol/mutations-ts") -const { useMutation } = require("@graphprotocol/mutations-apollo-react") -const myMutations = require("mutations-js-module") + createMutationsLink, + MutationState +} from "@graphprotocol/mutations-ts" +import { + Mutation, + useMutation +} from "@graphprotocol/mutations-apollo-react" +import myMutations, { State } from "mutations-js-module" +import { createHttpLink } from "apollo-link-http" const mutations = createMutations({ mutations: myMutations, // Config values, which will be passed to the generators config: { - ethereum: async () => { + // Config values can take the form of functions to allow + // for dynamic fetching behavior + ethereum: async (): AsyncSendable => { const { ethereum } = (window as any) await ethereum.enable() return ethereum }, ipfs: "http://localhost:5001", - customProperty: 5, - rootProperty: { - nestedProperty: "foo" + property: { + a: "...", + b: "..." } - } + }, + subgraph: "my-subgraph", + node: "http://localhost:8080" }) // Create Apollo links to handle queries and mutation queries const mutationLink = createMutationLink({ mutations }) const queryLink = createHttpLink({ - uri: "http://localhost:5001/subgraphs/name/my-subgraph" + uri: "http://localhost:8080/subgraphs/name/my-subgraph" }) +// Create a root ApolloLink which splits queries between +// the two different operation links (query & mutation) const link = split( ({ query }) => { const node = getMainDefinition(query); @@ -403,9 +512,9 @@ const link = split( }, mutationLink, queryLink -); +) -// Create Apollo Client +// Create an Apollo Client const client = new ApolloClient({ link, cache: new InMemoryCache() @@ -424,7 +533,7 @@ const CREATE_ENTITY = gql` // exec: execution function for the mutation query // loading: https://www.apollographql.com/docs/react/data/mutations/#tracking-mutation-status // state: mutation state instance -const [exec, { loading, state }] = useMutation( +const [exec, { loading, state }] = useMutation>( CREATE_ENTITY, { client, @@ -458,6 +567,15 @@ const [exec, { loading, state }] = useMutation( } ) ``` +```html +// Use the Mutation JSX Component + +{(exec, { loading, state }) => ( +