diff --git a/gql-schema/schema.graphql b/gql-schema/schema.graphql index d37d36f..879fa44 100644 --- a/gql-schema/schema.graphql +++ b/gql-schema/schema.graphql @@ -1,5 +1,31 @@ -schema { - query: TwitterAPI +# An object with an ID +interface Node { + # The id of the object. + id: ID! +} + +# Information about pagination in a connection. +type PageInfo { + # When paginating forwards, are there more items? + hasNextPage: Boolean! + + # When paginating backwards, are there more items? + hasPreviousPage: Boolean! + + # When paginating backwards, the cursor to continue. + startCursor: String + + # When paginating forwards, the cursor to continue. + endCursor: String +} + +type Query { + # Fetches an object given its ID + node( + # The ID of an object + id: ID! + ): Node + store: Store } # Retweet of a tweet @@ -13,35 +39,53 @@ type Retweet { user: TwitterUser } -# A tweet object -type Tweet { - id: ID - created_at: String - text: String - retweet_count: Int - user: TwitterUser +# A connection to a list of items. +type SearchLinkConnection { + # Information to aid in pagination. + pageInfo: PageInfo! - # Get a list of retweets - retweets(limit: Int = 5): [Retweet] + # A list of edges. + edges: [SearchLinkEdge] } -# The Twitter API -type TwitterAPI { - tweet( - # Unique ID of tweet - id: String! - ): Tweet +# An edge in a connection. +type SearchLinkEdge { + # The item at the end of the edge + node: Tweet + + # A cursor for use in pagination + cursor: String! +} + +type Store implements Node { + # The ID of an object + id: ID! + searchConnection( + after: String + first: Int + before: String + last: Int - # Returns a collection of relevant Tweets matching a specified query. - search( # A UTF-8, URL-encoded search query of 500 characters maximum, including # operators. Queries may additionally be limited by complexity. - q: String! + q: String # The number of tweets to return per page, up to a maximum of 100. This was # formerly the “rpp” parameter in the old Search API. count: Int - ): Viewer + ): SearchLinkConnection +} + +# A tweet object +type Tweet { + id: ID + created_at: String + text: String + retweet_count: Int + user: TwitterUser + + # Get a list of retweets + retweets(limit: Int = 5): [Retweet] } # Twitter user @@ -65,7 +109,3 @@ type TwitterUser { # Get a list of tweets for current user tweets(limit: Int = 10): [Tweet] } - -type Viewer { - tweet: [Tweet] -} diff --git a/gql-schema/schema.js b/gql-schema/schema.js index 8e62b62..b2fc22f 100644 --- a/gql-schema/schema.js +++ b/gql-schema/schema.js @@ -7,9 +7,41 @@ import { GraphQLInt, GraphQLList } from 'graphql'; + + +import{ + globalIdField, + fromGlobalId, + nodeDefinitions, + connectionDefinitions, + connectionArgs, + connectionFromPromisedArray, + mutationWithClientMutationId +} from 'graphql-relay'; + import * as twitterCli from '../twitter-cli'; import * as Weather from '../weather'; +class Store {}; +let store = new Store(); + +let nodeDefs = nodeDefinitions( + (globalId) => { + let {type} = fromGlobalId(globalId); + if(type === "Store"){ + return store; + } + return null; + }, + + (obj) => { + if(obj instanceof Store){ + return storeType; + } + return null; + } + ); + let UserType = new GraphQLObjectType({ name: 'TwitterUser', description: 'Twitter user', @@ -95,48 +127,62 @@ let RetweetType = new GraphQLObjectType({ }) }); +/*let viewer = {}; let viewerType = new GraphQLObjectType({ name: 'Viewer', - fields: { + fields: () => ({ + id: globalIdField("Viewer"), tweet: { type: new GraphQLList(TweetType), - resolve: (viewer) => viewer, - }, - }, -}); + resolve: (viewer) => viewer + } + }) +});*/ -let twitterType = new GraphQLObjectType({ - name: 'TwitterAPI', - description: 'The Twitter API', - fields: { - tweet: { - type: TweetType, - args: { - id: { - type: new GraphQLNonNull(GraphQLString), - description: 'Unique ID of tweet' - } - }, - resolve: (_, { id: tweetId }) => twitterCli.getTweet(tweetId) - }, - search: { - type: viewerType, - description: "Returns a collection of relevant Tweets matching a specified query.", - args: { - q: { - type: new GraphQLNonNull(GraphQLString), - description: "A UTF-8, URL-encoded search query of 500 characters maximum, including operators. Queries may additionally be limited by complexity." +let searchConnection = connectionDefinitions({ + name:'SearchLink', + nodeType:TweetType +}) + +let storeType = new GraphQLObjectType({ + name: 'Store', + fields: () => ({ + id: globalIdField("Store"), + searchConnection: { + type: searchConnection.connectionType, + id: globalIdField("Search"), + args: { + ...connectionArgs, + q:{ + type: GraphQLString, + description:"A UTF-8, URL-encoded search query of 500 characters maximum, including operators. Queries may additionally be limited by complexity." + }, + count: { + type: GraphQLInt, + description: "The number of tweets to return per page, up to a maximum of 100. This was formerly the “rpp” parameter in the old Search API." + } + }, + resolve: (_,args) => { + return (connectionFromPromisedArray( + twitterCli.searchTweets(args),args)) + } }, - count: { - type: GraphQLInt, - description: "The number of tweets to return per page, up to a maximum of 100. This was formerly the “rpp” parameter in the old Search API." - } - }, - resolve: (_, searchArgs) => twitterCli.searchTweets(searchArgs) - } - } -}); + + }), + interfaces:[nodeDefs.nodeInterface] + }); + + export const Schema = new GraphQLSchema({ - query: twitterType + query: new GraphQLObjectType({ + name: 'Query', + fields: () => ({ + node: nodeDefs.nodeField, + store: { + type: storeType, + resolve: () => store + } + }) + }) }); diff --git a/gql-schema/schema.json b/gql-schema/schema.json index c8ce2ee..91dd995 100644 --- a/gql-schema/schema.json +++ b/gql-schema/schema.json @@ -2,58 +2,176 @@ "data": { "__schema": { "queryType": { - "name": "TwitterAPI" + "name": "Query" }, "mutationType": null, "subscriptionType": null, "types": [ { "kind": "OBJECT", - "name": "TwitterAPI", - "description": "The Twitter API", + "name": "Query", + "description": null, "fields": [ { - "name": "tweet", - "description": null, + "name": "node", + "description": "Fetches an object given its ID", "args": [ { "name": "id", - "description": "Unique ID of tweet", + "description": "The ID of an object", "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", - "name": "String", + "name": "ID", "ofType": null } }, "defaultValue": null } ], + "type": { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "store", + "description": null, + "args": [], "type": { "kind": "OBJECT", - "name": "Tweet", + "name": "Store", "ofType": null }, "isDeprecated": false, "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "ID", + "description": "The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `\"4\"`) or integer (such as `4`) input value will be accepted as an ID.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INTERFACE", + "name": "Node", + "description": "An object with an ID", + "fields": [ + { + "name": "id", + "description": "The id of the object.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": [ + { + "kind": "OBJECT", + "name": "Store", + "ofType": null + } + ] + }, + { + "kind": "OBJECT", + "name": "Store", + "description": null, + "fields": [ + { + "name": "id", + "description": "The ID of an object", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null }, { - "name": "search", - "description": "Returns a collection of relevant Tweets matching a specified query.", + "name": "searchConnection", + "description": null, "args": [ + { + "name": "after", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "first", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "before", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "last", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, { "name": "q", "description": "A UTF-8, URL-encoded search query of 500 characters maximum, including operators. Queries may additionally be limited by complexity.", "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } + "kind": "SCALAR", + "name": "String", + "ofType": null }, "defaultValue": null }, @@ -70,7 +188,7 @@ ], "type": { "kind": "OBJECT", - "name": "Viewer", + "name": "SearchLinkConnection", "ofType": null }, "isDeprecated": false, @@ -78,7 +196,13 @@ } ], "inputFields": null, - "interfaces": [], + "interfaces": [ + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + } + ], "enumValues": null, "possibleTypes": null }, @@ -92,6 +216,175 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "SCALAR", + "name": "Int", + "description": "The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. ", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "SearchLinkConnection", + "description": "A connection to a list of items.", + "fields": [ + { + "name": "pageInfo", + "description": "Information to aid in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "edges", + "description": "A list of edges.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "SearchLinkEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "PageInfo", + "description": "Information about pagination in a connection.", + "fields": [ + { + "name": "hasNextPage", + "description": "When paginating forwards, are there more items?", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "hasPreviousPage", + "description": "When paginating backwards, are there more items?", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "startCursor", + "description": "When paginating backwards, the cursor to continue.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "endCursor", + "description": "When paginating forwards, the cursor to continue.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "Boolean", + "description": "The `Boolean` scalar type represents `true` or `false`.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "SearchLinkEdge", + "description": "An edge in a connection.", + "fields": [ + { + "name": "node", + "description": "The item at the end of the edge", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Tweet", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "cursor", + "description": "A cursor for use in pagination", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, { "kind": "OBJECT", "name": "Tweet", @@ -190,26 +483,6 @@ "enumValues": null, "possibleTypes": null }, - { - "kind": "SCALAR", - "name": "ID", - "description": "The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `\"4\"`) or integer (such as `4`) input value will be accepted as an ID.", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "SCALAR", - "name": "Int", - "description": "The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. ", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, { "kind": "OBJECT", "name": "TwitterUser", @@ -475,33 +748,6 @@ "enumValues": null, "possibleTypes": null }, - { - "kind": "OBJECT", - "name": "Viewer", - "description": null, - "fields": [ - { - "name": "tweet", - "description": null, - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "Tweet", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, { "kind": "OBJECT", "name": "__Schema", @@ -845,16 +1091,6 @@ ], "possibleTypes": null }, - { - "kind": "SCALAR", - "name": "Boolean", - "description": "The `Boolean` scalar type represents `true` or `false`.", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, { "kind": "OBJECT", "name": "__Field", diff --git a/js/app.js b/js/app.js index 373ab4d..5dba57c 100644 --- a/js/app.js +++ b/js/app.js @@ -25,4 +25,4 @@ ReactDOM.render( , document.getElementById('root') -); +); \ No newline at end of file diff --git a/js/components/App.js b/js/components/App.js index 228e721..84d2315 100644 --- a/js/components/App.js +++ b/js/components/App.js @@ -2,16 +2,31 @@ import React from 'react'; import Relay from 'react-relay'; import { Card, CardHeader, CardText } from 'material-ui/Card'; import './app.css'; - +import debounce from 'lodash.debounce'; class App extends React.Component { - + constructor(props){ + super(props) + this.searchInput = null; + this.searchData = debounce(this.searchData.bind(this),500); + } trimDate(fullDate) { const dateLength = fullDate.length; let dateStamp = fullDate.slice(3, 10) + " " + fullDate.slice(dateLength - 4, dateLength) return dateStamp; } - + searchData(){ + let value = this.searchInput.value; + this.props.relay.setVariables({ + query:value + }) + } + selectLimit(e){ + let value = parseInt(e.target.value); + this.props.relay.setVariables({ + limit:value + }) + } render() { return (
@@ -19,12 +34,18 @@ class App extends React.Component {

Twitteratis

- + {this.searchInput = el;}} placeholder="Type here to search further" onChange={this.searchData.bind(this)}/> +
- {this.props.tweet.tweet.map(tweet => + {this.props.tweet.searchConnection.edges.map(tweet => // id: {tweet.id}

{tweet.text}

-- @{tweet.user.screen_name}
- - {tweet.text}+ {tweet.user.profile_image_url} + {tweet.node.text}+ {tweet.node.user.profile_image_url} Relay.QL` - fragment on Viewer { - tweet{ - id, - text, - created_at - user{ - screen_name, - profile_image_url - } + fragment on Store{ + searchConnection(q:$query,count:$limit,first:5){ + edges{ + node{ + id, + text, + created_at, + user{ + screen_name, + profile_image_url } + } } - `, - }, + } + } + ` + } }); + +export default App; \ No newline at end of file diff --git a/js/routes/AppHomeRoute.js b/js/routes/AppHomeRoute.js index c4543c2..5805b6b 100644 --- a/js/routes/AppHomeRoute.js +++ b/js/routes/AppHomeRoute.js @@ -2,11 +2,13 @@ import Relay from 'react-relay'; export default class extends Relay.Route { static queries = { - tweet: () => Relay.QL` - query TwitterAPI{ - search(q: "graphQl") - } - `, + tweet: (Component) => Relay.QL` + query TwitterAPI { + store{ + ${Component.getFragment('tweet')} + } + } + ` }; static routeName = 'AppHomeRoute'; } diff --git a/package.json b/package.json index b615b81..3f994a0 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "graphql": "0.7.0", "graphql-relay": "0.4.3", "lodash": "^4.17.2", + "lodash.debounce": "^4.0.8", "material-ui": "^0.16.4", "node-fetch": "1.5.1", "nodemon": "1.9.1",