├── README.md ├── backend ├── .gitignore ├── LICENSE ├── README.md ├── app.yaml ├── package-lock.json ├── package.json ├── src │ └── index.ts ├── tsconfig.json └── tslint.json └── frontend ├── .editorconfig ├── .gitignore ├── README.md ├── angular.json ├── e2e ├── protractor.conf.js ├── src │ ├── app.e2e-spec.ts │ └── app.po.ts └── tsconfig.e2e.json ├── package-lock.json ├── package.json ├── src ├── app │ ├── app.component.css │ ├── app.component.html │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── graphql.module.ts │ └── graphql │ │ ├── index.ts │ │ ├── like-tweet.graphql │ │ └── tweets.graphql ├── assets │ └── .gitkeep ├── browserslist ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── karma.conf.js ├── main.ts ├── polyfills.ts ├── styles.css ├── test.ts ├── tsconfig.app.json ├── tsconfig.spec.json └── tslint.json ├── tsconfig.json └── tslint.json /README.md: -------------------------------------------------------------------------------- 1 | # angular-apollo-example 2 | 3 | An example of GraphQL queries/mutations with Angular Apollo Boost. 4 | 5 | ## Initial setup 6 | 7 | We start with our [Apollo server from last time](https://github.com/arjunyel/firestore-apollo-graphql), placing the files in the backend folder. Set it up and run the server 8 | 9 | ```bash 10 | npm run serve 11 | ``` 12 | 13 | Generate a new Angular app and install dependencies 14 | 15 | ```bash 16 | ng new frontend 17 | cd frontend 18 | ng add apollo-angular 19 | 20 | npm install --save-dev graphql-code-generator 21 | npm install --save-dev graphql-codegen-apollo-angular-template 22 | npm install --save-dev graphql-codegen-introspection-template 23 | ``` 24 | 25 | Because Apollo Angular supports schematics, it creates a ready to use setup for you: 26 | 27 | Inside the graphql module setup the GraphQL endpoint: 28 | 29 | ```diff 30 | import {NgModule} from '@angular/core'; 31 | import {ApolloModule, APOLLO_OPTIONS} from 'apollo-angular'; 32 | import {HttpLinkModule, HttpLink} from 'apollo-angular-link-http'; 33 | import {InMemoryCache} from 'apollo-cache-inmemory'; 34 | 35 | - const uri = ''; // <-- add the URL of the GraphQL server here 36 | + const uri = 'http://localhost:4000'; 37 | 38 | export function createApollo(httpLink: HttpLink) { 39 | return { 40 | link: httpLink.create({uri}), 41 | cache: new InMemoryCache(), 42 | }; 43 | } 44 | ``` 45 | 46 | And that's it! The GraphQL Module has been already added to the AppModule. 47 | 48 | ## Setup Query 49 | 50 | Lets setup our GraphQL query that retrieves all the tweets. In `src/app/graphql` directory first we create a file called `tweets.graphql` with our tweet query: 51 | 52 | ```graphql 53 | query tweets { 54 | tweets { 55 | id 56 | text 57 | likes 58 | } 59 | } 60 | ``` 61 | 62 | One of the most useful tools to work with GraphQL is [GraphQL Code Generator](https://github.com/dotansimha/graphql-code-generator) which will generate types for our queries and also a [ready to use services](https://www.apollographql.com/docs/angular/basics/services.html) introduced in Apollo Angular v1.2.0. 63 | 64 | To start out with, we point gql-gen at our GraphQL server to generate a schema.json file. 65 | 66 | ```bash 67 | npx gql-gen --schema http://localhost:4000 --template graphql-codegen-introspection-template --out schema.json 68 | ``` 69 | 70 | Now that we have our schema we have code-gen read the graphql files to see which documents we have 71 | 72 | ```bash 73 | npx gql-gen --schema schema.json --template graphql-codegen-apollo-angular-template --out src/app/graphql/index.ts src/app/graphql/*.graphql 74 | ``` 75 | 76 | Now let's add those commands to npm scripts to make the whole process easier. First one, under `graphql:introspect`, the second one will be `graphql:generate`. 77 | 78 | With Apollo Angular and GraphQL Code Generator you don't have to manually inject `Apollo` service and use generated types in every of your query or mutation. 79 | 80 | Now lets add a class property, a tweets observable, and call our query on init 81 | 82 | ```typescript 83 | import { TweetsGQL, Tweets } from './graphql'; 84 | 85 | export class AppComponent implements OnInit { 86 | tweets: Observable; 87 | 88 | constructor(private tweetsGQL: TweetsGQL) {} 89 | 90 | ngOnInit() { 91 | this.tweets = this.tweetsGQL 92 | .watch() 93 | .valueChanges.pipe(map(tweets => tweets.data)); 94 | } 95 | } 96 | ``` 97 | 98 | The `watch` method is going to update our observable whenever the underlying Apollo store on our client is updated, even from another query. We'll see that in the next section when we go over optimistic updates. 99 | 100 | In our app.component.html lets output our tweets and a button/likeTweet function we'll cover in the next section to like a tweet. 101 | 102 | ```html 103 |
104 | {{tweet.text}} 105 | 106 | {{tweet.likes}} 107 |
108 | ``` 109 | 110 | I used [Ionicons](https://ionicons.com/) for the button, when we use web components make sure to add 111 | 112 | ```typescript 113 | schemas: [CUSTOM_ELEMENTS_SCHEMA]; 114 | ``` 115 | 116 | in our app.module.ts. When you run the app you should now see tweets displayed on the screen! 117 | 118 | ## Setup Mutation 119 | 120 | Lets setup a Mutation on our backend that increments the likes on a tweet by 1. Jump into the backend/src/index.ts file and add the mutation to the typeDefs: 121 | 122 | ```typescript 123 | const typeDefs = gql` 124 | 125 | ... 126 | 127 | type Mutation { 128 | likeTweet(id: ID!): Tweet 129 | } 130 | `; 131 | ``` 132 | 133 | Then we code our mutation in the resolver, note that in a real firebase application you would use a transaction to increment the likes. In our example we are just setting the variable, refetching it, and returning. 134 | 135 | ```typescript 136 | const resolvers = { 137 | 138 | ... 139 | 140 | Mutation: { 141 | likeTweet: async (_, args: {id: string} ) => { 142 | try { 143 | const tweetRef = admin.firestore().doc(`tweets/${args.id}`); 144 | 145 | // Increment likes on tweet, in real life you'd use a transaction! 146 | let tweetDoc = await tweetRef.get(); 147 | const tweet = tweetDoc.data() as Tweet; 148 | await tweetRef.update({ likes: tweet.likes + 1 }); 149 | 150 | tweetDoc = await tweetRef.get(); 151 | return tweetDoc.data(); 152 | } catch (error) { 153 | throw new ApolloError(error); 154 | } 155 | } 156 | } 157 | }; 158 | ``` 159 | 160 | Save and reload the server 161 | 162 | ```bash 163 | npm run serve 164 | ``` 165 | 166 | Return back to our app and lets start setting up the like tweet mutation by create `like-tweet.graphql` file 167 | 168 | ```graphql 169 | mutation likeTweet($id: ID!) { 170 | likeTweet(id: $id) { 171 | id 172 | text 173 | likes 174 | } 175 | } 176 | ``` 177 | 178 | Make sure your terminal is in the src/app folder and re-run two npm scripts 179 | 180 | ```bash 181 | npm run graphql:introspect 182 | npm run graphql:generate 183 | ``` 184 | 185 | Now with our types and services we are ready to write the mutation function 186 | 187 | ```diff 188 | - import { TweetsGQL, Tweets } from './graphql'; 189 | + import { TweetsGQL, Tweets, LikeTweetGQL } from './graphql'; 190 | ``` 191 | 192 | ```typescript 193 | constructor( 194 | private tweetsGQL: TweetsGQL, 195 | private likeTweetGQL: LikeTweetGQL 196 | ) {} 197 | 198 | likeTweet(id: string, likes: number, text: string) { 199 | this.likeTweetGQL.mutate({ 200 | id 201 | }).pipe( 202 | tap((data) => console.log(data.data)) 203 | ).subscribe(); 204 | } 205 | ``` 206 | 207 | Clicking on the button should now increment the likes, congrats you've created a GraphQL mutation! 208 | 209 | You'll notice that even though we didn't tie our query to our tweets observable, it still updated the number with the return from the server. This is because under the hood, Apollo client has its own store where it keeps track of things and one queries result can update another. We can use this to implement optimistic updates. 210 | 211 | ### Optimistic Mutation 212 | 213 | Currently when we update the likes it will wait for the server response to update. However for a great user experience we might want to update the UI immediately while the update happens through the network, this is an example of optimistic UI. 214 | 215 | We do this by telling Apollo the type, ID, and values of the object we're going to update. Apollo can update the local store immediately, then when the server response comes it will overwrite it in the store. Change the mutate function: 216 | 217 | ```typescript 218 | this.likeTweetGQL 219 | .mutate( 220 | { 221 | id, 222 | }, 223 | { 224 | optimisticResponse: { 225 | __typename: 'Mutation', 226 | likeTweet: { 227 | __typename: 'Tweet', 228 | id, 229 | likes: likes + 1, 230 | text, 231 | }, 232 | }, 233 | }, 234 | ) 235 | .pipe(tap(data => console.log(data.data))) 236 | .subscribe(); 237 | ``` 238 | 239 | You can use your browser's dev tools to slow down your internet connection and see that now when you click the button the number changes instantly! Have fun! 240 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | service-account.json 3 | lib/ -------------------------------------------------------------------------------- /backend/LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /backend/README.md: -------------------------------------------------------------------------------- 1 | # firebase-firestore-graphql 2 | 3 | An example of a [GraphQL](https://graphql.org/) setup with a Firebase Firestore backend. Uses [Apollo Engine/Server 2.0](https://www.apollographql.com/) and deployed to Google App Engine. 4 | 5 | ## Initial setup 6 | 7 | ```bash 8 | npm init --yes 9 | npm install apollo-server firebase-admin graphql 10 | npm install --save-dev typescript tslint 11 | ``` 12 | 13 | You'll also want to set up some scripts and other settings, as of writing here is what the package.json looks like 14 | 15 | ```json 16 | { 17 | "name": "firebase-firestore-graphql", 18 | "scripts": { 19 | "build": "tsc", 20 | "serve": "npm run build && node lib/index.js", 21 | "start": "node lib/index.js", 22 | "deploy": "npm run build && gcloud app deploy" 23 | }, 24 | "main": "lib/index.js", 25 | "dependencies": { 26 | "apollo-server": "^2.1.0", 27 | "firebase-admin": "^5.12.1", 28 | "graphql": "^0.13.2" 29 | }, 30 | "devDependencies": { 31 | "tslint": "^5.10.0", 32 | "typescript": "^3.0.3" 33 | } 34 | } 35 | ``` 36 | 37 | ## Firebase setup 38 | 39 | Download Firebase service account as `service-account.json` and put in root of this directory. 40 | 41 | In your firestore database setup two collections, one of tweets and one of users. The userId in tweets should point to a user Id that the tweet came from. 42 | 43 | ```typescript 44 | interface User { 45 | id: string; 46 | name: string; 47 | screenName: string; 48 | statusesCount: number; 49 | } 50 | 51 | interface Tweet { 52 | id: string; 53 | name: string; 54 | screenName: string; 55 | statusesCount: number; 56 | userId: string; 57 | } 58 | ``` 59 | 60 | ## Typescript 61 | 62 | Copy the tslint and tsconfig json files from this repo into your own. 63 | 64 | ## GraphQL 65 | 66 | Make a src directory and a index.ts file inside. Setup the imports 67 | 68 | ```typescript 69 | import * as admin from 'firebase-admin'; 70 | 71 | const serviceAccount = require('../service-account.json'); 72 | 73 | admin.initializeApp({ 74 | credential: admin.credential.cert(serviceAccount) 75 | }); 76 | 77 | import { ApolloServer, ApolloError, ValidationError, gql } from 'apollo-server'; 78 | 79 | interface User { 80 | id: string; 81 | name: string; 82 | screenName: string; 83 | statusesCount: number; 84 | } 85 | 86 | interface Tweet { 87 | id: string; 88 | name: string; 89 | screenName: string; 90 | statusesCount: number; 91 | userId: string; 92 | } 93 | ``` 94 | 95 | ## Schema 96 | 97 | Now we setup our GraphQL schema 98 | 99 | ```typescript 100 | const typeDefs = gql` 101 | # A Twitter User 102 | type User { 103 | id: ID! 104 | name: String! 105 | screenName: String! 106 | statusesCount: Int! 107 | tweets: [Tweets]! 108 | } 109 | 110 | # A Tweet Object 111 | type Tweets { 112 | id: ID! 113 | text: String! 114 | userId: String! 115 | user: User! 116 | likes: Int! 117 | } 118 | 119 | type Query { 120 | tweets: [Tweets] 121 | user(id: String!): User 122 | } 123 | `; 124 | ``` 125 | 126 | The ! signifies that this property is guaranteed to not be null. You'll notice that a user has an array of Tweets and a tweet has a user object in it, despite them being separate collections in our database. This is the magic of GraphQL, we can combine things across collections. 127 | 128 | For the purpose of this tutorial we have two queries, an array of all tweets and a specific user based on their ID. 129 | 130 | ## Resolver 131 | 132 | Next we setup our resolver, this turns GraphQL queries into data. First we setup our resolver for the base queries 133 | 134 | ```typescript 135 | const resolvers = { 136 | Query: { 137 | async tweets() { 138 | const tweets = await admin 139 | .firestore() 140 | .collection('tweets') 141 | .get(); 142 | return tweets.docs.map(tweet => tweet.data()) as Tweet[]; 143 | }, 144 | async user(_: null, args: { id: string }) { 145 | try { 146 | const userDoc = await admin 147 | .firestore() 148 | .doc(`users/${args.id}`) 149 | .get(); 150 | const user = userDoc.data() as User | undefined; 151 | return user || new ValidationError('User ID not found'); 152 | } catch (error) { 153 | throw new ApolloError(error); 154 | } 155 | } 156 | } 157 | }; 158 | ``` 159 | 160 | This will get an array of tweets or a user but how do we add the graph part of GraphQL and interconnect different collections such as all the Tweets a user has made or the details of a user that made a certain tweet? 161 | 162 | ```typescript 163 | const resolvers = { 164 | Query: { 165 | ... 166 | }, 167 | User: { 168 | async tweets(user) { 169 | try { 170 | const userTweets = await admin 171 | .firestore() 172 | .collection('tweets') 173 | .where('userId', '==', user.id) 174 | .get(); 175 | return userTweets.docs.map(tweet => tweet.data()) as Tweet[]; 176 | } catch (error) { 177 | throw new ApolloError(error); 178 | } 179 | } 180 | }, 181 | Tweets: { 182 | async user(tweet) { 183 | try { 184 | const tweetAuthor = await admin 185 | .firestore() 186 | .doc(`users/${tweet.userId}`) 187 | .get(); 188 | return tweetAuthor.data() as User; 189 | } catch (error) { 190 | throw new ApolloError(error); 191 | } 192 | } 193 | } 194 | }; 195 | ``` 196 | 197 | Take getting all the tweets a user has made as an example. You can see in our resolver we have a user object with a tweets property. Because tweets is a child of user, we can use the parent user to then query the tweets collection for all the tweets with that user ID. 198 | 199 | ### Apollo Server 200 | 201 | Finally we setup our Apollo server 202 | 203 | ```typescript 204 | const server = new ApolloServer({ 205 | typeDefs, 206 | resolvers, 207 | introspection: true 208 | }); 209 | 210 | server.listen().then(({ url }) => { 211 | console.log(`🚀 Server ready at ${url}`); 212 | }); 213 | ``` 214 | 215 | If you setup your npm scripts you should be able to run 216 | 217 | ```bash 218 | npm run serve 219 | ``` 220 | 221 | If you navigate to the URL you shoud be able to see a GraphQL playground where you can query your API, congrats! 222 | 223 | ## Apollo Engine 224 | 225 | [Apollo Engine](https://www.apollographql.com/engine) gives use awesome features such as caching, tracing, and error logging. First get an [Apollo Engine API key](https://engine.apollographql.com/) then change your Apollo server config to turn on engine 226 | 227 | ```typescript 228 | const server = new ApolloServer({ 229 | typeDefs, 230 | resolvers, 231 | engine: { 232 | apiKey: "" 233 | }, 234 | introspection: true 235 | }); 236 | ``` 237 | 238 | Now when you npm serve and run some queries you should see some data populate the Apollo Engine dashboard with things like how fast your queries resolved. Cool! 239 | 240 | ## App Engine 241 | 242 | Finally we can deploy to App engine so the world can access our GraphQL endpoint. In the root project folder create a file app.yaml. Inside is just one line 243 | 244 | ```yaml 245 | runtime: nodejs8 246 | ``` 247 | 248 | Also add the .gcloudignore file from this repo to your folder. Setup the gcloud SDK then point it to your Firebase project. 249 | 250 | ```bash 251 | gcloud config set project 252 | npm run build 253 | gcloud app deploy 254 | ``` 255 | 256 | You should get a deployed URL, you can then query that using an GraphQL tool. I personally use [Insomnia’s GraphQL mode](https://support.insomnia.rest/article/61-graphql). 257 | 258 | Congratulations, you've setup a GraphQL server! 259 | -------------------------------------------------------------------------------- /backend/app.yaml: -------------------------------------------------------------------------------- 1 | runtime: nodejs8 2 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "scripts": { 4 | "build": "tsc", 5 | "serve": "npm run build && node lib/index.js", 6 | "start": "node lib/index.js", 7 | "deploy": "npm run build && gcloud app deploy" 8 | }, 9 | "main": "lib/index.js", 10 | "dependencies": { 11 | "apollo-server": "^2.1.0", 12 | "firebase-admin": "^5.12.1", 13 | "graphql": "^0.13.2" 14 | }, 15 | "devDependencies": { 16 | "@types/graphql": "^14.0.0", 17 | "tslint": "^5.10.0", 18 | "typescript": "^3.0.3" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /backend/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as admin from 'firebase-admin'; 2 | 3 | const serviceAccount = require('../service-account.json'); 4 | 5 | admin.initializeApp({ 6 | credential: admin.credential.cert(serviceAccount) 7 | }); 8 | 9 | import { ApolloServer, ApolloError, ValidationError, gql } from 'apollo-server'; 10 | 11 | interface User { 12 | id: string; 13 | name: string; 14 | screenName: string; 15 | statusesCount: number; 16 | } 17 | 18 | interface Tweet { 19 | id: string; 20 | likes: number; 21 | text: string; 22 | userId: string; 23 | } 24 | 25 | const typeDefs = gql` 26 | # A Twitter User 27 | type User { 28 | id: ID! 29 | name: String! 30 | screenName: String! 31 | statusesCount: Int! 32 | tweets: [Tweet]! 33 | } 34 | 35 | # A Tweet Object 36 | type Tweet { 37 | id: ID! 38 | text: String! 39 | userId: String! 40 | user: User! 41 | likes: Int! 42 | } 43 | 44 | type Query { 45 | tweets: [Tweet] 46 | user(id: String!): User 47 | } 48 | 49 | type Mutation { 50 | likeTweet(id: ID!): Tweet 51 | } 52 | `; 53 | 54 | const resolvers = { 55 | User: { 56 | async tweets(user) { 57 | try { 58 | const userTweets = await admin 59 | .firestore() 60 | .collection('tweets') 61 | .where('userId', '==', user.id) 62 | .get(); 63 | return userTweets.docs.map(tweet => tweet.data()) as Tweet[]; 64 | } catch (error) { 65 | throw new ApolloError(error); 66 | } 67 | } 68 | }, 69 | Tweet: { 70 | async user(tweet) { 71 | try { 72 | const tweetAuthor = await admin 73 | .firestore() 74 | .doc(`users/${tweet.userId}`) 75 | .get(); 76 | return tweetAuthor.data() as User; 77 | } catch (error) { 78 | throw new ApolloError(error); 79 | } 80 | } 81 | }, 82 | Query: { 83 | async tweets() { 84 | const tweets = await admin 85 | .firestore() 86 | .collection('tweets') 87 | .get(); 88 | return tweets.docs.map(tweet => tweet.data()) as Tweet[]; 89 | }, 90 | async user(_: null, args: { id: string }) { 91 | try { 92 | const userDoc = await admin 93 | .firestore() 94 | .doc(`users/${args.id}`) 95 | .get(); 96 | const user = userDoc.data() as User | undefined; 97 | return user || new ValidationError('User ID not found'); 98 | } catch (error) { 99 | throw new ApolloError(error); 100 | } 101 | } 102 | }, 103 | Mutation: { 104 | likeTweet: async (_, args: {id: string} ) => { 105 | try { 106 | const tweetRef = admin.firestore().doc(`tweets/${args.id}`); 107 | 108 | // Increment likes on tweet, in real life you'd use a transaction! 109 | let tweetDoc = await tweetRef.get(); 110 | const tweet = tweetDoc.data() as Tweet; 111 | await tweetRef.update({ likes: tweet.likes + 1 }); 112 | 113 | tweetDoc = await tweetRef.get(); 114 | return tweetDoc.data(); 115 | } catch (error) { 116 | throw new ApolloError(error); 117 | } 118 | } 119 | } 120 | }; 121 | 122 | const server = new ApolloServer({ 123 | typeDefs, 124 | resolvers, 125 | introspection: true 126 | }); 127 | 128 | server.listen().then(({ url }) => { 129 | console.log(`🚀 Server ready at ${url}`); 130 | }); 131 | -------------------------------------------------------------------------------- /backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["es6", "dom", "esnext.asynciterable"], 4 | "module": "commonjs", 5 | "noImplicitReturns": false, 6 | "outDir": "lib", 7 | "sourceMap": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017" 10 | }, 11 | "compileOnSave": true, 12 | "include": ["src"] 13 | } 14 | -------------------------------------------------------------------------------- /backend/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | // -- Strict errors -- 4 | // These lint rules are likely always a good idea. 5 | 6 | // Force function overloads to be declared together. This ensures readers understand APIs. 7 | "adjacent-overload-signatures": true, 8 | 9 | // Do not allow the subtle/obscure comma operator. 10 | "ban-comma-operator": true, 11 | 12 | // Do not allow internal modules or namespaces . These are deprecated in favor of ES6 modules. 13 | "no-namespace": true, 14 | 15 | // Do not allow parameters to be reassigned. To avoid bugs, developers should instead assign new values to new vars. 16 | "no-parameter-reassignment": true, 17 | 18 | // Force the use of ES6-style imports instead of /// imports. 19 | "no-reference": true, 20 | 21 | // Do not allow type assertions that do nothing. This is a big warning that the developer may not understand the 22 | // code currently being edited (they may be incorrectly handling a different type case that does not exist). 23 | "no-unnecessary-type-assertion": true, 24 | 25 | // Disallow nonsensical label usage. 26 | "label-position": true, 27 | 28 | // Disallows the (often typo) syntax if (var1 = var2). Replace with if (var2) { var1 = var2 }. 29 | "no-conditional-assignment": true, 30 | 31 | // Disallows constructors for primitive types (e.g. new Number('123'), though Number('123') is still allowed). 32 | "no-construct": true, 33 | 34 | // Do not allow super() to be called twice in a constructor. 35 | "no-duplicate-super": true, 36 | 37 | // Do not allow the same case to appear more than once in a switch block. 38 | "no-duplicate-switch-case": true, 39 | 40 | // Do not allow a variable to be declared more than once in the same block. Consider function parameters in this 41 | // rule. 42 | "no-duplicate-variable": [true, "check-parameters"], 43 | 44 | // Disallows a variable definition in an inner scope from shadowing a variable in an outer scope. Developers should 45 | // instead use a separate variable name. 46 | "no-shadowed-variable": true, 47 | 48 | // Empty blocks are almost never needed. Allow the one general exception: empty catch blocks. 49 | "no-empty": [true, "allow-empty-catch"], 50 | 51 | // Functions must either be handled directly (e.g. with a catch() handler) or returned to another function. 52 | // This is a major source of errors in Cloud Functions and the team strongly recommends leaving this rule on. 53 | "no-floating-promises": true, 54 | 55 | // Do not allow any imports for modules that are not in package.json. These will almost certainly fail when 56 | // deployed. 57 | "no-implicit-dependencies": true, 58 | 59 | // The 'this' keyword can only be used inside of classes. 60 | "no-invalid-this": true, 61 | 62 | // Do not allow strings to be thrown because they will not include stack traces. Throw Errors instead. 63 | "no-string-throw": true, 64 | 65 | // Disallow control flow statements, such as return, continue, break, and throw in finally blocks. 66 | "no-unsafe-finally": true, 67 | 68 | // Do not allow variables to be used before they are declared. 69 | "no-use-before-declare": true, 70 | 71 | // Expressions must always return a value. Avoids common errors like const myValue = functionReturningVoid(); 72 | "no-void-expression": [true, "ignore-arrow-function-shorthand"], 73 | 74 | // Disallow duplicate imports in the same file. 75 | "no-duplicate-imports": true, 76 | 77 | 78 | // -- Strong Warnings -- 79 | // These rules should almost never be needed, but may be included due to legacy code. 80 | // They are left as a warning to avoid frustration with blocked deploys when the developer 81 | // understand the warning and wants to deploy anyway. 82 | 83 | // Warn when an empty interface is defined. These are generally not useful. 84 | "no-empty-interface": {"severity": "warning"}, 85 | 86 | // Warn when an import will have side effects. 87 | "no-import-side-effect": {"severity": "warning"}, 88 | 89 | // Warn when variables are defined with var. Var has subtle meaning that can lead to bugs. Strongly prefer const for 90 | // most values and let for values that will change. 91 | "no-var-keyword": {"severity": "warning"}, 92 | 93 | // Prefer === and !== over == and !=. The latter operators support overloads that are often accidental. 94 | "triple-equals": {"severity": "warning"}, 95 | 96 | // Warn when using deprecated APIs. 97 | "deprecation": {"severity": "warning"}, 98 | 99 | // -- Light Warnigns -- 100 | // These rules are intended to help developers use better style. Simpler code has fewer bugs. These would be "info" 101 | // if TSLint supported such a level. 102 | 103 | // prefer for( ... of ... ) to an index loop when the index is only used to fetch an object from an array. 104 | // (Even better: check out utils like .map if transforming an array!) 105 | "prefer-for-of": {"severity": "warning"}, 106 | 107 | // Warns if function overloads could be unified into a single function with optional or rest parameters. 108 | "unified-signatures": {"severity": "warning"}, 109 | 110 | // Warns if code has an import or variable that is unused. 111 | "no-unused-variable": {"severity": "warning"}, 112 | 113 | // Prefer const for values that will not change. This better documents code. 114 | "prefer-const": {"severity": "warning"}, 115 | 116 | // Multi-line object liiterals and function calls should have a trailing comma. This helps avoid merge conflicts. 117 | "trailing-comma": {"severity": "warning"} 118 | }, 119 | 120 | "defaultSeverity": "error" 121 | } 122 | -------------------------------------------------------------------------------- /frontend/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | /node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | # misc 28 | /.sass-cache 29 | /connect.lock 30 | /coverage 31 | /libpeerconnection.log 32 | npm-debug.log 33 | yarn-error.log 34 | testem.log 35 | /typings 36 | 37 | # System Files 38 | .DS_Store 39 | Thumbs.db 40 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # AngularApolloExample 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 6.0.8. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). 24 | 25 | ## Further help 26 | 27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). 28 | -------------------------------------------------------------------------------- /frontend/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "angular-apollo-example": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "prefix": "app", 11 | "schematics": {}, 12 | "architect": { 13 | "build": { 14 | "builder": "@angular-devkit/build-angular:browser", 15 | "options": { 16 | "outputPath": "dist/angular-apollo-example", 17 | "index": "src/index.html", 18 | "main": "src/main.ts", 19 | "polyfills": "src/polyfills.ts", 20 | "tsConfig": "src/tsconfig.app.json", 21 | "assets": [ 22 | "src/favicon.ico", 23 | "src/assets" 24 | ], 25 | "styles": [ 26 | "src/styles.css" 27 | ], 28 | "scripts": [] 29 | }, 30 | "configurations": { 31 | "production": { 32 | "fileReplacements": [ 33 | { 34 | "replace": "src/environments/environment.ts", 35 | "with": "src/environments/environment.prod.ts" 36 | } 37 | ], 38 | "optimization": true, 39 | "outputHashing": "all", 40 | "sourceMap": false, 41 | "extractCss": true, 42 | "namedChunks": false, 43 | "aot": true, 44 | "extractLicenses": true, 45 | "vendorChunk": false, 46 | "buildOptimizer": true 47 | } 48 | } 49 | }, 50 | "serve": { 51 | "builder": "@angular-devkit/build-angular:dev-server", 52 | "options": { 53 | "browserTarget": "angular-apollo-example:build" 54 | }, 55 | "configurations": { 56 | "production": { 57 | "browserTarget": "angular-apollo-example:build:production" 58 | } 59 | } 60 | }, 61 | "extract-i18n": { 62 | "builder": "@angular-devkit/build-angular:extract-i18n", 63 | "options": { 64 | "browserTarget": "angular-apollo-example:build" 65 | } 66 | }, 67 | "test": { 68 | "builder": "@angular-devkit/build-angular:karma", 69 | "options": { 70 | "main": "src/test.ts", 71 | "polyfills": "src/polyfills.ts", 72 | "tsConfig": "src/tsconfig.spec.json", 73 | "karmaConfig": "src/karma.conf.js", 74 | "styles": [ 75 | "src/styles.css" 76 | ], 77 | "scripts": [], 78 | "assets": [ 79 | "src/favicon.ico", 80 | "src/assets" 81 | ] 82 | } 83 | }, 84 | "lint": { 85 | "builder": "@angular-devkit/build-angular:tslint", 86 | "options": { 87 | "tsConfig": [ 88 | "src/tsconfig.app.json", 89 | "src/tsconfig.spec.json" 90 | ], 91 | "exclude": [ 92 | "**/node_modules/**" 93 | ] 94 | } 95 | } 96 | } 97 | }, 98 | "angular-apollo-example-e2e": { 99 | "root": "e2e/", 100 | "projectType": "application", 101 | "architect": { 102 | "e2e": { 103 | "builder": "@angular-devkit/build-angular:protractor", 104 | "options": { 105 | "protractorConfig": "e2e/protractor.conf.js", 106 | "devServerTarget": "angular-apollo-example:serve" 107 | }, 108 | "configurations": { 109 | "production": { 110 | "devServerTarget": "angular-apollo-example:serve:production" 111 | } 112 | } 113 | }, 114 | "lint": { 115 | "builder": "@angular-devkit/build-angular:tslint", 116 | "options": { 117 | "tsConfig": "e2e/tsconfig.e2e.json", 118 | "exclude": [ 119 | "**/node_modules/**" 120 | ] 121 | } 122 | } 123 | } 124 | } 125 | }, 126 | "defaultProject": "angular-apollo-example" 127 | } -------------------------------------------------------------------------------- /frontend/e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './src/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | onPrepare() { 23 | require('ts-node').register({ 24 | project: require('path').join(__dirname, './tsconfig.e2e.json') 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; -------------------------------------------------------------------------------- /frontend/e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | 3 | describe('workspace-project App', () => { 4 | let page: AppPage; 5 | 6 | beforeEach(() => { 7 | page = new AppPage(); 8 | }); 9 | 10 | it('should display welcome message', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('Welcome to angular-apollo-example!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /frontend/e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /frontend/e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-apollo-example", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "test": "ng test", 9 | "lint": "ng lint", 10 | "e2e": "ng e2e", 11 | "graphql:introspect": "gql-gen --schema http://localhost:4000 --template graphql-codegen-introspection-template --out schema.json", 12 | "graphql:generate": "gql-gen --schema schema.json --template graphql-codegen-apollo-angular-template --out src/app/graphql/index.ts src/app/graphql/*.graphql" 13 | }, 14 | "private": true, 15 | "dependencies": { 16 | "@angular/animations": "^6.1.8", 17 | "@angular/common": "^6.1.8", 18 | "@angular/compiler": "^6.1.8", 19 | "@angular/core": "^6.1.8", 20 | "@angular/forms": "^6.1.8", 21 | "@angular/http": "^6.1.8", 22 | "@angular/platform-browser": "^6.1.8", 23 | "@angular/platform-browser-dynamic": "^6.1.8", 24 | "@angular/router": "^6.1.8", 25 | "core-js": "^2.5.4", 26 | "graphql": "^0.13.2", 27 | "graphql-code-generator": "^0.12.5", 28 | "graphql-codegen-apollo-angular-template": "^0.12.5", 29 | "graphql-codegen-introspection-template": "^0.12.5", 30 | "rxjs": "^6.3.2", 31 | "zone.js": "^0.8.26", 32 | "apollo-angular": "^1.3.0", 33 | "apollo-angular-link-http": "^1.2.0", 34 | "apollo-link": "^1.2.0", 35 | "apollo-client": "^2.4.0", 36 | "apollo-cache-inmemory": "^1.2.0", 37 | "graphql-tag": "^2.9.2" 38 | }, 39 | "devDependencies": { 40 | "@angular-devkit/build-angular": "~0.8.3", 41 | "@angular/cli": "~6.2.3", 42 | "@angular/compiler-cli": "^6.1.8", 43 | "@angular/language-service": "^6.1.8", 44 | "@types/jasmine": "~2.8.6", 45 | "@types/jasminewd2": "~2.0.3", 46 | "@types/node": "~8.9.4", 47 | "codelyzer": "~4.3.0", 48 | "jasmine-core": "~2.99.1", 49 | "jasmine-spec-reporter": "~4.2.1", 50 | "karma": "~1.7.1", 51 | "karma-chrome-launcher": "~2.2.0", 52 | "karma-coverage-istanbul-reporter": "~2.0.0", 53 | "karma-jasmine": "~1.1.1", 54 | "karma-jasmine-html-reporter": "^0.2.2", 55 | "protractor": "~5.3.0", 56 | "ts-node": "~5.0.1", 57 | "tslint": "~5.9.1", 58 | "typescript": "~2.9.2" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /frontend/src/app/app.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arjunyel/angular-apollo-example/aa1af32b71aa61b448edab62777795ad9abf8652/frontend/src/app/app.component.css -------------------------------------------------------------------------------- /frontend/src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 | {{tweet.text}} 3 | 4 | {{tweet.likes}} 5 |
6 | -------------------------------------------------------------------------------- /frontend/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | import { AppComponent } from './app.component'; 3 | describe('AppComponent', () => { 4 | beforeEach(async(() => { 5 | TestBed.configureTestingModule({ 6 | declarations: [ 7 | AppComponent 8 | ], 9 | }).compileComponents(); 10 | })); 11 | it('should create the app', async(() => { 12 | const fixture = TestBed.createComponent(AppComponent); 13 | const app = fixture.debugElement.componentInstance; 14 | expect(app).toBeTruthy(); 15 | })); 16 | it(`should have as title 'app'`, async(() => { 17 | const fixture = TestBed.createComponent(AppComponent); 18 | const app = fixture.debugElement.componentInstance; 19 | expect(app.title).toEqual('app'); 20 | })); 21 | it('should render title in a h1 tag', async(() => { 22 | const fixture = TestBed.createComponent(AppComponent); 23 | fixture.detectChanges(); 24 | const compiled = fixture.debugElement.nativeElement; 25 | expect(compiled.querySelector('h1').textContent).toContain('Welcome to angular-apollo-example!'); 26 | })); 27 | }); 28 | -------------------------------------------------------------------------------- /frontend/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | import { Observable } from 'rxjs'; 4 | import { map, tap } from 'rxjs/operators'; 5 | 6 | import { 7 | TweetsGQL, 8 | Tweets, 9 | LikeTweetGQL, 10 | } from './graphql'; 11 | 12 | @Component({ 13 | selector: 'app-root', 14 | templateUrl: './app.component.html', 15 | styleUrls: ['./app.component.css'], 16 | }) 17 | export class AppComponent implements OnInit { 18 | tweets: Observable; 19 | 20 | constructor( 21 | private tweetsGQL: TweetsGQL, 22 | private likeTweetGQL: LikeTweetGQL, 23 | ) {} 24 | 25 | ngOnInit() { 26 | this.tweets = this.tweetsGQL 27 | .watch() 28 | .valueChanges.pipe(map(tweets => tweets.data)); 29 | } 30 | 31 | likeTweet(id: string, likes: number, text: string) { 32 | this.likeTweetGQL 33 | .mutate( 34 | { id }, 35 | { 36 | optimisticResponse: { 37 | __typename: 'Mutation', 38 | likeTweet: { 39 | __typename: 'Tweet', 40 | id, 41 | likes: likes + 1, 42 | text, 43 | }, 44 | }, 45 | }, 46 | ) 47 | .pipe(tap(data => console.log(data.data))) 48 | .subscribe(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /frontend/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 3 | 4 | import { AppComponent } from './app.component'; 5 | import { GraphQLModule } from './graphql.module'; 6 | import { HttpClientModule } from '@angular/common/http'; 7 | 8 | @NgModule({ 9 | declarations: [ 10 | AppComponent 11 | ], 12 | imports: [ 13 | BrowserModule, 14 | GraphQLModule, 15 | HttpClientModule 16 | ], 17 | providers: [], 18 | bootstrap: [AppComponent], 19 | schemas: [CUSTOM_ELEMENTS_SCHEMA] 20 | }) 21 | export class AppModule { } 22 | -------------------------------------------------------------------------------- /frontend/src/app/graphql.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {ApolloModule, APOLLO_OPTIONS} from 'apollo-angular'; 3 | import {HttpLinkModule, HttpLink} from 'apollo-angular-link-http'; 4 | import {InMemoryCache} from 'apollo-cache-inmemory'; 5 | 6 | const uri = 'http://localhost:4000'; 7 | 8 | export function createApollo(httpLink: HttpLink) { 9 | return { 10 | link: httpLink.create({uri}), 11 | cache: new InMemoryCache(), 12 | }; 13 | } 14 | 15 | @NgModule({ 16 | exports: [ApolloModule, HttpLinkModule], 17 | providers: [ 18 | { 19 | provide: APOLLO_OPTIONS, 20 | useFactory: createApollo, 21 | deps: [HttpLink], 22 | }, 23 | ], 24 | }) 25 | export class GraphQLModule {} 26 | -------------------------------------------------------------------------------- /frontend/src/app/graphql/index.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | import { GraphQLResolveInfo } from "graphql"; 3 | 4 | export type Resolver = ( 5 | parent: Parent, 6 | args: Args, 7 | context: Context, 8 | info: GraphQLResolveInfo 9 | ) => Promise | Result; 10 | 11 | export type SubscriptionResolver< 12 | Result, 13 | Parent = any, 14 | Context = any, 15 | Args = any 16 | > = { 17 | subscribe( 18 | parent: P, 19 | args: Args, 20 | context: Context, 21 | info: GraphQLResolveInfo 22 | ): AsyncIterator; 23 | resolve?( 24 | parent: P, 25 | args: Args, 26 | context: Context, 27 | info: GraphQLResolveInfo 28 | ): R | Result | Promise; 29 | }; 30 | 31 | /** The `Upload` scalar type represents a file upload promise that resolves an object containing `stream`, `filename`, `mimetype` and `encoding`. */ 32 | export type Upload = any; 33 | 34 | export interface Query { 35 | tweets?: (Tweet | null)[] | null; 36 | user?: User | null; 37 | } 38 | 39 | export interface Tweet { 40 | id: string; 41 | text: string; 42 | userId: string; 43 | user: User; 44 | likes: number; 45 | } 46 | 47 | export interface User { 48 | id: string; 49 | name: string; 50 | screenName: string; 51 | statusesCount: number; 52 | tweets: (Tweet | null)[]; 53 | } 54 | 55 | export interface Mutation { 56 | likeTweet?: Tweet | null; 57 | } 58 | export interface UserQueryArgs { 59 | id: string; 60 | } 61 | export interface LikeTweetMutationArgs { 62 | id: string; 63 | } 64 | 65 | export enum CacheControlScope { 66 | PUBLIC = "PUBLIC", 67 | PRIVATE = "PRIVATE" 68 | } 69 | 70 | export namespace QueryResolvers { 71 | export interface Resolvers { 72 | tweets?: TweetsResolver<(Tweet | null)[] | null, any, Context>; 73 | user?: UserResolver; 74 | } 75 | 76 | export type TweetsResolver< 77 | R = (Tweet | null)[] | null, 78 | Parent = any, 79 | Context = any 80 | > = Resolver; 81 | export type UserResolver< 82 | R = User | null, 83 | Parent = any, 84 | Context = any 85 | > = Resolver; 86 | export interface UserArgs { 87 | id: string; 88 | } 89 | } 90 | 91 | export namespace TweetResolvers { 92 | export interface Resolvers { 93 | id?: IdResolver; 94 | text?: TextResolver; 95 | userId?: UserIdResolver; 96 | user?: UserResolver; 97 | likes?: LikesResolver; 98 | } 99 | 100 | export type IdResolver = Resolver< 101 | R, 102 | Parent, 103 | Context 104 | >; 105 | export type TextResolver = Resolver< 106 | R, 107 | Parent, 108 | Context 109 | >; 110 | export type UserIdResolver< 111 | R = string, 112 | Parent = any, 113 | Context = any 114 | > = Resolver; 115 | export type UserResolver = Resolver< 116 | R, 117 | Parent, 118 | Context 119 | >; 120 | export type LikesResolver = Resolver< 121 | R, 122 | Parent, 123 | Context 124 | >; 125 | } 126 | 127 | export namespace UserResolvers { 128 | export interface Resolvers { 129 | id?: IdResolver; 130 | name?: NameResolver; 131 | screenName?: ScreenNameResolver; 132 | statusesCount?: StatusesCountResolver; 133 | tweets?: TweetsResolver<(Tweet | null)[], any, Context>; 134 | } 135 | 136 | export type IdResolver = Resolver< 137 | R, 138 | Parent, 139 | Context 140 | >; 141 | export type NameResolver = Resolver< 142 | R, 143 | Parent, 144 | Context 145 | >; 146 | export type ScreenNameResolver< 147 | R = string, 148 | Parent = any, 149 | Context = any 150 | > = Resolver; 151 | export type StatusesCountResolver< 152 | R = number, 153 | Parent = any, 154 | Context = any 155 | > = Resolver; 156 | export type TweetsResolver< 157 | R = (Tweet | null)[], 158 | Parent = any, 159 | Context = any 160 | > = Resolver; 161 | } 162 | 163 | export namespace MutationResolvers { 164 | export interface Resolvers { 165 | likeTweet?: LikeTweetResolver; 166 | } 167 | 168 | export type LikeTweetResolver< 169 | R = Tweet | null, 170 | Parent = any, 171 | Context = any 172 | > = Resolver; 173 | export interface LikeTweetArgs { 174 | id: string; 175 | } 176 | } 177 | 178 | export namespace LikeTweet { 179 | export type Variables = { 180 | id: string; 181 | }; 182 | 183 | export type Mutation = { 184 | __typename?: "Mutation"; 185 | likeTweet?: LikeTweet | null; 186 | }; 187 | 188 | export type LikeTweet = { 189 | __typename?: "Tweet"; 190 | id: string; 191 | text: string; 192 | likes: number; 193 | }; 194 | } 195 | 196 | export namespace Tweets { 197 | export type Variables = {}; 198 | 199 | export type Query = { 200 | __typename?: "Query"; 201 | tweets?: (Tweets | null)[] | null; 202 | }; 203 | 204 | export type Tweets = { 205 | __typename?: "Tweet"; 206 | id: string; 207 | text: string; 208 | likes: number; 209 | }; 210 | } 211 | 212 | import { Injectable } from "@angular/core"; 213 | 214 | import * as Apollo from "apollo-angular"; 215 | 216 | import gql from "graphql-tag"; 217 | 218 | @Injectable({ 219 | providedIn: "root" 220 | }) 221 | export class LikeTweetGQL extends Apollo.Mutation< 222 | LikeTweet.Mutation, 223 | LikeTweet.Variables 224 | > { 225 | document: any = gql` 226 | mutation likeTweet($id: ID!) { 227 | likeTweet(id: $id) { 228 | id 229 | text 230 | likes 231 | } 232 | } 233 | `; 234 | } 235 | @Injectable({ 236 | providedIn: "root" 237 | }) 238 | export class TweetsGQL extends Apollo.Query { 239 | document: any = gql` 240 | query tweets { 241 | tweets { 242 | id 243 | text 244 | likes 245 | } 246 | } 247 | `; 248 | } 249 | -------------------------------------------------------------------------------- /frontend/src/app/graphql/like-tweet.graphql: -------------------------------------------------------------------------------- 1 | mutation likeTweet($id: ID!) { 2 | likeTweet(id: $id) { 3 | id 4 | text 5 | likes 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /frontend/src/app/graphql/tweets.graphql: -------------------------------------------------------------------------------- 1 | query tweets { 2 | tweets { 3 | id 4 | text 5 | likes 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /frontend/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arjunyel/angular-apollo-example/aa1af32b71aa61b448edab62777795ad9abf8652/frontend/src/assets/.gitkeep -------------------------------------------------------------------------------- /frontend/src/browserslist: -------------------------------------------------------------------------------- 1 | # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | # For IE 9-11 support, please uncomment the last line of the file and adjust as needed 5 | > 0.5% 6 | last 2 versions 7 | Firefox ESR 8 | not dead 9 | # IE 9-11 -------------------------------------------------------------------------------- /frontend/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /frontend/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build ---prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * In development mode, to ignore zone related error stack frames such as 11 | * `zone.run`, `zoneDelegate.invokeTask` for easier debugging, you can 12 | * import the following file, but please comment it out in production mode 13 | * because it will have performance impact when throw error 14 | */ 15 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 16 | -------------------------------------------------------------------------------- /frontend/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arjunyel/angular-apollo-example/aa1af32b71aa61b448edab62777795ad9abf8652/frontend/src/favicon.ico -------------------------------------------------------------------------------- /frontend/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AngularApolloExample 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /frontend/src/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../coverage'), 20 | reports: ['html', 'lcovonly'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false 30 | }); 31 | }; -------------------------------------------------------------------------------- /frontend/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.log(err)); 13 | -------------------------------------------------------------------------------- /frontend/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 22 | // import 'core-js/es6/symbol'; 23 | // import 'core-js/es6/object'; 24 | // import 'core-js/es6/function'; 25 | // import 'core-js/es6/parse-int'; 26 | // import 'core-js/es6/parse-float'; 27 | // import 'core-js/es6/number'; 28 | // import 'core-js/es6/math'; 29 | // import 'core-js/es6/string'; 30 | // import 'core-js/es6/date'; 31 | // import 'core-js/es6/array'; 32 | // import 'core-js/es6/regexp'; 33 | // import 'core-js/es6/map'; 34 | // import 'core-js/es6/weak-map'; 35 | // import 'core-js/es6/set'; 36 | 37 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 38 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 39 | 40 | /** IE10 and IE11 requires the following for the Reflect API. */ 41 | // import 'core-js/es6/reflect'; 42 | 43 | 44 | /** Evergreen browsers require these. **/ 45 | // Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove. 46 | import 'core-js/es7/reflect'; 47 | 48 | 49 | /** 50 | * Web Animations `@angular/platform-browser/animations` 51 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 52 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 53 | **/ 54 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 55 | 56 | /** 57 | * By default, zone.js will patch all possible macroTask and DomEvents 58 | * user can disable parts of macroTask/DomEvents patch by setting following flags 59 | */ 60 | 61 | // (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 62 | // (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 63 | // (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 64 | 65 | /* 66 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 67 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 68 | */ 69 | // (window as any).__Zone_enable_cross_context_check = true; 70 | 71 | /*************************************************************************************************** 72 | * Zone JS is required by default for Angular itself. 73 | */ 74 | import 'zone.js/dist/zone'; // Included with Angular CLI. 75 | 76 | 77 | 78 | /*************************************************************************************************** 79 | * APPLICATION IMPORTS 80 | */ 81 | -------------------------------------------------------------------------------- /frontend/src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /frontend/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /frontend/src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "es2015", 6 | "types": [] 7 | }, 8 | "exclude": [ 9 | "src/test.ts", 10 | "**/*.spec.ts" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /frontend/src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "module": "commonjs", 6 | "types": [ 7 | "jasmine", 8 | "node" 9 | ] 10 | }, 11 | "files": [ 12 | "test.ts", 13 | "polyfills.ts" 14 | ], 15 | "include": [ 16 | "**/*.spec.ts", 17 | "**/*.d.ts" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /frontend/src/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "app", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "app", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "moduleResolution": "node", 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "target": "es5", 12 | "typeRoots": [ 13 | "node_modules/@types" 14 | ], 15 | "lib": [ 16 | "es2017", 17 | "dom", 18 | "esnext.asynciterable" 19 | ] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /frontend/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "arrow-return-shorthand": true, 7 | "callable-types": true, 8 | "class-name": true, 9 | "comment-format": [ 10 | true, 11 | "check-space" 12 | ], 13 | "curly": true, 14 | "deprecation": { 15 | "severity": "warn" 16 | }, 17 | "eofline": true, 18 | "forin": true, 19 | "import-blacklist": [ 20 | true, 21 | "rxjs/Rx" 22 | ], 23 | "import-spacing": true, 24 | "indent": [ 25 | true, 26 | "spaces" 27 | ], 28 | "interface-over-type-literal": true, 29 | "label-position": true, 30 | "max-line-length": [ 31 | true, 32 | 140 33 | ], 34 | "member-access": false, 35 | "member-ordering": [ 36 | true, 37 | { 38 | "order": [ 39 | "static-field", 40 | "instance-field", 41 | "static-method", 42 | "instance-method" 43 | ] 44 | } 45 | ], 46 | "no-arg": true, 47 | "no-bitwise": true, 48 | "no-console": [ 49 | true, 50 | "debug", 51 | "info", 52 | "time", 53 | "timeEnd", 54 | "trace" 55 | ], 56 | "no-construct": true, 57 | "no-debugger": true, 58 | "no-duplicate-super": true, 59 | "no-empty": false, 60 | "no-empty-interface": true, 61 | "no-eval": true, 62 | "no-inferrable-types": [ 63 | true, 64 | "ignore-params" 65 | ], 66 | "no-misused-new": true, 67 | "no-non-null-assertion": true, 68 | "no-shadowed-variable": true, 69 | "no-string-literal": false, 70 | "no-string-throw": true, 71 | "no-switch-case-fall-through": true, 72 | "no-trailing-whitespace": true, 73 | "no-unnecessary-initializer": true, 74 | "no-unused-expression": true, 75 | "no-use-before-declare": true, 76 | "no-var-keyword": true, 77 | "object-literal-sort-keys": false, 78 | "one-line": [ 79 | true, 80 | "check-open-brace", 81 | "check-catch", 82 | "check-else", 83 | "check-whitespace" 84 | ], 85 | "prefer-const": true, 86 | "quotemark": [ 87 | true, 88 | "single" 89 | ], 90 | "radix": true, 91 | "semicolon": [ 92 | true, 93 | "always" 94 | ], 95 | "triple-equals": [ 96 | true, 97 | "allow-null-check" 98 | ], 99 | "typedef-whitespace": [ 100 | true, 101 | { 102 | "call-signature": "nospace", 103 | "index-signature": "nospace", 104 | "parameter": "nospace", 105 | "property-declaration": "nospace", 106 | "variable-declaration": "nospace" 107 | } 108 | ], 109 | "unified-signatures": true, 110 | "variable-name": false, 111 | "whitespace": [ 112 | true, 113 | "check-branch", 114 | "check-decl", 115 | "check-operator", 116 | "check-separator", 117 | "check-type" 118 | ], 119 | "no-output-on-prefix": true, 120 | "use-input-property-decorator": true, 121 | "use-output-property-decorator": true, 122 | "use-host-property-decorator": true, 123 | "no-input-rename": true, 124 | "no-output-rename": true, 125 | "use-life-cycle-interface": true, 126 | "use-pipe-transform-interface": true, 127 | "component-class-suffix": true, 128 | "directive-class-suffix": true 129 | } 130 | } 131 | --------------------------------------------------------------------------------