Last active
December 2, 2015 16:26
-
-
Save steveluscher/3abe6d6f598d70423f76 to your computer and use it in GitHub Desktop.
[WIP] New Relay Tutorial App
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class Comment extends React.Component { | |
render() { | |
var {comment} = this.props; | |
var {author} = comment; | |
var savePending = this.props.relay.hasOptimisticUpdate(this.props.comment); | |
return ( | |
<div style={{opacity: savePending ? 0.4 : null}}> | |
<img src={author.avatar} width={16} /> <strong>{author.name}</strong> {comment.text} | |
{savePending && | |
<img src="data:image/gif;base64,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" style={{marginLeft: 4, verticalAlign: 'text-top'}} /> | |
} | |
</div> | |
); | |
} | |
} | |
Comment = Relay.createContainer(Comment, { | |
fragments: { | |
comment: () => Relay.QL` | |
fragment on Comment { | |
author { avatar, name } | |
text | |
} | |
`, | |
}, | |
}); | |
class Story extends React.Component { | |
_augmentNumCommentsToShow = (delta) => { | |
this.props.relay.setVariables({ | |
numCommentsToShow: this.props.relay.variables.numCommentsToShow + delta, | |
}); | |
} | |
_handleKeyDown = (e) => { | |
if (e.keyCode === 13 && e.target.value != '') { // enter key | |
Relay.Store.update( | |
new AddCommentMutation({ | |
story: this.props.story, | |
text: e.target.value, | |
viewer: this.props.viewer, | |
}) | |
); | |
this._augmentNumCommentsToShow(1); | |
e.target.value = ''; | |
} | |
} | |
_handleMoreCommentsClick = (e) => { | |
e.preventDefault(); | |
this._augmentNumCommentsToShow(5); | |
} | |
render() { | |
var {story} = this.props; | |
var {author, comments} = story; | |
return ( | |
<div> | |
<header> | |
<img src={author.avatar} width={32} /> <strong>{author.name}</strong> | |
</header> | |
<p>{story.text}</p> | |
{comments.pageInfo.hasPreviousPage && | |
<a href="#" onClick={this._handleMoreCommentsClick}> | |
View previous comments | |
</a> | |
} | |
<ul> | |
{comments && comments.edges.map(commentEdge => | |
<li key={commentEdge.node.id}> | |
<Comment comment={commentEdge.node} /> | |
</li> | |
)} | |
<li> | |
<input | |
onKeyDown={this._handleKeyDown} | |
placeholder="Leave a comment…" | |
type="text" | |
/> | |
</li> | |
</ul> | |
</div> | |
); | |
} | |
} | |
Story = Relay.createContainer(Story, { | |
initialVariables: { | |
numCommentsToShow: 3, | |
}, | |
fragments: { | |
story: () => Relay.QL` | |
fragment on Story { | |
author { avatar, name } | |
comments(last: $numCommentsToShow) { | |
edges { | |
node { | |
id | |
${Comment.getFragment('comment')} | |
} | |
} | |
pageInfo { hasPreviousPage } | |
} | |
text | |
${AddCommentMutation.getFragment('story')} | |
} | |
`, | |
viewer: () => Relay.QL` | |
fragment on Viewer { | |
avatar | |
id | |
name | |
${AddCommentMutation.getFragment('viewer')} | |
} | |
`, | |
}, | |
}); | |
class StoriesApp extends React.Component { | |
_handleLoadMoreClick = () => { | |
this.props.relay.setVariables({ | |
numStoriesToLoad: this.props.relay.variables.numStoriesToLoad + 3, | |
}); | |
} | |
render() { | |
var {storyFeed} = this.props.viewer; | |
return ( | |
<div> | |
<ul> | |
{storyFeed.edges.map(edge => | |
<li key={edge.node.id}> | |
<Story | |
story={edge.node} | |
viewer={this.props.viewer} | |
/> | |
</li> | |
)} | |
</ul> | |
{storyFeed.pageInfo.hasNextPage && | |
<button onClick={this._handleLoadMoreClick}> | |
Load more stories | |
</button> | |
} | |
</div> | |
); | |
} | |
} | |
StoriesApp = Relay.createContainer(StoriesApp, { | |
initialVariables: { | |
numStoriesToLoad: 3, | |
}, | |
fragments: { | |
viewer: () => Relay.QL` | |
fragment on Viewer { | |
storyFeed(first: $numStoriesToLoad) { | |
edges { | |
node { | |
id | |
${Story.getFragment('story')}, | |
} | |
} | |
pageInfo { hasNextPage } | |
} | |
${Story.getFragment('viewer')} | |
} | |
`, | |
} | |
}); | |
class AddCommentMutation extends Relay.Mutation { | |
static fragments = { | |
story: () => Relay.QL` | |
fragment on Story { | |
id | |
} | |
`, | |
viewer: () => Relay.QL` | |
fragment on Viewer { | |
id | |
} | |
`, | |
}; | |
getCollisionKey() { | |
return `story-${this.props.story.id}`; | |
} | |
getMutation() { | |
return Relay.QL`mutation{addComment}`; | |
} | |
getFatQuery() { | |
return Relay.QL` | |
fragment on AddCommentPayload { | |
commentEdge | |
story { comments } | |
} | |
`; | |
} | |
getConfigs() { | |
return [{ | |
type: 'RANGE_ADD', | |
parentName: 'story', | |
parentID: this.props.story.id, | |
connectionName: 'comments', | |
edgeName: 'commentEdge', | |
rangeBehaviors: { | |
'': 'append', | |
}, | |
}]; | |
} | |
getVariables() { | |
return { | |
storyId: this.props.story.id, | |
text: this.props.text, | |
}; | |
} | |
getOptimisticResponse() { | |
return { | |
commentEdge: { | |
node: { | |
author: { | |
avatar: this.props.viewer.avatar, | |
id: this.props.viewer.id, | |
name: this.props.viewer.name, | |
}, | |
story: { | |
id: this.props.story.id, | |
}, | |
text: this.props.text, | |
}, | |
}, | |
}; | |
} | |
} | |
class StoriesRoute extends Relay.Route { | |
static params = {}; | |
static queries = { | |
viewer: (Component) => Relay.QL` | |
query ViewerQuery { | |
viewer { | |
${Component.getFragment('viewer')}, | |
} | |
} | |
`, | |
}; | |
static routeName = 'Stories'; | |
} | |
ReactDOM.render( | |
<Relay.RootContainer | |
Component={StoriesApp} | |
route={new StoriesRoute()} | |
/>, | |
mountNode | |
); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
GraphQLBoolean, | |
GraphQLEnumType, | |
GraphQLFloat, | |
GraphQLID, | |
GraphQLInputObjectType, | |
GraphQLInt, | |
GraphQLInterfaceType, | |
GraphQLList, | |
GraphQLNonNull, | |
GraphQLObjectType, | |
GraphQLSchema, | |
GraphQLString, | |
GraphQLUnionType, | |
} from 'graphql'; | |
import { | |
connectionArgs, | |
connectionDefinitions, | |
connectionFromArray, | |
cursorForObjectInConnection, | |
fromGlobalId, | |
globalIdField, | |
mutationWithClientMutationId, | |
nodeDefinitions, | |
toGlobalId, | |
} from 'graphql-relay'; | |
/** | |
* Set up some test data | |
*/ | |
var TUTORIAL_VERSION = 1; | |
class Comment { | |
constructor(data) { | |
this.authorId = data.authorId; | |
this.id = data.id; | |
this.storyId = data.storyId; | |
this.text = data.text; | |
} | |
} | |
class Person { | |
constructor(data) { | |
this.avatar = data.avatar; | |
this.id = data.id; | |
this.name = data.name; | |
} | |
} | |
class Story { | |
constructor(data) { | |
this.authorId = data.authorId; | |
this.id = data.id; | |
this.text = data.text; | |
} | |
} | |
class Viewer { | |
constructor(data) { | |
this.avatar = data.avatar; | |
this.id = data.id; | |
this.name = data.name; | |
} | |
} | |
var COMMENTS; | |
try { | |
COMMENTS = JSON.parse(localStorage.getItem(`relay-tutorial-${TUTORIAL_VERSION}-comments`)); | |
} catch(e) {} | |
if (COMMENTS == null) { | |
COMMENTS = [ | |
new Comment({id: '0', authorId: '2', storyId: '0', text: 'Yeah!'}), | |
new Comment({id: '1', authorId: '3', storyId: '1', text: 'OK!'}), | |
]; | |
} | |
var PEOPLE; | |
try { | |
PEOPLE = JSON.parse(localStorage.getItem(`relay-tutorial-${TUTORIAL_VERSION}-people`)); | |
} catch(e) {} | |
if (PEOPLE == null) { | |
PEOPLE = [ | |
new Person({id: '0', name: 'You', avatar: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABAAgMAAADXB5lNAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAMUExURcTM3/////v8+uDk6c+84MYAAACySURBVDjLpZLBFcIgEEQDPg4UYAn0kRI8ODEHD5RgCTaRftKE/ShidAee2aDc+G9nZ9mh634/9sx3g8gA6OnugQOBGzBwB7DGJSA1IYGjAFMCIFMF7DKY2VX62gyuDcCoFZWtPmkFzBfb/h+wYdLnkgcODjgJsC9z0UGVrU8grn0HHdh2SQU2vmVcqyg35vNOYxHtJ1y3gFgULF3CG2RnA3EuUvASOQKPvoHByIo0Szu4A87wlHkPruK3AAAAAElFTkSuQmCC'}), | |
new Person({id: '1', name: 'Steve', avatar: 'data:image/png;base64,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'}), | |
new Person({id: '2', name: 'Yuzhi', avatar: 'data:image/png;base64,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'}), | |
new Person({id: '3', name: 'Joe', avatar: 'data:image/png;base64,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'}), | |
new Person({id: '4', name: 'Tim', avatar: 'data:image/png;base64,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'}), | |
new Person({id: '5', name: 'Jan', avatar: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAADAUExURVN8tFaEumGQxI5ZPw0FB1yMwx0QDFuJvWCMwFV/tz8rJFF9uYJNOCYZFmGSy3hINoRTPW5ALjEjHZNfRkY0LptoUlN6rFA7NGpZVWE3KA4SHmFFOd60nFx+rbyQeW2Pu9Ooj6d0XsabgmBMR7GDbYZnWGJ8omt4kktCRYNdTG9QQVxjd+nBqZh2ZGyHrE1liRchNIp5b358iVcnGD4ZDi4zP5uGfnVsbfLKsrOflFJRX1dymz1ScI2NmSc9X/vcx86TuIcAAAkhSURBVFjDbJaHeqJaFIVBEBRFilQRpEoTe4smmfd/q7v2wZSZ724zkYzu/6xdgeNgEzKOc0JcOqHjcBNuPJk46/J4PVyvt9ux3O3ed2tnjG86+L7j0O+XTb4Ir7/H+JCfOM7YCdflx/V69Tzv2O627e4JtDMhj78A4/GY+4Ujzlgm7yf8P463zLJM73gsP0DYrcNxD+B+A3pNzpcQSAjDNbzL8lFlimS6iuU92lv7eH8v10/nXwAZ4g57fwDxMcSXx9tlvxBFUZVMxTt4VuVdxMWl3Dp90OO//B06lPLGsdO327L8OCiiKImiqeLH9TxLyirTqtotO9z5GwALASAB0I7Ty+PxqIgSTK1MUVVVSbFE01IUpWr/DzAmY3GR9rJ9tMebp0oMQG/wd03JzALFuryz7/0GjF9t4Mgs9o+Pm+ex1EsUAnMXkQaQskOmABAyCZPfxzPA9hmS//HgeYdDZqkiSwFyqJqmJJngqEqDajz7GMa/A2DF3623CP1wLZqgKAJTWiB2yqHUh4IfU1WqR4lkMdV/dxLE71pU7nY9NE3QNIqE01XEoahdlBfBWTqruJbMql2HrHcm45exDFLfHB+PG8QXRVEzf/UionzN2/3PZrnMA0WBAkm9VE9kcfzLiAb/4/H2sFw3COpDgYwToJKUouv+/Plz32yWUW0pJhK5uOwc5vajYEICkPzrregChO8qlDIEoCpuUXfL+2YZR2maN4FyPqvSfvdk0/MXAPo/3t6gviP5KpofFTBN0y2aIl5u4jTK864OAgsttXjfURP8CoEPS5SeIu+u1zpD3lzXVFDHs5QhouiNLOpsrQn801lVLy1mcsIzBf0Ahx8fnoXMI31FoOBgybRcNI5ouYHbdG9v1+sbRHRUHctU9xcAuMnwdT4HBdtbpSjIX5C5+AKVS1QsSfU8CWN4fTtgmvGWMoKrLBbvVEn+J4R1WZkYE9e1ECM1r4oGEEWPWvJ6KLKM9QSVJ2hqjOR+vwtf5R/zPM9xAKiKghoiffCnBkQe0BJYZ8UByUVVTDS15XlN03jWZfEeyt/+PBc+j17lkgAEz7rWFE3vcLsdrjfrcF8iBOyVBdpa8bLA86rL5TPkHZ75jxmgvHmZYlUKa3ucLkmWZXmNFzTF8q0OXFVcEEJcuFmWVdnl8zPsVwDPFDx3LQGQBjZ/Kq0gGLZHUV+78wkdCG8gALGyzMsq2gkjjn8Zt27bzHORRctqssBEHkz0Abru5PuabWtIi8oIJMOsHl77+Rxz8k8jtY9HBQCObIrOU6SzEuCcOjjDpFOyTBv4LXoCynGpHu0a6xN9wLMXhyFG/dGAgV/nReNTI7pBjSnAFHa+FqWdRGuFKUCRxUv5HA8nk+8QMIeBpVhm5eqJoNu6gZOVIo/+bGgQY/3URZ2C2tJOYekQF+gDbsb3Anju5gXIDPb2OZkaeNWnrgu6OL7DNvc410963vk4XBGVoLla4n6xeIYy/63g5rloItcS3eSsTQ3N0OKu7tLl5r6J4yixp1Mtye2iu57dQquvhbjY7xc7efwNsKgEFgCePbWnwimKN12Xp2wPpLktzAXNsBO7s3V9Oq1r77JY7MXPcPQNcDM3aCBBzIwkN3wDrlHRLQHYMAHzZD6dTv3TScCHWlEAsNgjiG9A5Qa0BDCC1yhH9e30nibRcok9lkaQlER5rflGYRsno278Zq8iCfvwVwgVAIpbAbDB5rHtaBnHzD/Ok7luR8Bofh1vIigwfBcCLuI7hSCzF6dkWROgERTR3dzv2KA4vfdHAPY8T9MoqX0tjyPfN/xAoQj2axnWE7gKTyCey24gxWZ5X8Z2jA5CBdIcgCSiTNaapicdCQjcPYqAeX6dL8tc62HssIlolfiCbQvTCD3I/BM7SaIoSoSp4fvn08kwjCDbrXe7kOd4+WVcW1WKy3a+qhiaNp0KlAQswYQBIGOuGyeaLcM/GZm33oYrmR8MfwNQBAwxNqE2Ra2nCQEonWSJjT6AANTRZwA5hBeG4RuwbR+4BykUQg+YCyn0owJzAWbPhSkI0AbE6ZQdoZ3nh9yPgu22xVLGMgOg0XVhKgh5/OUuzEmFjgLquq4ZJ/9R9sHzPzmQaSuTP8YeAPhNk5T5zwVdt5Oc0mhrJ02HkqwtX34/gJGz/lyoJEA0A3gAME9svQcIqGMaU0pqdIFeey11wPBbPwGG4/X7fsEAkg8AScf59NOHQBJwc50jjtort/I/xvGD1fMCCdgUkguAzUIno9/QQ0rqurZRiuyxHg3/BQxH/HbHHimxMwDI53CZ2z0ChrqwOlIp8ZgovwA/VRjK3Gr9fsGmkkSUYZ7nc535vgAgaJpGdcTItKsZS8BwOByNvqqAq235wIyq4hmAJE6mwsvmlEfUjzWCETRZS65MAACjVxXwH6ORs1tg/ZvGvLaX6RxFh6fQK0BzwR8pDPCENRp+mfx650YDovHOhR4qdbtO8EilT4mhv3RQDEbg+1a1Ww2+/EhBD8DxZPKObl7NvEjie4xFNMUKJAJpQQ4C16U7yog5ftlLAcWCf2GLRZFRCHcEodEKJWdgsKgDci+3K3k4G/1jDMAgq08AikKgfZxPv40yGFD429VqNBj8AGT2AoDOH0DPavXc4UlNiHFDSBPtlz/WSFvSl4aDwS/9PwoABno8m22Pdaen9w32IO1yGu5egFeu+FHv/V2GwWjwArAPBsPZYLg6Fp2QbjZplOaUg1cTQcB2MGA+BBiwV19KmSkgAYPhaDgDAGsdNwRIsHVKJGtCZGA1Gw1msyEOnTEbDmdMRV8FhngB6hprnR5N53ofAQ2C2w6gb0ieMjuMLvFGV9zgiyCPAOh6AFap0KcQNSTAajbrVcOPfAckgF1ygy+TB7PVRz7/r+gqWkEYhoFSGVYQ3INDhrqglMkezB6GD51j//9ZXq4tCxQKJddLe/SaIcCUA630dE6X0F5nta1JnpuVukFhRzxbRJESu2bozNX3JuVCACJ+i8gBGBg52WfmBQAKcZVG2JiV0NSMdAIAQLunEFIKXyZiaRsA4CW+wocAfODrnA8AtGuQ8q/XnpmqDwK6jYEdayX2LYc1358sn4bQ3i7TBO/9whPNV9dlHJfVAp3D0bs/jC5EG4jOzAQAAAAASUVORK5CYII='}), | |
]; | |
} | |
var STORIES; | |
try { | |
STORIES = JSON.parse(localStorage.getItem(`relay-tutorial-${TUTORIAL_VERSION}-stories`)); | |
} catch(e) {} | |
if (STORIES == null) { | |
STORIES = [ | |
new Story({id: '0', text: 'Everybody ready to publish a new version?', authorId: '1'}), | |
new Story({id: '1', text: 'Anyone want to grab lunch?', authorId: '2'}), | |
new Story({id: '2', text: 'I have a new idea; anyone want to grab a whiteboard and sketch it out?', authorId: '3'}), | |
new Story({id: '3', text: '#728131, #711151, and #817129 are fixed and landed.', authorId: '4'}), | |
new Story({id: '4', text: 'I\'m working on something that should increase developer efficiency. Stay tuned!', authorId: '5'}), | |
]; | |
} | |
var VIEWER = new Viewer(PEOPLE[0]); | |
/** | |
* Let Relay map between: | |
* - global IDs and the object they represent | |
* - objects and the GraphQL type associated with them | |
*/ | |
var {nodeInterface, nodeField} = nodeDefinitions( | |
(globalId) => { | |
var {type, id} = fromGlobalId(globalId); | |
if (type === 'Comment') { | |
return COMMENTS.find(obj => obj.id === id); | |
} else if (type === 'Person') { | |
return PEOPLE.find(obj => obj.id === id); | |
} else if (type === 'Story') { | |
return STORIES.find(obj => obj.id === id); | |
} else if (type === 'Viewer') { | |
return VIEWER; | |
} | |
return null; | |
}, | |
(obj) => { | |
if (obj instanceof Comment) { | |
return CommentType; | |
} else if (obj instanceof Person) { | |
return PersonType; | |
} else if (obj instanceof Story) { | |
return StoryType; | |
} else if (obj instanceof Viewer) { | |
return ViewerType; | |
} | |
return null; | |
} | |
); | |
/** | |
* Define an interface that all person-like objects will conform to | |
*/ | |
var PersonableInterface = new GraphQLInterfaceType({ | |
name: 'Personable', | |
fields: () => ({ | |
avatar: { | |
type: GraphQLString, | |
description: 'The URL of a person\'s avatar image', | |
}, | |
comments: { | |
type: CommentConnectionType, | |
description: 'Comments made on stories by this person', | |
args: connectionArgs, | |
resolve: (obj, args) => connectionFromArray( | |
COMMENTS.filter(comment => comment.authorId === obj.id), | |
args | |
), | |
}, | |
name: { type: GraphQLString }, | |
stories: { | |
type: StoryConnectionType, | |
description: 'Stories written by this person', | |
args: connectionArgs, | |
resolve: (obj, args) => connectionFromArray( | |
STORIES.filter(story => story.authorId === obj.id), | |
args | |
), | |
}, | |
}), | |
resolveType(obj) { | |
return obj instanceof Person ? PersonType : | |
obj instanceof Viewer ? ViewerType : | |
null; | |
}, | |
}); | |
/** | |
* Configure each type of object: Comments, People, Stories, and the Viewer | |
*/ | |
var CommentType = new GraphQLObjectType({ | |
name: 'Comment', | |
description: 'A comment on a story', | |
fields: () => ({ | |
author: { | |
type: PersonType, | |
description: 'The author of this comment', | |
resolve: (obj) => PEOPLE[obj.authorId], | |
}, | |
id: globalIdField('Comment'), | |
story: { | |
type: StoryType, | |
description: 'The story this comment is attached to', | |
resolve: () => STORIES[obj.storyId], | |
}, | |
text: { | |
type: GraphQLString, | |
}, | |
}), | |
interfaces: [nodeInterface], | |
}); | |
var PersonType = new GraphQLObjectType({ | |
name: 'Person', | |
description: 'A person who writes stories and comments', | |
fields: () => ({ | |
avatar: { | |
type: GraphQLString, | |
description: 'The URL of a person\'s avatar image', | |
}, | |
comments: { | |
type: CommentConnectionType, | |
description: 'Comments made on stories by this person', | |
args: connectionArgs, | |
resolve: (obj, args) => connectionFromArray( | |
COMMENTS.filter(comment => comment.authorId === obj.id), | |
args | |
), | |
}, | |
id: globalIdField('Person'), | |
name: { type: GraphQLString }, | |
stories: { | |
type: StoryConnectionType, | |
description: 'Stories written by this person', | |
args: connectionArgs, | |
resolve: (obj, args) => connectionFromArray( | |
STORIES.filter(story => story.authorId === obj.id), | |
args | |
), | |
}, | |
}), | |
interfaces: [nodeInterface, PersonableInterface], | |
}); | |
var StoryType = new GraphQLObjectType({ | |
name: 'Story', | |
description: 'A story written by a person', | |
fields: () => ({ | |
author: { | |
type: PersonType, | |
description: 'The author of this story', | |
resolve: (obj) => PEOPLE[obj.authorId], | |
}, | |
comments: { | |
type: CommentConnectionType, | |
description: 'Comments people have made on this story', | |
args: connectionArgs, | |
resolve: (obj, args) => connectionFromArray( | |
COMMENTS.filter(comment => comment.storyId === obj.id), | |
args | |
), | |
}, | |
id: globalIdField('Story'), | |
text: { | |
type: GraphQLString, | |
}, | |
}), | |
interfaces: [nodeInterface], | |
}); | |
var ViewerType = new GraphQLObjectType({ | |
name: 'Viewer', | |
description: 'The acting person (eg. the logged in visitor)', | |
fields: () => ({ | |
avatar: { | |
type: GraphQLString, | |
description: 'The URL of the viewer\'s avatar image', | |
}, | |
comments: { | |
type: CommentConnectionType, | |
description: 'Comments on stories, made by the viewer', | |
args: connectionArgs, | |
resolve: (obj, args) => connectionFromArray( | |
COMMENTS.filter(c => c.authorId === obj.id), | |
args | |
), | |
}, | |
id: globalIdField('Viewer'), | |
name: { type: GraphQLString }, | |
friends: { | |
type: PersonConnectionType, | |
description: 'Friends of the viewer', | |
args: connectionArgs, | |
resolve: (obj, args) => connectionFromArray( | |
PEOPLE.filter(p => p.id !== obj.id), | |
args | |
), | |
}, | |
stories: { | |
type: StoryConnectionType, | |
description: 'Stories written by the viewer', | |
args: connectionArgs, | |
resolve: (obj, args) => connectionFromArray( | |
STORIES.filter(story => story.authorId === obj.id), | |
args | |
), | |
}, | |
storyFeed: { | |
type: StoryConnectionType, | |
description: 'Stories visible to the viewer', | |
args: connectionArgs, | |
resolve: (obj, args) => connectionFromArray(STORIES, args), | |
}, | |
}), | |
interfaces: [nodeInterface, PersonableInterface], | |
}); | |
/** | |
* Set up the connections between types | |
*/ | |
var { | |
connectionType: CommentConnectionType, | |
edgeType: CommentEdgeType, | |
} = connectionDefinitions({ | |
name: 'Comment', | |
nodeType: CommentType, | |
}); | |
var { | |
connectionType: PersonConnectionType, | |
edgeType: PersonEdgeType, | |
} = connectionDefinitions({ | |
name: 'Person', | |
nodeType: PersonType, | |
}); | |
var { | |
connectionType: StoryConnectionType, | |
edgeType: StoryEdgeType, | |
} = connectionDefinitions({ | |
name: 'Story', | |
nodeType: StoryType, | |
}); | |
/** | |
* Configure a mutation to allow people to make comments | |
*/ | |
var AddCommentMutation = mutationWithClientMutationId({ | |
name: 'AddComment', | |
inputFields: { | |
storyId: { type: new GraphQLNonNull(GraphQLID) }, | |
text: { type: new GraphQLNonNull(GraphQLString) }, | |
}, | |
outputFields: { | |
commentEdge: { | |
type: CommentEdgeType, | |
resolve: ({localCommentId}) => { | |
var comment = COMMENTS.find((c) => c.id === localCommentId); | |
return { | |
cursor: cursorForObjectInConnection(COMMENTS, comment), | |
node: comment, | |
}; | |
}, | |
}, | |
story: { | |
type: StoryType, | |
resolve: ({storyId}) => STORIES.find(s => s.id === storyId), | |
}, | |
viewer: { | |
type: ViewerType, | |
resolve: () => VIEWER, | |
}, | |
}, | |
mutateAndGetPayload: async ({storyId, text}) => { | |
var localCommentId = COMMENTS.length; | |
COMMENTS.push({ | |
authorId: VIEWER.id, | |
id: localCommentId, | |
storyId: fromGlobalId(storyId, 'Story').id, | |
text, | |
}); | |
localStorage.setItem(`relay-tutorial-${TUTORIAL_VERSION}-comments`, JSON.stringify(COMMENTS)); | |
await new Promise(r => setTimeout(r, 500 + Math.random() * 1000)); | |
return {localCommentId, storyId}; | |
} | |
}); | |
/** | |
* Finally, configure the root Query and Mutation type | |
*/ | |
var QueryType = new GraphQLObjectType({ | |
name: 'Query', | |
fields: () => ({ | |
node: nodeField, | |
viewer: { | |
type: ViewerType, | |
resolve: () => VIEWER, | |
}, | |
}), | |
}); | |
var MutationType = new GraphQLObjectType({ | |
name: 'Mutation', | |
fields: { | |
addComment: AddCommentMutation, | |
}, | |
}); | |
export default new GraphQLSchema({ | |
query: QueryType, | |
mutation: MutationType, | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment