├── client ├── src │ ├── assets │ │ └── .gitkeep │ ├── app │ │ ├── app.component.css │ │ ├── follow-list │ │ │ ├── follow-list.component.css │ │ │ ├── follow-list.component.html │ │ │ ├── follow-list.component.spec.ts │ │ │ └── follow-list.component.ts │ │ ├── follow-list-item │ │ │ ├── follow-list-item.component.css │ │ │ ├── follow-list-item.component.html │ │ │ ├── follow-list-item.component.ts │ │ │ └── follow-list-item.component.spec.ts │ │ ├── follow-user-form │ │ │ ├── follow-user-form.component.css │ │ │ ├── follow-user-form.component.html │ │ │ ├── follow-user-form.component.spec.ts │ │ │ └── follow-user-form.component.ts │ │ ├── app.component.ts │ │ ├── graphql │ │ │ ├── follow.mutation.ts │ │ │ └── me.query.ts │ │ ├── apollo │ │ │ └── client.ts │ │ ├── app.module.ts │ │ ├── app.component.spec.ts │ │ └── app.component.html │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── styles.css │ ├── favicon.ico │ ├── typings.d.ts │ ├── tsconfig.app.json │ ├── index.html │ ├── main.ts │ ├── tsconfig.spec.json │ ├── test.ts │ └── polyfills.ts ├── e2e │ ├── tsconfig.e2e.json │ ├── app.po.ts │ └── app.e2e-spec.ts ├── .editorconfig ├── tsconfig.json ├── .gitignore ├── protractor.conf.js ├── karma.conf.js ├── README.md ├── .angular-cli.json ├── package.json └── tslint.json ├── .tortilla └── manuals │ ├── views │ ├── root.md │ ├── step1.md │ ├── step6.md │ ├── step4.md │ └── step2.md │ └── templates │ ├── step6.tmpl │ ├── root.tmpl │ ├── step1.tmpl │ ├── step3.tmpl │ ├── step5.tmpl │ ├── step4.tmpl │ └── step2.tmpl ├── .gitignore ├── package.json ├── server ├── package.json └── src │ ├── index.js │ ├── schema.js │ └── github-connector.js └── README.md /client/src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/app/app.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.tortilla/manuals/views/root.md: -------------------------------------------------------------------------------- 1 | ../../../README.md -------------------------------------------------------------------------------- /client/src/app/follow-list/follow-list.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | .idea 4 | -------------------------------------------------------------------------------- /client/src/app/follow-list-item/follow-list-item.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/app/follow-user-form/follow-user-form.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /client/src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /client/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotansimha/graphql-angular-workshop/HEAD/client/src/favicon.ico -------------------------------------------------------------------------------- /client/src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /* SystemJS module definition */ 2 | declare var module: NodeModule; 3 | interface NodeModule { 4 | id: string; 5 | } 6 | -------------------------------------------------------------------------------- /client/src/app/follow-list-item/follow-list-item.component.html: -------------------------------------------------------------------------------- 1 |
  • 2 | {{ user.name && user.name !== '' ? user.name : user.login }} 3 |
  • -------------------------------------------------------------------------------- /client/src/app/follow-user-form/follow-user-form.component.html: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 4 | 5 |
    -------------------------------------------------------------------------------- /client/src/app/follow-list/follow-list.component.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /client/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.css'] 7 | }) 8 | export class AppComponent { 9 | } 10 | -------------------------------------------------------------------------------- /client/src/app/graphql/follow.mutation.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | export const FollowMutation = gql` 4 | mutation follow($login: String!) { 5 | follow(login: $login) { 6 | id 7 | name 8 | login 9 | } 10 | }`; 11 | -------------------------------------------------------------------------------- /client/e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "node" 10 | ] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /client/e2e/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class ClientPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /client/src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "es2015", 6 | "baseUrl": "", 7 | "types": [] 8 | }, 9 | "exclude": [ 10 | "test.ts", 11 | "**/*.spec.ts" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /client/.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 | -------------------------------------------------------------------------------- /client/src/app/graphql/me.query.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | export const MeQuery = gql` 4 | query Me($page: Int!, $perPage: Int!) { 5 | me { 6 | id 7 | followingCount 8 | following(page: $page, perPage: $perPage) { 9 | name 10 | login 11 | } 12 | } 13 | } 14 | `; 15 | -------------------------------------------------------------------------------- /client/src/app/apollo/client.ts: -------------------------------------------------------------------------------- 1 | import { ApolloClient, createNetworkInterface } from 'apollo-client'; 2 | 3 | export const client = new ApolloClient({ 4 | networkInterface: createNetworkInterface({ 5 | uri: 'http://localhost:3001/graphql', 6 | }), 7 | }); 8 | 9 | export function provideClient(): ApolloClient { 10 | return client; 11 | } 12 | -------------------------------------------------------------------------------- /client/src/app/follow-list-item/follow-list-item.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-follow-list-item', 5 | templateUrl: './follow-list-item.component.html', 6 | styleUrls: ['./follow-list-item.component.css'] 7 | }) 8 | export class FollowListItemComponent { 9 | @Input() user: any; 10 | } 11 | -------------------------------------------------------------------------------- /client/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Client 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /client/e2e/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { ClientPage } from './app.po'; 2 | 3 | describe('client App', () => { 4 | let page: ClientPage; 5 | 6 | beforeEach(() => { 7 | page = new ClientPage(); 8 | }); 9 | 10 | it('should display welcome message', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('Welcome to app!!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /client/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 | -------------------------------------------------------------------------------- /client/src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "baseUrl": "", 8 | "types": [ 9 | "jasmine", 10 | "node" 11 | ] 12 | }, 13 | "files": [ 14 | "test.ts" 15 | ], 16 | "include": [ 17 | "**/*.spec.ts", 18 | "**/*.d.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /client/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `.angular-cli.json`. 5 | 6 | export const environment = { 7 | production: false 8 | }; 9 | -------------------------------------------------------------------------------- /client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "outDir": "./dist/out-tsc", 5 | "baseUrl": "src", 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 | "es2016", 17 | "dom" 18 | ] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-angular-workshop", 3 | "description": "GraphQL and Angular 2 workshop", 4 | "scripts": { 5 | "server": "cd server && yarn watch", 6 | "web": "cd client && ng server --open", 7 | "start": "concurrently \"yarn web\" \"yarn server\"", 8 | "postinstall": "(cd client && yarn) && (cd server && yarn)" 9 | }, 10 | "keywords": [], 11 | "author": "Dotan Simha", 12 | "license": "MIT", 13 | "bugs": { 14 | "url": "https://github.com/dotansimha/graphql-angular-workshop/issues" 15 | }, 16 | "homepage": "https://github.com/dotansimha/graphql-angular-workshop#readme", 17 | "devDependencies": { 18 | "concurrently": "^3.1.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /client/.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 | testem.log 34 | /typings 35 | 36 | # e2e 37 | /e2e/*.js 38 | /e2e/*.map 39 | 40 | # System Files 41 | .DS_Store 42 | Thumbs.db 43 | -------------------------------------------------------------------------------- /client/src/app/follow-list/follow-list.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { FollowListComponent } from './follow-list.component'; 4 | 5 | describe('FollowListComponent', () => { 6 | let component: FollowListComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ FollowListComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(FollowListComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should be created', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "workshop-server", 3 | "version": "1.0.0", 4 | "description": "GraphQL Basic server", 5 | "main": "index.js", 6 | "license": "MIT", 7 | "scripts": { 8 | "start": "node src/index.js", 9 | "watch": "nodemon src/index.js" 10 | }, 11 | "keywords": [ 12 | "tutorial", 13 | "graphql", 14 | "apollo", 15 | "server", 16 | "express" 17 | ], 18 | "dependencies": { 19 | "body-parser": "^1.17.2", 20 | "casual": "^1.5.14", 21 | "cors": "^2.8.3", 22 | "dataloader": "^1.3.0", 23 | "express": "^4.15.3", 24 | "graphql": "^0.10.3", 25 | "graphql-server-express": "^0.9.0", 26 | "graphql-tools": "^1.0.0", 27 | "morgan": "^1.8.2", 28 | "node-fetch": "^1.7.1" 29 | }, 30 | "devDependencies": { 31 | "nodemon": "^1.11.0" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /client/src/app/follow-list-item/follow-list-item.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { FollowListItemComponent } from './follow-list-item.component'; 4 | 5 | describe('FollowListItemComponent', () => { 6 | let component: FollowListItemComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ FollowListItemComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(FollowListItemComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should be created', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /client/src/app/follow-user-form/follow-user-form.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { FollowUserFormComponent } from './follow-user-form.component'; 4 | 5 | describe('FollowUserFormComponent', () => { 6 | let component: FollowUserFormComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ FollowUserFormComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(FollowUserFormComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should be created', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /client/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 | './e2e/**/*.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: 'e2e/tsconfig.e2e.json' 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /server/src/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const bodyParser = require('body-parser'); 3 | const morgan = require('morgan'); 4 | const cors = require('cors'); 5 | const { GithubConnector } = require('./github-connector'); 6 | const { graphqlExpress, graphiqlExpress } = require('graphql-server-express'); 7 | 8 | const { Schema } = require('./schema'); 9 | 10 | const GITHUB_LOGIN = ''; 11 | const GITHUB_ACCESS_TOKEN = ''; 12 | 13 | const app = express(); 14 | 15 | app.use(cors()); 16 | app.use(morgan('tiny')); 17 | 18 | app.use('/graphql', bodyParser.json(), graphqlExpress({ 19 | schema: Schema, 20 | context: { 21 | githubConnector: new GithubConnector(GITHUB_ACCESS_TOKEN), 22 | user: { login: GITHUB_LOGIN }, 23 | } 24 | })); 25 | 26 | app.use('/graphiql', graphiqlExpress({ 27 | endpointURL: '/graphql', 28 | })); 29 | 30 | app.listen(3001); 31 | -------------------------------------------------------------------------------- /client/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | import { FormsModule } from '@angular/forms'; 4 | 5 | import { AppComponent } from './app.component'; 6 | import { FollowListItemComponent } from './follow-list-item/follow-list-item.component'; 7 | import { FollowListComponent } from './follow-list/follow-list.component'; 8 | import { ApolloModule } from 'apollo-angular'; 9 | import { provideClient } from './apollo/client'; 10 | import { FollowUserFormComponent } from './follow-user-form/follow-user-form.component'; 11 | 12 | @NgModule({ 13 | declarations: [ 14 | AppComponent, 15 | FollowListItemComponent, 16 | FollowListComponent, 17 | FollowUserFormComponent 18 | ], 19 | imports: [ 20 | BrowserModule, 21 | ApolloModule.forRoot(provideClient), 22 | FormsModule, 23 | ], 24 | providers: [], 25 | bootstrap: [AppComponent] 26 | }) 27 | export class AppModule { } 28 | -------------------------------------------------------------------------------- /client/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/0.13/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular/cli'], 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/cli/plugins/karma') 14 | ], 15 | client:{ 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | reports: [ 'html', 'lcovonly' ], 20 | fixWebpackSourcePaths: true 21 | }, 22 | angularCli: { 23 | environment: 'dev' 24 | }, 25 | reporters: ['progress', 'kjhtml'], 26 | port: 9876, 27 | colors: true, 28 | logLevel: config.LOG_INFO, 29 | autoWatch: true, 30 | browsers: ['Chrome'], 31 | singleRun: false 32 | }); 33 | }; 34 | -------------------------------------------------------------------------------- /client/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | 3 | import { AppComponent } from './app.component'; 4 | 5 | describe('AppComponent', () => { 6 | beforeEach(async(() => { 7 | TestBed.configureTestingModule({ 8 | declarations: [ 9 | AppComponent 10 | ], 11 | }).compileComponents(); 12 | })); 13 | 14 | it('should create the app', async(() => { 15 | const fixture = TestBed.createComponent(AppComponent); 16 | const app = fixture.debugElement.componentInstance; 17 | expect(app).toBeTruthy(); 18 | })); 19 | 20 | it(`should have as title 'app'`, async(() => { 21 | const fixture = TestBed.createComponent(AppComponent); 22 | const app = fixture.debugElement.componentInstance; 23 | expect(app.title).toEqual('app'); 24 | })); 25 | 26 | it('should render title in a h1 tag', async(() => { 27 | const fixture = TestBed.createComponent(AppComponent); 28 | fixture.detectChanges(); 29 | const compiled = fixture.debugElement.nativeElement; 30 | expect(compiled.querySelector('h1').textContent).toContain('Welcome to app!!'); 31 | })); 32 | }); 33 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | # Client 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 1.1.3. 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|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 | Before running the tests make sure you are serving the app via `ng serve`. 25 | 26 | ## Further help 27 | 28 | 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). 29 | -------------------------------------------------------------------------------- /client/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/long-stack-trace-zone'; 4 | import 'zone.js/dist/proxy.js'; 5 | import 'zone.js/dist/sync-test'; 6 | import 'zone.js/dist/jasmine-patch'; 7 | import 'zone.js/dist/async-test'; 8 | import 'zone.js/dist/fake-async-test'; 9 | import { getTestBed } from '@angular/core/testing'; 10 | import { 11 | BrowserDynamicTestingModule, 12 | platformBrowserDynamicTesting 13 | } from '@angular/platform-browser-dynamic/testing'; 14 | 15 | // Unfortunately there's no typing for the `__karma__` variable. Just declare it as any. 16 | declare const __karma__: any; 17 | declare const require: any; 18 | 19 | // Prevent Karma from running prematurely. 20 | __karma__.loaded = function () {}; 21 | 22 | // First, initialize the Angular testing environment. 23 | getTestBed().initTestEnvironment( 24 | BrowserDynamicTestingModule, 25 | platformBrowserDynamicTesting() 26 | ); 27 | // Then we find all the tests. 28 | const context = require.context('./', true, /\.spec\.ts$/); 29 | // And load the modules. 30 | context.keys().map(context); 31 | // Finally, start Karma to run the tests. 32 | __karma__.start(); 33 | -------------------------------------------------------------------------------- /.tortilla/manuals/templates/step6.tmpl: -------------------------------------------------------------------------------- 1 | Our last step of the tutorial is to add pagination (load more) feature to the list. 2 | 3 | So let's add the basics of pagination - we need an indication for the current page, the amount of items to load per page, and we need to add it to the GraphQL Query so we can pass these variables from the client to the server. 4 | 5 | Our GraphQL API can also tell us the total amount of items (called `followingCount`), so we will also add this field in our Query, and use it's value to show/hide the "load more" button. 6 | 7 | {{{ diffStep 6.1 }}} 8 | 9 | Now, all we have to do is to add a "load more" button to the template, that triggers a class method. And we also need to hide this button when there are no more items to load. 10 | 11 | Apollo-client API allows us to use our existing query and execute `fetchMore` with new variables, and then to get more data. 12 | 13 | We can also use the new data with a mechanism called `updateQuery` (the same as `updateQueries` we used in step 5) to patch the cache and append the new page of data. 14 | 15 | {{{ diffStep 6.2 }}} 16 | 17 | As you can see, we are using the Query result now for another use: subscribing to Query data changes and check `followingCount` every time the data changes, and update the class property `hasMoreToLoad` (which show/hide the "load more" button). 18 | 19 | -------------------------------------------------------------------------------- /server/src/schema.js: -------------------------------------------------------------------------------- 1 | const { makeExecutableSchema } = require('graphql-tools'); 2 | 3 | const typeDefs = ` 4 | schema { 5 | query: Query 6 | mutation: Mutation 7 | } 8 | 9 | type Query { 10 | me: User 11 | } 12 | 13 | type Mutation { 14 | follow(login: String!): User 15 | } 16 | 17 | type User { 18 | id: ID! 19 | login: String! 20 | name: String 21 | followingCount: Int 22 | following(page: Int = 0, perPage: Int = 10): [User] 23 | } 24 | `; 25 | 26 | const resolvers = { 27 | Query: { 28 | me(_, args, { githubConnector, user }) { 29 | return githubConnector.getUserForLogin(user.login); 30 | } 31 | }, 32 | Mutation: { 33 | follow(_, { login }, { githubConnector }) { 34 | return githubConnector.follow(login) 35 | .then(() => githubConnector.getUserForLogin(login)) 36 | }, 37 | }, 38 | User: { 39 | following(user, { page, perPage }, { githubConnector }) { 40 | return githubConnector.getFollowingForLogin(user.login, page, perPage) 41 | .then(users => 42 | users.map(user => githubConnector.getUserForLogin(user.login)) 43 | ); 44 | }, 45 | followingCount: user => user.following, 46 | } 47 | }; 48 | 49 | const Schema = makeExecutableSchema({ typeDefs, resolvers }); 50 | 51 | module.exports = { 52 | Schema, 53 | }; 54 | -------------------------------------------------------------------------------- /client/.angular-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "project": { 4 | "name": "client" 5 | }, 6 | "apps": [ 7 | { 8 | "root": "src", 9 | "outDir": "dist", 10 | "assets": [ 11 | "assets", 12 | "favicon.ico" 13 | ], 14 | "index": "index.html", 15 | "main": "main.ts", 16 | "polyfills": "polyfills.ts", 17 | "test": "test.ts", 18 | "tsconfig": "tsconfig.app.json", 19 | "testTsconfig": "tsconfig.spec.json", 20 | "prefix": "app", 21 | "styles": [ 22 | "styles.css" 23 | ], 24 | "scripts": [], 25 | "environmentSource": "environments/environment.ts", 26 | "environments": { 27 | "dev": "environments/environment.ts", 28 | "prod": "environments/environment.prod.ts" 29 | } 30 | } 31 | ], 32 | "e2e": { 33 | "protractor": { 34 | "config": "./protractor.conf.js" 35 | } 36 | }, 37 | "lint": [ 38 | { 39 | "project": "src/tsconfig.app.json" 40 | }, 41 | { 42 | "project": "src/tsconfig.spec.json" 43 | }, 44 | { 45 | "project": "e2e/tsconfig.e2e.json" 46 | } 47 | ], 48 | "test": { 49 | "karma": { 50 | "config": "./karma.conf.js" 51 | } 52 | }, 53 | "defaults": { 54 | "styleExt": "css", 55 | "component": {} 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /.tortilla/manuals/templates/root.tmpl: -------------------------------------------------------------------------------- 1 | ![GraphQL and Angular 2](https://cdn-images-1.medium.com/max/608/1*yTMBzO8zfEhKr4Lky6pjZQ.png) 2 | 3 | This workshop goes through the following: 4 | 5 | - How to create GraphQL schema and server (with Express). 6 | - Creating Angular 2 application with `@angular/cli` and fetch data from GraphQL server with Apollo. 7 | - Wrap existing REST services with GraphQL, and optimize it with Dataloader. 8 | - Implement GraphQL mutations with optimistic response. 9 | - Implement client-side pagination with Angular 2, RxJS and GraphQL. 10 | 11 | ### [Contact me for on-site workshops and training!](mailto:dotansimha@gmail.com) 12 | 13 | > If you are looking for a similar workshop using GraphQL and React, check out [graphql-react-workshop](https://github.com/davidyaha/graphql-workshop) by @davidyaha ! 14 | 15 | ### Chapters 16 | 17 | - **[Step 1](.tortilla/manuals/views/step1.md)** - GraphQL Basics 18 | - **[Step 2](.tortilla/manuals/views/step2.md)** - Create GraphQL Server and Schema 19 | - **[Step 3](.tortilla/manuals/views/step3.md)** - Create Angular 2 application with GraphQL and Apollo 20 | - **[Step 4](.tortilla/manuals/views/step4.md)** - Fetch data from external data sources 21 | - **[Step 5](.tortilla/manuals/views/step5.md)** - GraphQL Mutations and Optimistic Response 22 | - **[Step 6](.tortilla/manuals/views/step6.md)** - Pagination 23 | 24 | 25 | > This repository and tutorial created with [Tortilla](https://github.com/Urigo/tortilla). 26 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "ng": "ng", 7 | "start": "ng serve", 8 | "build": "ng build", 9 | "test": "ng test", 10 | "lint": "ng lint", 11 | "e2e": "ng e2e" 12 | }, 13 | "private": true, 14 | "dependencies": { 15 | "@angular/animations": "^4.0.0", 16 | "@angular/common": "^4.0.0", 17 | "@angular/compiler": "^4.0.0", 18 | "@angular/core": "^4.0.0", 19 | "@angular/forms": "^4.2.4", 20 | "@angular/http": "^4.0.0", 21 | "@angular/platform-browser": "^4.0.0", 22 | "@angular/platform-browser-dynamic": "^4.0.0", 23 | "@angular/router": "^4.0.0", 24 | "apollo-angular": "^0.13.0", 25 | "apollo-client": "^1.5.0", 26 | "core-js": "^2.4.1", 27 | "graphql-tag": "^2.4.2", 28 | "immutability-helper": "^2.2.2", 29 | "rxjs": "^5.1.0", 30 | "zone.js": "^0.8.4" 31 | }, 32 | "devDependencies": { 33 | "@angular/cli": "1.1.3", 34 | "@angular/compiler-cli": "^4.0.0", 35 | "@angular/language-service": "^4.0.0", 36 | "@types/jasmine": "2.5.45", 37 | "@types/node": "~6.0.60", 38 | "codelyzer": "~3.0.1", 39 | "jasmine-core": "~2.6.2", 40 | "jasmine-spec-reporter": "~4.1.0", 41 | "karma": "~1.7.0", 42 | "karma-chrome-launcher": "~2.1.1", 43 | "karma-cli": "~1.0.1", 44 | "karma-coverage-istanbul-reporter": "^1.2.1", 45 | "karma-jasmine": "~1.1.0", 46 | "karma-jasmine-html-reporter": "^0.2.2", 47 | "protractor": "~5.1.2", 48 | "ts-node": "~3.0.4", 49 | "tslint": "~5.3.2", 50 | "typescript": "~2.3.3" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GraphQL (Apollo) and Angular workshop 2 | 3 | ![GraphQL and Angular 2](https://cdn-images-1.medium.com/max/608/1*yTMBzO8zfEhKr4Lky6pjZQ.png) 4 | 5 | This workshop goes through the following: 6 | 7 | - How to create GraphQL schema and server (with Express). 8 | - Creating Angular 2 application with `@angular/cli` and fetch data from GraphQL server with Apollo. 9 | - Wrap existing REST services with GraphQL, and optimize it with Dataloader. 10 | - Implement GraphQL mutations with optimistic response. 11 | - Implement client-side pagination with Angular 2, RxJS and GraphQL. 12 | 13 | ### [Contact me for on-site workshops and training!](mailto:dotansimha@gmail.com) 14 | 15 | > If you are looking for a similar workshop using GraphQL and React, check out [graphql-react-workshop](https://github.com/davidyaha/graphql-workshop) by @davidyaha ! 16 | 17 | ### Chapters 18 | 19 | - **[Step 1](.tortilla/manuals/views/step1.md)** - GraphQL Basics 20 | - **[Step 2](.tortilla/manuals/views/step2.md)** - Create GraphQL Server and Schema 21 | - **[Step 3](.tortilla/manuals/views/step3.md)** - Create Angular 2 application with GraphQL and Apollo 22 | - **[Step 4](.tortilla/manuals/views/step4.md)** - Fetch data from external data sources 23 | - **[Step 5](.tortilla/manuals/views/step5.md)** - GraphQL Mutations and Optimistic Response 24 | - **[Step 6](.tortilla/manuals/views/step6.md)** - Pagination 25 | 26 | 27 | > This repository and tutorial created with [Tortilla](https://github.com/Urigo/tortilla). 28 | 29 | [{]: (navStep) 30 | 31 | | [Begin Tutorial >](.tortilla/manuals/views/step1.md) | 32 | |----------------------:| 33 | 34 | [}]: # 35 | -------------------------------------------------------------------------------- /client/src/app/follow-list/follow-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Apollo } from 'apollo-angular'; 3 | import { MeQuery } from '../graphql/me.query'; 4 | import update from 'immutability-helper'; 5 | import 'rxjs/add/operator/map'; 6 | 7 | const PER_PAGE = 10; 8 | 9 | @Component({ 10 | selector: 'app-follow-list', 11 | templateUrl: './follow-list.component.html', 12 | styleUrls: ['./follow-list.component.css'] 13 | }) 14 | export class FollowListComponent implements OnInit { 15 | private items$: any; 16 | private currentPage: number = 1; 17 | private hasMoreToLoad: boolean = false; 18 | 19 | constructor(private apollo: Apollo) { 20 | } 21 | 22 | ngOnInit() { 23 | this.items$ = this.apollo.watchQuery({ 24 | query: MeQuery, 25 | variables: { 26 | perPage: PER_PAGE, 27 | page: this.currentPage, 28 | }, 29 | }).map(({ data }) => data.me); 30 | 31 | this.items$.subscribe(({ followingCount }) => { 32 | this.hasMoreToLoad = this.currentPage * PER_PAGE < followingCount; 33 | }); 34 | } 35 | 36 | loadMore() { 37 | if (!this.hasMoreToLoad) { 38 | return; 39 | } 40 | 41 | this.currentPage = this.currentPage + 1; 42 | 43 | this.items$.fetchMore({ 44 | variables: { 45 | page: this.currentPage, 46 | }, 47 | updateQuery: (prev: any, { fetchMoreResult }: { fetchMoreResult: any }) => { 48 | if (!fetchMoreResult.me) { 49 | return prev; 50 | } 51 | 52 | return update(prev, { 53 | me: { 54 | following: { 55 | $push: fetchMoreResult.me.following, 56 | }, 57 | } 58 | }); 59 | } 60 | }) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /client/src/app/follow-user-form/follow-user-form.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Apollo } from 'apollo-angular'; 3 | import update from 'immutability-helper'; 4 | import { FollowMutation } from '../graphql/follow.mutation'; 5 | 6 | @Component({ 7 | selector: 'app-follow-user-form', 8 | templateUrl: './follow-user-form.component.html', 9 | styleUrls: ['./follow-user-form.component.css'] 10 | }) 11 | export class FollowUserFormComponent implements OnInit { 12 | private usernameToFollow: string = ''; 13 | 14 | constructor(private apollo: Apollo) { 15 | } 16 | 17 | ngOnInit() { 18 | } 19 | 20 | follow() { 21 | if (this.usernameToFollow === '') { 22 | return; 23 | } 24 | 25 | this.apollo.mutate({ 26 | mutation: FollowMutation, 27 | variables: { 28 | login: this.usernameToFollow, 29 | }, 30 | optimisticResponse: { 31 | __typename: 'Mutation', 32 | follow: { 33 | __typename: 'User', 34 | id: '', 35 | name: '', 36 | login: this.usernameToFollow, 37 | }, 38 | }, 39 | updateQueries: { 40 | Me: (prev: any, { mutationResult }: { mutationResult: any }) => { 41 | const result = mutationResult.data.follow; 42 | 43 | if (prev.me.following && prev.me.following.find(followingUser => followingUser.login === result.login)) { 44 | return prev; 45 | } 46 | 47 | return update(prev, { 48 | me: { 49 | following: { 50 | $push: [result] 51 | }, 52 | }, 53 | }); 54 | }, 55 | } 56 | }).subscribe(() => { 57 | this.usernameToFollow = ''; 58 | }); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /server/src/github-connector.js: -------------------------------------------------------------------------------- 1 | const fetch = require('node-fetch'); 2 | const DataLoader = require('dataloader'); 3 | 4 | class GithubConnector { 5 | constructor(accessToken) { 6 | this.accessToken = accessToken; 7 | this.dataLoader = new DataLoader(this.fetchAll.bind(this), { batch: false }); 8 | } 9 | 10 | getUserForLogin(login) { 11 | return this.getFromGithub(`/users/${login}`); 12 | } 13 | 14 | getFollowingForLogin(login, page, perPage) { 15 | return this.getFromGithub(`/users/${login}/following`, page, perPage); 16 | } 17 | 18 | getFromGithub(relativeUrl, page, perPage) { 19 | const url = `https://api.github.com${relativeUrl}?access_token=${this.accessToken}`; 20 | 21 | return this.dataLoader.load(this.paginate(url, page, perPage)); 22 | } 23 | 24 | follow( login ) { 25 | return this.putToGithub(`/user/following/${login}`); 26 | } 27 | 28 | putToGithub( relativeUrl ) { 29 | const url = `https://api.github.com${relativeUrl}?access_token=${this.accessToken}`; 30 | 31 | const options = { method: 'PUT', headers: { 'Content-Length': 0 } }; 32 | return fetch(url, options).then(() => this.dataLoader.clearAll()); 33 | } 34 | 35 | paginate(url, page, perPage) { 36 | let transformed = url.indexOf('?') !== -1 ? url : url + '?'; 37 | 38 | if (page) { 39 | transformed = `${transformed}&page=${page}` 40 | } 41 | 42 | if (perPage) { 43 | transformed = `${transformed}&per_page=${perPage}` 44 | } 45 | 46 | return transformed; 47 | } 48 | 49 | fetchAll(urls) { 50 | return Promise.all( 51 | urls.map(url => { 52 | console.log('Fetching Url', url); 53 | return fetch(url).then(res => res.json()) 54 | }) 55 | ); 56 | } 57 | } 58 | 59 | module.exports = { 60 | GithubConnector, 61 | }; -------------------------------------------------------------------------------- /client/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 to support `@angular/animation`. */ 41 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 42 | 43 | 44 | /** Evergreen browsers require these. **/ 45 | import 'core-js/es6/reflect'; 46 | import 'core-js/es7/reflect'; 47 | 48 | 49 | /** ALL Firefox browsers require the following to support `@angular/animation`. **/ 50 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 51 | 52 | 53 | 54 | /*************************************************************************************************** 55 | * Zone JS is required by Angular itself. 56 | */ 57 | import 'zone.js/dist/zone'; // Included with Angular CLI. 58 | 59 | 60 | 61 | /*************************************************************************************************** 62 | * APPLICATION IMPORTS 63 | */ 64 | 65 | /** 66 | * Date, currency, decimal and percent pipes. 67 | * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10 68 | */ 69 | // import 'intl'; // Run `npm install --save intl`. 70 | /** 71 | * Need to import at least one locale-data with intl. 72 | */ 73 | // import 'intl/locale-data/jsonp/en'; 74 | -------------------------------------------------------------------------------- /client/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 | "eofline": true, 15 | "forin": true, 16 | "import-blacklist": [ 17 | true, 18 | "rxjs" 19 | ], 20 | "import-spacing": true, 21 | "indent": [ 22 | true, 23 | "spaces" 24 | ], 25 | "interface-over-type-literal": true, 26 | "label-position": true, 27 | "max-line-length": [ 28 | true, 29 | 140 30 | ], 31 | "member-access": false, 32 | "member-ordering": [ 33 | true, 34 | "static-before-instance", 35 | "variables-before-functions" 36 | ], 37 | "no-arg": true, 38 | "no-bitwise": true, 39 | "no-console": [ 40 | true, 41 | "debug", 42 | "info", 43 | "time", 44 | "timeEnd", 45 | "trace" 46 | ], 47 | "no-construct": true, 48 | "no-debugger": true, 49 | "no-duplicate-super": true, 50 | "no-empty": false, 51 | "no-empty-interface": true, 52 | "no-eval": true, 53 | "no-inferrable-types": [ 54 | true, 55 | "ignore-params" 56 | ], 57 | "no-misused-new": true, 58 | "no-non-null-assertion": true, 59 | "no-shadowed-variable": true, 60 | "no-string-literal": false, 61 | "no-string-throw": true, 62 | "no-switch-case-fall-through": true, 63 | "no-trailing-whitespace": true, 64 | "no-unnecessary-initializer": true, 65 | "no-unused-expression": true, 66 | "no-use-before-declare": true, 67 | "no-var-keyword": true, 68 | "object-literal-sort-keys": false, 69 | "one-line": [ 70 | true, 71 | "check-open-brace", 72 | "check-catch", 73 | "check-else", 74 | "check-whitespace" 75 | ], 76 | "prefer-const": true, 77 | "quotemark": [ 78 | true, 79 | "single" 80 | ], 81 | "radix": true, 82 | "semicolon": [ 83 | "always" 84 | ], 85 | "triple-equals": [ 86 | true, 87 | "allow-null-check" 88 | ], 89 | "typedef-whitespace": [ 90 | true, 91 | { 92 | "call-signature": "nospace", 93 | "index-signature": "nospace", 94 | "parameter": "nospace", 95 | "property-declaration": "nospace", 96 | "variable-declaration": "nospace" 97 | } 98 | ], 99 | "typeof-compare": true, 100 | "unified-signatures": true, 101 | "variable-name": false, 102 | "whitespace": [ 103 | true, 104 | "check-branch", 105 | "check-decl", 106 | "check-operator", 107 | "check-separator", 108 | "check-type" 109 | ], 110 | "directive-selector": [ 111 | true, 112 | "attribute", 113 | "app", 114 | "camelCase" 115 | ], 116 | "component-selector": [ 117 | true, 118 | "element", 119 | "app", 120 | "kebab-case" 121 | ], 122 | "use-input-property-decorator": true, 123 | "use-output-property-decorator": true, 124 | "use-host-property-decorator": true, 125 | "no-input-rename": true, 126 | "no-output-rename": true, 127 | "use-life-cycle-interface": true, 128 | "use-pipe-transform-interface": true, 129 | "component-class-suffix": true, 130 | "directive-class-suffix": true, 131 | "no-access-missing-member": true, 132 | "templates-use-public": true, 133 | "invoke-injectable": true 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /.tortilla/manuals/templates/step1.tmpl: -------------------------------------------------------------------------------- 1 | ## Step 1.0 - Setup 2 | 3 | - Sign github's pre release agreenment https://github.com/prerelease/agreement 4 | 5 | ## Step 1.1 6 | 7 | Queries - Go into github’s graphiql and run some queries - https://developer.github.com/early-access/graphql/explorer/ 8 | - Show your login, the number of followers and a list of the first 5. For each follower: show login id, name and avatarURL 9 | ```graphql 10 | { 11 | viewer { 12 | login 13 | followers(first: 5) { 14 | totalCount 15 | edges { 16 | node { 17 | login 18 | name 19 | avatarURL(size: 100) 20 | } 21 | } 22 | } 23 | } 24 | } 25 | ``` 26 | 27 | - Get the number of stargazers of some repository you know 28 | ```graphql 29 | { 30 | repository(owner: "graphql", name:"graphql-js") { 31 | id 32 | name 33 | stargazers {totalCount} 34 | } 35 | } 36 | ``` 37 | 38 | - Get issue [#462](https://github.com/graphql/graphql-js/issues/462) from graphql/graphql-js repository and copy it's id 39 | ```graphql 40 | { 41 | repository(owner: "graphql", name:"graphql-js") { 42 | id 43 | name 44 | issue(number: 462) { 45 | id 46 | title 47 | } 48 | } 49 | } 50 | ``` 51 | ---- 52 | 53 | ## Step 1.2 Now let's use mutations 54 | 55 | - Use a mutation to add a reaction to the issue with the id we've picked up on the last query 56 | ```graphql 57 | mutation { 58 | addReaction(input: {subjectId: "MDU6SXNzdWUxNzA3MzcyMzg", content: HOORAY}) { 59 | reaction { 60 | id 61 | content 62 | user { 63 | login 64 | } 65 | } 66 | } 67 | } 68 | ``` 69 | 70 | - Now query the issue again but this time get it's reactions as well 71 | ```graphql 72 | { 73 | repository(owner: "graphql", name: "graphql-js") { 74 | id 75 | name 76 | issue(number: 462) { 77 | id 78 | title 79 | reactions(last: 10) { 80 | totalCount 81 | edges { 82 | node { 83 | content 84 | user { 85 | login 86 | } 87 | } 88 | } 89 | } 90 | } 91 | } 92 | } 93 | ``` 94 | ---- 95 | 96 | ## Step 1.3 - Fragments and misc. 97 | 98 | Cool we've succesfully queried and mutated data on a GraphQL API server. Let's try using fragments 99 | 100 | - Get the repository owner "graphql" and look what are your permissions for that organization 101 | ```graphql 102 | { 103 | repositoryOwner(login:"graphql") { 104 | __typename 105 | ... on Organization { 106 | name 107 | viewerCanCreateProjects 108 | } 109 | } 110 | } 111 | ``` 112 | 113 | - Get your first follower's id, then query both the viewer(which is you) and your follower. 114 | For both users make sure to query their name and bio 115 | ```graphql 116 | { 117 | me: viewer { 118 | ...userFields 119 | } 120 | charleno: node(id: "MDQ6VXNlcjEzNjU=") { 121 | ...userFields 122 | } 123 | } 124 | 125 | fragment userFields on User { 126 | name 127 | bio 128 | } 129 | ``` 130 | 131 | - Use field aliases to make viewer query return on your own name and your first follower to return on his/her name 132 | ```graphql 133 | { 134 | david: viewer { 135 | ...userFields 136 | } 137 | charleno: node(id: "MDQ6VXNlcjEzNjU=") { 138 | ...userFields 139 | } 140 | } 141 | ``` -------------------------------------------------------------------------------- /.tortilla/manuals/views/step1.md: -------------------------------------------------------------------------------- 1 | # Step 1: GraphQL Basics 2 | 3 | ## Step 1.0 - Setup 4 | 5 | - Sign github's pre release agreenment https://github.com/prerelease/agreement 6 | 7 | ## Step 1.1 8 | 9 | Queries - Go into github’s graphiql and run some queries - https://developer.github.com/early-access/graphql/explorer/ 10 | - Show your login, the number of followers and a list of the first 5. For each follower: show login id, name and avatarURL 11 | ```graphql 12 | { 13 | viewer { 14 | login 15 | followers(first: 5) { 16 | totalCount 17 | edges { 18 | node { 19 | login 20 | name 21 | avatarURL(size: 100) 22 | } 23 | } 24 | } 25 | } 26 | } 27 | ``` 28 | 29 | - Get the number of stargazers of some repository you know 30 | ```graphql 31 | { 32 | repository(owner: "graphql", name:"graphql-js") { 33 | id 34 | name 35 | stargazers {totalCount} 36 | } 37 | } 38 | ``` 39 | 40 | - Get issue [#462](https://github.com/graphql/graphql-js/issues/462) from graphql/graphql-js repository and copy it's id 41 | ```graphql 42 | { 43 | repository(owner: "graphql", name:"graphql-js") { 44 | id 45 | name 46 | issue(number: 462) { 47 | id 48 | title 49 | } 50 | } 51 | } 52 | ``` 53 | ---- 54 | 55 | ## Step 1.2 Now let's use mutations 56 | 57 | - Use a mutation to add a reaction to the issue with the id we've picked up on the last query 58 | ```graphql 59 | mutation { 60 | addReaction(input: {subjectId: "MDU6SXNzdWUxNzA3MzcyMzg", content: HOORAY}) { 61 | reaction { 62 | id 63 | content 64 | user { 65 | login 66 | } 67 | } 68 | } 69 | } 70 | ``` 71 | 72 | - Now query the issue again but this time get it's reactions as well 73 | ```graphql 74 | { 75 | repository(owner: "graphql", name: "graphql-js") { 76 | id 77 | name 78 | issue(number: 462) { 79 | id 80 | title 81 | reactions(last: 10) { 82 | totalCount 83 | edges { 84 | node { 85 | content 86 | user { 87 | login 88 | } 89 | } 90 | } 91 | } 92 | } 93 | } 94 | } 95 | ``` 96 | ---- 97 | 98 | ## Step 1.3 - Fragments and misc. 99 | 100 | Cool we've succesfully queried and mutated data on a GraphQL API server. Let's try using fragments 101 | 102 | - Get the repository owner "graphql" and look what are your permissions for that organization 103 | ```graphql 104 | { 105 | repositoryOwner(login:"graphql") { 106 | __typename 107 | ... on Organization { 108 | name 109 | viewerCanCreateProjects 110 | } 111 | } 112 | } 113 | ``` 114 | 115 | - Get your first follower's id, then query both the viewer(which is you) and your follower. 116 | For both users make sure to query their name and bio 117 | ```graphql 118 | { 119 | me: viewer { 120 | ...userFields 121 | } 122 | charleno: node(id: "MDQ6VXNlcjEzNjU=") { 123 | ...userFields 124 | } 125 | } 126 | 127 | fragment userFields on User { 128 | name 129 | bio 130 | } 131 | ``` 132 | 133 | - Use field aliases to make viewer query return on your own name and your first follower to return on his/her name 134 | ```graphql 135 | { 136 | david: viewer { 137 | ...userFields 138 | } 139 | charleno: node(id: "MDQ6VXNlcjEzNjU=") { 140 | ...userFields 141 | } 142 | } 143 | ``` 144 | [{]: (navStep) 145 | 146 | | [< Intro](../../../README.md) | [Next Step >](step2.md) | 147 | |:--------------------------------|--------------------------------:| 148 | 149 | [}]: # 150 | -------------------------------------------------------------------------------- /.tortilla/manuals/templates/step3.tmpl: -------------------------------------------------------------------------------- 1 | In this step we will create Angular 2 application using `@angular/cli`, and we will connect it to our server. 2 | 3 | Let's start by installing the required dependencies: 4 | 5 | $ yarn global add @angular/cli 6 | // Or, with npm: 7 | $ npm install -g @angular/cli 8 | 9 | ### Create Angular 2 application 10 | 11 | Now, let's create our client application with Angular CLI: 12 | 13 | $ ng new client 14 | $ cd client 15 | 16 | Great! So we have now an Angular 2 application, let's run it: 17 | 18 | $ ng serve --open 19 | 20 | ### Create your first Component 21 | 22 | Now let's create an Angular Component for a single list item, that displays the name of the GitHub user we are following. 23 | 24 | We can create Angular 2 Component using it's CLI, by running: 25 | 26 | $ ng g component follow-list-item 27 | 28 | > Angular CLI creates a Component file, template file, CSS file and tests. It also adds it to your `NgModule` declaration file. 29 | 30 | So our new component is created under `client/app/follow-list-item/` directory, let's implement it: 31 | 32 | {{{ diffStep 3.3 files="client/src/app/follow-list-item/follow-list-item.component.html,client/src/app/follow-list-item/follow-list-item.component.ts" }}} 33 | 34 | ### Implement the list of data 35 | 36 | Now let's create the list of followers, using the same angular CLI command: 37 | 38 | $ ng g component follow-list 39 | 40 | And now we will implement the actual Component logic. 41 | 42 | We will use `Observable` as data source - Angular has a built in support for this kind of Iterables (you can read more [here](http://blog.angular-university.io/functional-reactive-programming-for-angular-2-developers-rxjs-and-observables/)) 43 | 44 | Our `Observable` will have some static data, and we will connect it to our GraphQL server later. 45 | 46 | {{{ diffStep 3.5 files= "client/src/app/follow-list/follow-list.component.ts" }}} 47 | 48 | Now, let's modify the template of this component: 49 | 50 | {{{ diffStep 3.5 files= "client/src/app/follow-list/follow-list.component.html" }}} 51 | 52 | The template generates a simple list container (`ul` tag), with `app-follow-list-item` as our list items. 53 | 54 | We iterates over the `Observable` using `ngFor` (we use `async` pipe because we are using `Observable` - this way Angular 2 known to update our UI each time the data changes). 55 | 56 | Next, let's add our following list to the main app view (`app.component.html`): 57 | 58 | {{{ diffStep 3.6 }}} 59 | 60 | ### Add GraphQL and Apollo-Client 61 | 62 | Now let's add our GraphQL client - Apollo. 63 | 64 | $ yarn add apollo-client apollo-angular graphql-tag 65 | 66 | And let's create a new file, and declare our `ApolloClient` instance. 67 | 68 | We will also use `networkInterface` to tell ApolloClient where our GraphQL server. 69 | 70 | {{{ diffStep 3.8 }}} 71 | 72 | > We added `provideClient` because we will need a pure function later for `NgModule` declaration. 73 | 74 | Now let's add a the `apollo-angular` bridge that connects our Angular application to the ApolloClient instance: 75 | 76 | {{{ diffStep 3.9 }}} 77 | 78 | ### Connect GraphQL to the Following List 79 | 80 | Now let's use GraphQL with Apollo to fetch some data from our server. 81 | 82 | To do so, we need to create GraphQL `Query`, and we need to modify our list component and use `Apollo` instead of static `Observable` implementation. 83 | 84 | Let's create our basic Query - we will fetch `me` fields and it's sub-fields: `id` and `following`, and for each following user we want to fetch it's login and name: 85 | 86 | {{{ diffStep "3.10" }}} 87 | 88 | Now let's replace the static `Observable` with real data, using the Query we just created. 89 | 90 | Apollo also support `Observable` so replacing the static one should be really simple! 91 | 92 | {{{ diffStep 3.11 files="client/src/app/follow-list/follow-list.component.ts" }}} 93 | 94 | We are using Angular's dependency injection to ask for `Apollo` instance - which is our `ApolloClient` wrapper for Angular. 95 | 96 | Next, we are using `watchQuery` with the Query we just created. 97 | 98 | We are using `rxjs` operator called `map` in order to change each value that our Query results emits, because the JSON structure is different from the one we used with the static `Observable`. 99 | 100 | Now, each value of the `Observable` will be the result of `me`: `id` (string) and `following` (array of users). But our `ngFor` iterates over the array of following, so we need to change the template to iterate over the correct field: 101 | 102 | {{{ diffStep 3.11 files="client/src/app/follow-list/follow-list.component.html" }}} 103 | 104 | > Note that we are using `(items$ | async)?.following` because we first want to map the `items$` observable, and then get the `following` array from the result. the `?` operator helps us to avoid iterating null values (while loading the data). 105 | 106 | -------------------------------------------------------------------------------- /.tortilla/manuals/templates/step5.tmpl: -------------------------------------------------------------------------------- 1 | So far we learned how to Query and fetch data from our GraphQL server, and in this step we will modify data using GraphQL Mutations. 2 | 3 | The mutation we will add is `follow` and it will add a GitHub users to own following list. 4 | 5 | We will first add it with a regular mutation behavior, and then we will update it to use optimistic response. 6 | 7 | ### Implement Mutation 8 | 9 | So our schema already has `follow` mutation declared, and we just need to implement it and call our GitHub connector: 10 | 11 | {{{ diffStep 5.1 }}} 12 | 13 | ### Add Angular Form 14 | 15 | We will add a form with a simple `` tag for the GitHub username, and a simple button that triggers the actual mutation. 16 | 17 | So let's start with adding a new Component for the form: 18 | 19 | $ ng g component follow-user-form 20 | 21 | And let's add it to the main HTML file: 22 | 23 | {{{ diffStep 5.3 }}} 24 | 25 | Now we are going to use Angular features that related to forms, so we need to add `@angular/forms`: 26 | 27 | $ yarn add @angular/forms 28 | 29 | And import it into the `NgModule`: 30 | 31 | {{{ diffStep 5.4 files="client/src/app/app.module.ts" }}} 32 | 33 | The implementation of the actual form is simple - it's just an `` tag with two-way-binding using `ngModel` of Angular, and a simple button that triggers an action in click: 34 | 35 | {{{ diffStep 5.5 }}} 36 | 37 | ### Adding GraphQL Mutation to client-side 38 | 39 | Now let's create a GraphQL file for our mutation: 40 | 41 | {{{ diffStep 5.6 }}} 42 | 43 | > We are using GraphQL variable, called `$login`, and we will later fill this variable with the form data. 44 | 45 | Next, we need to implement `follow()` method using `Apollo`, so let's add it using Angular dependency injection, and use add to trigger our GraphQL mutation: 46 | 47 | {{{ diffStep 5.7 }}} 48 | 49 | We also created a class variable called `followResultMessage` and display it - this will be our temporary feedback for the action's success. 50 | 51 | ### Optimistic Response 52 | 53 | At the moment, the user's feedback after adding sending the form is just a message that says that the user is now being followed by you. 54 | 55 | We can improve this behavior by adding optimistic response. 56 | 57 | Optimistic response is our way to predict the result of the server, and reflect it to the client immediately, and later replace it with the actual response from the server. 58 | 59 | This is a powerful feature that allows you to create good UI behavior and great experience. 60 | 61 | So our goal is to replace the simple "success" message, and add the new followed user into the following list. 62 | 63 | Apollo-client allows you to add `optimisticResponse` object to your Mutation definition, and we also need implement `updateQueries`. 64 | 65 | `updateQueries` is a mechanism that allows the developer to "patch" the Apollo-client cache, and update specific GraphQL requests with data - causing every Component that use these Queries to update. 66 | 67 | This is how we implemented `updateQueries` and `optimisticResponse` in our project: 68 | 69 | {{{ diffStep 5.8 }}} 70 | 71 | The `optimisticResponse` object must match and specify the exact GraphQL `type` that returns from the server requests, in a special field called `__typename` - this is how GraphQL identify each object. 72 | 73 | So in this case, we are returning a `Mutation` type that contains a fields called `follow` (this is the mutation itself), that contains a `User` type with the fields. We don't have all the fields to create a full UI prediction - but we do have the login - to let's use, and let's add `name` as empty string. 74 | 75 | So we know that object returned from the server, we just need to patch the cache data. 76 | 77 | The implementation of `updateQueries` is an Object, there the key is the GraphQL operation name of the Query we want to patch. 78 | 79 | The GraphQL operation name is the name that comes after the word `query ` in you Query definition, so in this case we want to patch `Me` Query, because this is where the `following` array comes from: 80 | 81 | ```graphql 82 | query Me { /// <--- This is the GraphQL operation name 83 | me { 84 | id 85 | following { 86 | ... 87 | } 88 | } 89 | } 90 | ``` 91 | 92 | Next, the callback of `updateQueries` will get the current cache state, and the mutation result. This callback actually called twice now - the first time for our optimistic response, and the for actual mutation result. 93 | 94 | We take the current state, and patch it using a tool called `update` from the package `immutability-helper`, so we are taking the result object of the mutation, and `$push` it into the existing array of following users. 95 | 96 | Don't forget to add `immutability-helper` by running: 97 | 98 | $ yarn add immutability-helper 99 | 100 | Next - let's do some minor UI change and display the GitHub login name instead of the GitHub name, when it's not available (because during the time between the optimistic response and the server response, we don't know the name of the user): 101 | 102 | {{{ diffStep "5.10" }}} 103 | 104 | Let's add another small change to the result handler - and check if the user exists in the list before adding it, so we won't have duplicates: 105 | 106 | {{{ diffStep 5.11 }}} 107 | -------------------------------------------------------------------------------- /.tortilla/manuals/views/step6.md: -------------------------------------------------------------------------------- 1 | # Step 6: Pagination 2 | 3 | Our last step of the tutorial is to add pagination (load more) feature to the list. 4 | 5 | So let's add the basics of pagination - we need an indication for the current page, the amount of items to load per page, and we need to add it to the GraphQL Query so we can pass these variables from the client to the server. 6 | 7 | Our GraphQL API can also tell us the total amount of items (called `followingCount`), so we will also add this field in our Query, and use it's value to show/hide the "load more" button. 8 | 9 | [{]: (diffStep 6.1) 10 | 11 | #### Step 6.1: Added pagination basics 12 | 13 | ##### Changed client/src/app/follow-list/follow-list.component.ts 14 | ```diff 15 | @@ -4,6 +4,8 @@ 16 | ┊ 4┊ 4┊import { MeQuery } from '../graphql/me.query'; 17 | ┊ 5┊ 5┊import 'rxjs/add/operator/map'; 18 | ┊ 6┊ 6┊ 19 | +┊ ┊ 7┊const PER_PAGE = 10; 20 | +┊ ┊ 8┊ 21 | ┊ 7┊ 9┊@Component({ 22 | ┊ 8┊10┊ selector: 'app-follow-list', 23 | ┊ 9┊11┊ templateUrl: './follow-list.component.html', 24 | ``` 25 | ```diff 26 | @@ -11,6 +13,7 @@ 27 | ┊11┊13┊}) 28 | ┊12┊14┊export class FollowListComponent implements OnInit { 29 | ┊13┊15┊ private items$: Observable; 30 | +┊ ┊16┊ private currentPage: number = 1; 31 | ┊14┊17┊ 32 | ┊15┊18┊ constructor(private apollo: Apollo) { 33 | ┊16┊19┊ } 34 | ``` 35 | ```diff 36 | @@ -18,6 +21,10 @@ 37 | ┊18┊21┊ ngOnInit() { 38 | ┊19┊22┊ this.items$ = this.apollo.watchQuery({ 39 | ┊20┊23┊ query: MeQuery, 40 | +┊ ┊24┊ variables: { 41 | +┊ ┊25┊ perPage: PER_PAGE, 42 | +┊ ┊26┊ page: this.currentPage, 43 | +┊ ┊27┊ }, 44 | ┊21┊28┊ }).map(({ data }) => data.me); 45 | ┊22┊29┊ } 46 | ┊23┊30┊} 47 | ``` 48 | 49 | ##### Changed client/src/app/graphql/me.query.ts 50 | ```diff 51 | @@ -1,10 +1,11 @@ 52 | ┊ 1┊ 1┊import gql from 'graphql-tag'; 53 | ┊ 2┊ 2┊ 54 | ┊ 3┊ 3┊export const MeQuery = gql` 55 | -┊ 4┊ ┊ query Me { 56 | +┊ ┊ 4┊ query Me($page: Int!, $perPage: Int!) { 57 | ┊ 5┊ 5┊ me { 58 | ┊ 6┊ 6┊ id 59 | -┊ 7┊ ┊ following { 60 | +┊ ┊ 7┊ followingCount 61 | +┊ ┊ 8┊ following(page: $page, perPage: $perPage) { 62 | ┊ 8┊ 9┊ name 63 | ┊ 9┊10┊ login 64 | ┊10┊11┊ } 65 | ``` 66 | 67 | [}]: # 68 | 69 | Now, all we have to do is to add a "load more" button to the template, that triggers a class method. And we also need to hide this button when there are no more items to load. 70 | 71 | Apollo-client API allows us to use our existing query and execute `fetchMore` with new variables, and then to get more data. 72 | 73 | We can also use the new data with a mechanism called `updateQuery` (the same as `updateQueries` we used in step 5) to patch the cache and append the new page of data. 74 | 75 | [{]: (diffStep 6.2) 76 | 77 | #### Step 6.2: Implemented pagination and load more 78 | 79 | ##### Changed client/src/app/follow-list/follow-list.component.html 80 | ```diff 81 | @@ -1,3 +1,4 @@ 82 | ┊1┊1┊
      83 | ┊2┊2┊ 84 | -┊3┊ ┊
    🚫↵ 85 | +┊ ┊3┊ 86 | +┊ ┊4┊🚫↵ 87 | ``` 88 | 89 | ##### Changed client/src/app/follow-list/follow-list.component.ts 90 | ```diff 91 | @@ -1,7 +1,7 @@ 92 | ┊1┊1┊import { Component, OnInit } from '@angular/core'; 93 | -┊2┊ ┊import { Observable } from 'rxjs'; 94 | ┊3┊2┊import { Apollo } from 'apollo-angular'; 95 | ┊4┊3┊import { MeQuery } from '../graphql/me.query'; 96 | +┊ ┊4┊import update from 'immutability-helper'; 97 | ┊5┊5┊import 'rxjs/add/operator/map'; 98 | ┊6┊6┊ 99 | ┊7┊7┊const PER_PAGE = 10; 100 | ``` 101 | ```diff 102 | @@ -12,8 +12,9 @@ 103 | ┊12┊12┊ styleUrls: ['./follow-list.component.css'] 104 | ┊13┊13┊}) 105 | ┊14┊14┊export class FollowListComponent implements OnInit { 106 | -┊15┊ ┊ private items$: Observable; 107 | +┊ ┊15┊ private items$: any; 108 | ┊16┊16┊ private currentPage: number = 1; 109 | +┊ ┊17┊ private hasMoreToLoad: boolean = false; 110 | ┊17┊18┊ 111 | ┊18┊19┊ constructor(private apollo: Apollo) { 112 | ┊19┊20┊ } 113 | ``` 114 | ```diff 115 | @@ -26,5 +27,36 @@ 116 | ┊26┊27┊ page: this.currentPage, 117 | ┊27┊28┊ }, 118 | ┊28┊29┊ }).map(({ data }) => data.me); 119 | +┊ ┊30┊ 120 | +┊ ┊31┊ this.items$.subscribe(({ followingCount }) => { 121 | +┊ ┊32┊ this.hasMoreToLoad = this.currentPage * PER_PAGE < followingCount; 122 | +┊ ┊33┊ }); 123 | +┊ ┊34┊ } 124 | +┊ ┊35┊ 125 | +┊ ┊36┊ loadMore() { 126 | +┊ ┊37┊ if (!this.hasMoreToLoad) { 127 | +┊ ┊38┊ return; 128 | +┊ ┊39┊ } 129 | +┊ ┊40┊ 130 | +┊ ┊41┊ this.currentPage = this.currentPage + 1; 131 | +┊ ┊42┊ 132 | +┊ ┊43┊ this.items$.fetchMore({ 133 | +┊ ┊44┊ variables: { 134 | +┊ ┊45┊ page: this.currentPage, 135 | +┊ ┊46┊ }, 136 | +┊ ┊47┊ updateQuery: (prev: any, { fetchMoreResult }: { fetchMoreResult: any }) => { 137 | +┊ ┊48┊ if (!fetchMoreResult.me) { 138 | +┊ ┊49┊ return prev; 139 | +┊ ┊50┊ } 140 | +┊ ┊51┊ 141 | +┊ ┊52┊ return update(prev, { 142 | +┊ ┊53┊ me: { 143 | +┊ ┊54┊ following: { 144 | +┊ ┊55┊ $push: fetchMoreResult.me.following, 145 | +┊ ┊56┊ }, 146 | +┊ ┊57┊ } 147 | +┊ ┊58┊ }); 148 | +┊ ┊59┊ } 149 | +┊ ┊60┊ }) 150 | ┊29┊61┊ } 151 | ┊30┊62┊} 152 | ``` 153 | 154 | [}]: # 155 | 156 | As you can see, we are using the Query result now for another use: subscribing to Query data changes and check `followingCount` every time the data changes, and update the class property `hasMoreToLoad` (which show/hide the "load more" button). 157 | 158 | 159 | [{]: (navStep) 160 | 161 | | [< Previous Step](step5.md) | 162 | |:----------------------| 163 | 164 | [}]: # 165 | -------------------------------------------------------------------------------- /.tortilla/manuals/templates/step4.tmpl: -------------------------------------------------------------------------------- 1 | Now that we have our app working with mocks, we would like change the mocks with real data, taken from GitHub. 2 | 3 | ## Setup 4 | 5 | - Create a github API token for your user here - [https://github.com/settings/tokens/new] 6 | Enter a description, then check the user scope and press "Generate token" button. 7 | 8 | - Create two constants on `server/index.js` file. One will hold you github login and the second will hold the token you've 9 | just created. 10 | 11 | ```javascript 12 | const GITHUB_LOGIN = 'YOUR_GITHUB_USERNAME_HERE'; 13 | const GITHUB_ACCESS_TOKEN = 'YOUR_GITHUB_TOKEN_HERE'; 14 | ``` 15 | 16 | ## Create a GitHub connector class 17 | 18 | - Create a new file under `server` directory, named `github-connector.js`. This file will hold everything that is needed in order to 19 | get data from the GitHub API. To do our REST calls we will use `fetch` from `node-fetch`. 20 | 21 | ```javascript 22 | const fetch = require('node-fetch'); 23 | ``` 24 | 25 | - Defining our `GithubConnector` class we will require the github's access token and save that on our instance: 26 | 27 | ```javascript 28 | class GithubConnector { 29 | constructor( accessToken ) { 30 | this.accessToken = accessToken; 31 | } 32 | } 33 | 34 | module.exports = { 35 | GithubConnector, 36 | }; 37 | ``` 38 | 39 | - First we need a way to get any user object using the `login` string. GitHub's REST Api defines this as a GET to the 40 | `/users/{login}` route. We will do just that while passing the responsibility of making the request and parsing the 41 | result to another method we will call `getFromGithub`. 42 | 43 | ```javascript 44 | class GithubConnector { 45 | getUserForLogin( login ) { 46 | return this.getFromGithub(`/users/${login}`); 47 | } 48 | } 49 | ``` 50 | 51 | - In order to fulfill our schema needs, we also got to have a way to get a certain user's following list. Github defines 52 | that similarly as GET to `/users/{login}/following`. Following is a list and we've already specifies in our schema, a way 53 | to control the results of this list. So we can require page and items per page here, and pass them to Github. 54 | 55 | ```javascript 56 | class GithubConnector { 57 | getFollowingForLogin( login, page, perPage ) { 58 | return this.getFromGithub(`/users/${login}/following`, page, perPage); 59 | } 60 | } 61 | ``` 62 | 63 | - All those requests will happen from this `getFromGithub` method. We will define it as 64 | `(relativeUrl, page, perPage) => Promise`. We use `fetch` to make the GET request use the `result.json()` 65 | method to get a parsed body object. We build the url using Github's API url `'https://api.github.com'` and add at the 66 | end `access_token` parameter. The responsibility of adding paginating parameters to the url, we transfer to a 67 | dedicated `paginate` method. 68 | 69 | 70 | This is how the final GitHub connection class should look like: 71 | 72 | {{{ diffStep 4.2 }}} 73 | 74 | - Our schema resolvers will be able to use the `GithubConnector` class, using a context object that is created in 75 | index.js and is passed into `graphqlExpress` middleware. Note that `user` field is also part of `context` and it holds 76 | the current user's github login. On a real setup, this will be created for every session after authenticating the user. 77 | 78 | {{{ diffStep 4.3 }}} 79 | 80 | ## Replace mocks with real data 81 | 82 | - Up until now, our schema used mocks to resolve the queried data. Now, we would like to tell our schema how it can acquire 83 | some real data. 84 | 85 | - On `schema.js` create an empty object called `resolvers` and pass it into `makeExecutableSchema`. 86 | 87 | ```javascript 88 | const resolvers = {}; 89 | const Schema = makeExecutableSchema({typeDefs, resolvers}); 90 | ``` 91 | 92 | - Now let's specify how to resolve the `Query` type. the first and only field we have on `Query` is `me`. The resolver 93 | function is being called by the graphql `execute` function with four argument. The `value` passed from the parent resolver. 94 | The `argumnets` (or `args`) passed as the field arguments. The `context` object we defined on our index.js file. And lastly 95 | the schema definition and other specific request information. The last argument is used mostly in framework like `join-monster` 96 | which allows optimization of sql database queries. It is out of our scope. 97 | 98 | - For resolving `me` we use the githubConnector we added to the `context` object. We are using the `getUserForLogin`, 99 | and passing it the logged in user that we also added to `context`. 100 | 101 | - We need to define the `User` type. The `following` field will use `getFollowingForLogin` to get the list of users 102 | that the current user is following. This list does not have all the data we need to satisfy the other `User` fields, so 103 | we need to get each user's full public profile. That is done by mapping each user to the `getUserForLogin` method. 104 | The only other resolver we need to specify is the `followingCount`. This data is available from `getUserForLogin` but 105 | is ironically called `following` on github's returned object. Other resolvers are redundant as github's response maps 106 | to our other field names (id, name, login). 107 | 108 | - We can now remove the mocks from our schema.js file and test from our web app or from graphiql 109 | 110 | So this is the resolvers implementation: 111 | 112 | {{{ diffStep 4.4 }}} 113 | 114 | 115 | ## Making fewer calls to GitHub 116 | 117 | - So our schema is working great but it has two apparent issues. One it is somewhat slow and is depending on GitHub's API 118 | to give quick responses. Second, it queries GitHub a bunch of times for each GraphQL query. If we have circular follow 119 | dependencies it will even query more than once to get the same user profile. We will now fix those problems to some 120 | extent using very simple tool from facebook called `dataloader`. 121 | 122 | - We will import `DataLoader` from the `dataloader` package on our github-connector.js file. 123 | ```javascript 124 | const DataLoader = require('dataloader'); 125 | ``` 126 | 127 | - The `DataLoader` constructor needs a function that will be able to mass load any of the objects it is required. We will 128 | also set our data loader to avoid batching requests as the GitHub API does not support batching. 129 | 130 | ```javascript 131 | new DataLoader(this.fetchAll.bind(this), { batch: false }) 132 | ``` 133 | 134 | - To implement fetchAll we just need to use `fetch` as done before on `getFromGithub` for each url we receive. After that, 135 | use `Promise.all()` to create a single promise and return that. Make sure to print each call to fetch so we would know 136 | when our data loader is using it's cache and when it's not. 137 | 138 | - We also need to change `getFromGithub` to use our data loader instead of fetch. 139 | 140 | {{{ diffStep 4.5 }}} 141 | 142 | > Note that we don't invalidate the data loader's cache. Facebook suggests we would invalidate when ever a mutation is 143 | > done. This a simple call to `clearAll` method and you can see an example for it on step 5. 144 | 145 | - You should now see that whenever you query you GraphQL API, it will get the data the first time from GitHub, causing 146 | a long response time and on the second time it will load the data from memory with a fraction of the time. 147 | -------------------------------------------------------------------------------- /.tortilla/manuals/templates/step2.tmpl: -------------------------------------------------------------------------------- 1 | In this step we will create a GraphQL server, we will implement it with NodeJS and Express. 2 | 3 | > My preference is to use Yarn as package manager - but you can also use NPM. 4 | 5 | Let's start by creating a NodeJS project: 6 | 7 | $ mkdir server 8 | $ cd server 9 | $ yarn init 10 | 11 | Great, now let's add the dependencies we need: 12 | 13 | $ yarn add graphql express graphql-tools graphql-server-express body-parser casual dataloader morgan node-fetch 14 | $ yarn add -D nodemon 15 | 16 | Now let's add some scripts to our `package.json`: 17 | 18 | ```json 19 | "scripts": { 20 | "start": "node src/index.js", 21 | "watch": "nodemon src/index.js" 22 | }, 23 | ``` 24 | 25 | Your final `package.json` should look like that: 26 | 27 | {{{ diffStep 2.1 files="server/package.json" }}} 28 | 29 | Now, after learning to work with Github's GraphQL explorer, we now want to try and learn how it's done. 30 | 31 | Our implementation will be much more simpler but it will eventually help us understand how to wrap every REST Api with GraphQL endpoint. 32 | 33 | 34 | ### Write you GraphQL schema 35 | 36 | Create a new file named schema.js. We would write our schema inside a single ES6 template string. 37 | 38 | ```js 39 | const typeDefs = ` 40 | ... our schema goes here .... 41 | `; 42 | ``` 43 | 44 | Every great schema starts with a schema declaration. It will have two fields, query of type Query and mutation of type Mutation. 45 | 46 | ```graphql 47 | schema { 48 | query: Query 49 | mutation: Mutation 50 | } 51 | ``` 52 | 53 | Next we define our Query type. We will add only one field me of type User, because this is the only thing the interests us at the moment. 54 | 55 | ```graphql 56 | type Query { 57 | me: User 58 | } 59 | ``` 60 | 61 | Now define our Mutation type. Add a field called follow. This field will get a mandatory argument called userId of the type ID. It will return the type User as well. 62 | 63 | ```graphql 64 | type Mutation { 65 | follow(userId: ID!): User 66 | } 67 | ``` 68 | 69 | > Note how we used the exclamation mark (!) to define a mandatory type. 70 | 71 | All we have left is to define our User type. A user will have id, login, name, followerCount and a list of followers. followers can accept optional skip and limit arguments to control the returned items on the followers list. We will give skip a default value of 0 and limit default value of 10. 72 | 73 | ```graphql 74 | type User { 75 | id: ID! 76 | login: String! 77 | name: String 78 | followerCount: Int 79 | followers(skip: Int = 0, limit: Int = 10): [User] 80 | } 81 | ``` 82 | 83 | Now we use apollo to parse that schema and make an executable schema out of it. 84 | 85 | Import the `makeExecutableSchema` from `graphql-tools` package, and use it to create your GraphQL schema object. 86 | 87 | This is how you file should look like: 88 | 89 | {{{ diffStep 2.2 }}} 90 | 91 | ### Create a GraphQL endpoint with your schema 92 | 93 | - Create a new file named index.js. This of course, will be our server's entry point. 94 | - Import `express`, `body-parser` and our `graphqlExpress` middleware. 95 | 96 | ```javascript 97 | const express = require('express'); 98 | const bodyParser = require('body-parser'); 99 | const {graphqlExpress} = require('graphql-server-express'); 100 | ``` 101 | 102 | - Import our `Schema` object. 103 | ```javascript 104 | const {Schema} = require('./schema'); 105 | ``` 106 | 107 | - Now we create our `express` app, and add our `bodyParser` and `graphqlExpress` middleware on `/graphql` path. 108 | 109 | ```javascript 110 | const app = express(); 111 | 112 | app.use('/graphql', bodyParser.json(), graphqlExpress({ 113 | schema: Schema, 114 | })); 115 | ``` 116 | 117 | - Lastly we need to tell `express` to start listening on some port. 118 | ```javascript 119 | app.listen(3001); 120 | ``` 121 | 122 | **Now we can go ahead and start the app by typing `npm start` in our project directory.** 123 | 124 | > If it works without errors, close it using Ctrl + C and run `npm run watch` to make our server restart when we change our files. 125 | 126 | - So now we have a GrpahQL endpoint but we don't know how to explore the API. To do just that, we will add `graphiqlExpress` middleware. 127 | ```javascript 128 | // Import graphiqlExpress from the same package 129 | const {graphqlExpress, graphiqlExpress} = require('graphql-server-express'); 130 | 131 | // ... Just before calling the listen method 132 | 133 | app.use('/graphiql', graphiqlExpress({ 134 | endpointURL: '/graphql', // This will tell the graphiql interface where to run queries 135 | })); 136 | ``` 137 | 138 | This is how your file should looks like: 139 | 140 | {{{ diffStep 2.3 }}} 141 | 142 | - Now open your browser on http://localhost:3001/graphiql and start exploring the schema we've just wrote! 143 | 144 | - Try to run the following `me` query. 145 | 146 | ```graphql 147 | query { 148 | me { 149 | login 150 | name 151 | followerCount 152 | followers(limit: 5) { 153 | login 154 | name 155 | } 156 | } 157 | } 158 | ``` 159 | 160 | - You will get the following response: 161 | ```json 162 | { 163 | "data": { 164 | "me": null 165 | } 166 | } 167 | ``` 168 | 169 | ### Adding GraphQL Mocks 170 | 171 | Our schema does not know how to resolve `me` or any other field for that matter. 172 | We need to provide it with proper resolvers but until we get to do that, 173 | there is one more very cool feature to apollo which is generating mock resolvers. 174 | 175 | - Import from `graphql-tools` the function `addMockFunctionsToSchema` on our schema.js file. 176 | 177 | ```javascript 178 | const {makeExecutableSchema, addMockFunctionsToSchema} = require('graphql-tools'); 179 | ``` 180 | 181 | - Now call this function right before the export of our `Schema` object. 182 | 183 | ```javascript 184 | addMockFunctionsToSchema({schema: Schema}); 185 | ``` 186 | 187 | - Go back to our grahpiql tool on http://localhost:3001/graphiql and run the `me` query again. 188 | 189 | - This time you will receive a response of the following structure: 190 | ```json 191 | { 192 | "data": { 193 | "me": { 194 | "login": "Hello World", 195 | "name": "Hello World", 196 | "followerCount": -96, 197 | "followers": [ 198 | { 199 | "login": "Hello World", 200 | "name": "Hello World" 201 | }, 202 | { 203 | "login": "Hello World", 204 | "name": "Hello World" 205 | } 206 | ] 207 | } 208 | } 209 | } 210 | ``` 211 | 212 | So we can see that our schema now knows how to return data. We can also see that the data is quite genric and sometimes doesn't make sense. 213 | In order to change that, we use a package called `casual` to tweak some of the mocked data. 214 | 215 | - Import `casual` to our schema.js file and create a `mocks` object. 216 | Pass that `mocks` object to the `addMockFunctionsToSchema` function. 217 | ```javascript 218 | const casual = require('casual'); 219 | // .... 220 | const mocks = {}; 221 | 222 | addMockFunctionsToSchema({schema: Schema, mocks}); 223 | ``` 224 | 225 | - First let's make `followerCount` to be a positive number. 226 | ```javascript 227 | const mocks = { 228 | User: () => ({ 229 | followerCount: () => casual.integer(0), // start from 0 230 | }), 231 | }; 232 | ``` 233 | 234 | - Now let's get `name` and `login` fields return fitting strings. 235 | ```javascript 236 | const mocks = { 237 | User: () => ({ 238 | login: () => casual.username, 239 | name: () => casual.name, 240 | 241 | followerCount: () => casual.integer(0), 242 | }), 243 | }; 244 | ``` 245 | 246 | - Lastly, we will use `MockedList` from `graphql-tools`, to make followers return a list of users that it's length corresponds to the given `limit` argument. 247 | 248 | ```javascript 249 | const {makeExecutableSchema, addMockFunctionsToSchema, MockList} = require('graphql-tools'); 250 | 251 | // .... 252 | 253 | const mocks = { 254 | User: () => ({ 255 | login: () => casual.username, 256 | name: () => casual.name, 257 | followerCount: () => casual.integer(0), 258 | 259 | followers: (_, args) => new MockList(args.limit), 260 | }), 261 | }; 262 | ``` 263 | 264 | - Now run the `me` query again. You should get a more sensible result. 265 | ```json 266 | { 267 | "data": { 268 | "follow": { 269 | "login": "Haleigh.Kutch", 270 | "name": "Dr. Marlen Smith", 271 | "followerCount": 182, 272 | "followers": [ 273 | { 274 | "login": "Solon_Hirthe", 275 | "name": "Mrs. Jamie Roberts" 276 | }, 277 | { 278 | "login": "Wyman_Arnold", 279 | "name": "Dr. Alisa Price" 280 | }, 281 | { 282 | "login": "Mabelle_Donnelly", 283 | "name": "Ms. Monica Bosco" 284 | }, 285 | { 286 | "login": "Wiegand_Keira", 287 | "name": "Miss Emilia McDermott" 288 | }, 289 | { 290 | "login": "Duncan.Hickle", 291 | "name": "Mrs. Jacinto Reinger" 292 | } 293 | ] 294 | } 295 | } 296 | } 297 | ``` 298 | 299 | This is how your schema file should look like: 300 | 301 | {{{ diffStep 2.4 }}} 302 | 303 | ### CORS 304 | 305 | Because we will separate our client and server and run them in different ports and instances, we need to make sure our server support [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS). 306 | 307 | To add CORS, install `cors` from NPM: 308 | 309 | $ cd server 310 | $ yarn add cors 311 | 312 | Now, use it with your Express instance: 313 | 314 | {{{ diffStep 2.5 files="server/src/index.js" }}} 315 | -------------------------------------------------------------------------------- /.tortilla/manuals/views/step4.md: -------------------------------------------------------------------------------- 1 | # Step 4: Fetch data from GitHub 2 | 3 | Now that we have our app working with mocks, we would like change the mocks with real data, taken from GitHub. 4 | 5 | ## Setup 6 | 7 | - Create a github API token for your user here - [https://github.com/settings/tokens/new] 8 | Enter a description, then check the user scope and press "Generate token" button. 9 | 10 | - Create two constants on `server/index.js` file. One will hold you github login and the second will hold the token you've 11 | just created. 12 | 13 | ```javascript 14 | const GITHUB_LOGIN = 'YOUR_GITHUB_USERNAME_HERE'; 15 | const GITHUB_ACCESS_TOKEN = 'YOUR_GITHUB_TOKEN_HERE'; 16 | ``` 17 | 18 | ## Create a GitHub connector class 19 | 20 | - Create a new file under `server` directory, named `github-connector.js`. This file will hold everything that is needed in order to 21 | get data from the GitHub API. To do our REST calls we will use `fetch` from `node-fetch`. 22 | 23 | ```javascript 24 | const fetch = require('node-fetch'); 25 | ``` 26 | 27 | - Defining our `GithubConnector` class we will require the github's access token and save that on our instance: 28 | 29 | ```javascript 30 | class GithubConnector { 31 | constructor( accessToken ) { 32 | this.accessToken = accessToken; 33 | } 34 | } 35 | 36 | module.exports = { 37 | GithubConnector, 38 | }; 39 | ``` 40 | 41 | - First we need a way to get any user object using the `login` string. GitHub's REST Api defines this as a GET to the 42 | `/users/{login}` route. We will do just that while passing the responsibility of making the request and parsing the 43 | result to another method we will call `getFromGithub`. 44 | 45 | ```javascript 46 | class GithubConnector { 47 | getUserForLogin( login ) { 48 | return this.getFromGithub(`/users/${login}`); 49 | } 50 | } 51 | ``` 52 | 53 | - In order to fulfill our schema needs, we also got to have a way to get a certain user's following list. Github defines 54 | that similarly as GET to `/users/{login}/following`. Following is a list and we've already specifies in our schema, a way 55 | to control the results of this list. So we can require page and items per page here, and pass them to Github. 56 | 57 | ```javascript 58 | class GithubConnector { 59 | getFollowingForLogin( login, page, perPage ) { 60 | return this.getFromGithub(`/users/${login}/following`, page, perPage); 61 | } 62 | } 63 | ``` 64 | 65 | - All those requests will happen from this `getFromGithub` method. We will define it as 66 | `(relativeUrl, page, perPage) => Promise`. We use `fetch` to make the GET request use the `result.json()` 67 | method to get a parsed body object. We build the url using Github's API url `'https://api.github.com'` and add at the 68 | end `access_token` parameter. The responsibility of adding paginating parameters to the url, we transfer to a 69 | dedicated `paginate` method. 70 | 71 | 72 | This is how the final GitHub connection class should look like: 73 | 74 | [{]: (diffStep 4.2) 75 | 76 | #### Step 4.2: Added GitHub connector class 77 | 78 | ##### Added server/src/github-connector.js 79 | ```diff 80 | @@ -0,0 +1,38 @@ 81 | +┊ ┊ 1┊const fetch = require('node-fetch'); 82 | +┊ ┊ 2┊ 83 | +┊ ┊ 3┊class GithubConnector { 84 | +┊ ┊ 4┊ constructor( accessToken ) { 85 | +┊ ┊ 5┊ this.accessToken = accessToken; 86 | +┊ ┊ 6┊ } 87 | +┊ ┊ 7┊ 88 | +┊ ┊ 8┊ getUserForLogin( login ) { 89 | +┊ ┊ 9┊ return this.getFromGithub(`/users/${login}`); 90 | +┊ ┊10┊ } 91 | +┊ ┊11┊ 92 | +┊ ┊12┊ getFollowingForLogin( login, page, perPage ) { 93 | +┊ ┊13┊ return this.getFromGithub(`/users/${login}/following`, page, perPage); 94 | +┊ ┊14┊ } 95 | +┊ ┊15┊ 96 | +┊ ┊16┊ getFromGithub( relativeUrl, page, perPage ) { 97 | +┊ ┊17┊ const url = `https://api.github.com${relativeUrl}?access_token=${this.accessToken}`; 98 | +┊ ┊18┊ return fetch(this.paginate(url, page, perPage)).then(res => res.json()); 99 | +┊ ┊19┊ } 100 | +┊ ┊20┊ 101 | +┊ ┊21┊ paginate( url, page, perPage ) { 102 | +┊ ┊22┊ let transformed = url.indexOf('?') !== -1 ? url : url + '?'; 103 | +┊ ┊23┊ 104 | +┊ ┊24┊ if ( page ) { 105 | +┊ ┊25┊ transformed = `${transformed}&page=${page}` 106 | +┊ ┊26┊ } 107 | +┊ ┊27┊ 108 | +┊ ┊28┊ if ( perPage ) { 109 | +┊ ┊29┊ transformed = `${transformed}&per_page=${perPage}` 110 | +┊ ┊30┊ } 111 | +┊ ┊31┊ 112 | +┊ ┊32┊ return transformed; 113 | +┊ ┊33┊ } 114 | +┊ ┊34┊} 115 | +┊ ┊35┊ 116 | +┊ ┊36┊module.exports = { 117 | +┊ ┊37┊ GithubConnector, 118 | +┊ ┊38┊};🚫↵ 119 | ``` 120 | 121 | [}]: # 122 | 123 | - Our schema resolvers will be able to use the `GithubConnector` class, using a context object that is created in 124 | index.js and is passed into `graphqlExpress` middleware. Note that `user` field is also part of `context` and it holds 125 | the current user's github login. On a real setup, this will be created for every session after authenticating the user. 126 | 127 | [{]: (diffStep 4.3) 128 | 129 | #### Step 4.3: Extend GraphQL execution context with GitHub connector instance 130 | 131 | ##### Changed server/src/index.js 132 | ```diff 133 | @@ -2,6 +2,7 @@ 134 | ┊2┊2┊const bodyParser = require('body-parser'); 135 | ┊3┊3┊const morgan = require('morgan'); 136 | ┊4┊4┊const cors = require('cors'); 137 | +┊ ┊5┊const { GithubConnector } = require('./github-connector'); 138 | ┊5┊6┊const { graphqlExpress, graphiqlExpress } = require('graphql-server-express'); 139 | ┊6┊7┊ 140 | ┊7┊8┊const { Schema } = require('./schema'); 141 | ``` 142 | ```diff 143 | @@ -16,6 +17,10 @@ 144 | ┊16┊17┊ 145 | ┊17┊18┊app.use('/graphql', bodyParser.json(), graphqlExpress({ 146 | ┊18┊19┊ schema: Schema, 147 | +┊ ┊20┊ context: { 148 | +┊ ┊21┊ githubConnector: new GithubConnector(GITHUB_ACCESS_TOKEN), 149 | +┊ ┊22┊ user: { login: GITHUB_LOGIN }, 150 | +┊ ┊23┊ } 151 | ┊19┊24┊})); 152 | ┊20┊25┊ 153 | ┊21┊26┊app.use('/graphiql', graphiqlExpress({ 154 | ``` 155 | 156 | [}]: # 157 | 158 | ## Replace mocks with real data 159 | 160 | - Up until now, our schema used mocks to resolve the queried data. Now, we would like to tell our schema how it can acquire 161 | some real data. 162 | 163 | - On `schema.js` create an empty object called `resolvers` and pass it into `makeExecutableSchema`. 164 | 165 | ```javascript 166 | const resolvers = {}; 167 | const Schema = makeExecutableSchema({typeDefs, resolvers}); 168 | ``` 169 | 170 | - Now let's specify how to resolve the `Query` type. the first and only field we have on `Query` is `me`. The resolver 171 | function is being called by the graphql `execute` function with four argument. The `value` passed from the parent resolver. 172 | The `argumnets` (or `args`) passed as the field arguments. The `context` object we defined on our index.js file. And lastly 173 | the schema definition and other specific request information. The last argument is used mostly in framework like `join-monster` 174 | which allows optimization of sql database queries. It is out of our scope. 175 | 176 | - For resolving `me` we use the githubConnector we added to the `context` object. We are using the `getUserForLogin`, 177 | and passing it the logged in user that we also added to `context`. 178 | 179 | - We need to define the `User` type. The `following` field will use `getFollowingForLogin` to get the list of users 180 | that the current user is following. This list does not have all the data we need to satisfy the other `User` fields, so 181 | we need to get each user's full public profile. That is done by mapping each user to the `getUserForLogin` method. 182 | The only other resolver we need to specify is the `followingCount`. This data is available from `getUserForLogin` but 183 | is ironically called `following` on github's returned object. Other resolvers are redundant as github's response maps 184 | to our other field names (id, name, login). 185 | 186 | - We can now remove the mocks from our schema.js file and test from our web app or from graphiql 187 | 188 | So this is the resolvers implementation: 189 | 190 | [{]: (diffStep 4.4) 191 | 192 | #### Step 4.4: Replace mocks with real resolvers 193 | 194 | ##### Changed server/src/schema.js 195 | ```diff 196 | @@ -1,5 +1,4 @@ 197 | -┊1┊ ┊const { makeExecutableSchema, addMockFunctionsToSchema, MockList } = require('graphql-tools'); 198 | -┊2┊ ┊const casual = require('casual'); 199 | +┊ ┊1┊const { makeExecutableSchema } = require('graphql-tools'); 200 | ┊3┊2┊ 201 | ┊4┊3┊const typeDefs = ` 202 | ┊5┊4┊ schema { 203 | ``` 204 | ```diff 205 | @@ -24,18 +23,24 @@ 206 | ┊24┊23┊ } 207 | ┊25┊24┊`; 208 | ┊26┊25┊ 209 | -┊27┊ ┊const Schema = makeExecutableSchema({ typeDefs }); 210 | -┊28┊ ┊ 211 | -┊29┊ ┊const mocks = { 212 | -┊30┊ ┊ User: () => ({ 213 | -┊31┊ ┊ login: () => casual.username, 214 | -┊32┊ ┊ name: () => casual.name, 215 | -┊33┊ ┊ followingCount: () => casual.integer(0), 216 | -┊34┊ ┊ following: (_, { perPage }) => new MockList(perPage), 217 | -┊35┊ ┊ }), 218 | +┊ ┊26┊const resolvers = { 219 | +┊ ┊27┊ Query: { 220 | +┊ ┊28┊ me(_, args, { githubConnector, user }) { 221 | +┊ ┊29┊ return githubConnector.getUserForLogin(user.login); 222 | +┊ ┊30┊ } 223 | +┊ ┊31┊ }, 224 | +┊ ┊32┊ User: { 225 | +┊ ┊33┊ following(user, { page, perPage }, { githubConnector }) { 226 | +┊ ┊34┊ return githubConnector.getFollowingForLogin(user.login, page, perPage) 227 | +┊ ┊35┊ .then(users => 228 | +┊ ┊36┊ users.map(user => githubConnector.getUserForLogin(user.login)) 229 | +┊ ┊37┊ ); 230 | +┊ ┊38┊ }, 231 | +┊ ┊39┊ followingCount: user => user.following, 232 | +┊ ┊40┊ } 233 | ┊36┊41┊}; 234 | ┊37┊42┊ 235 | -┊38┊ ┊addMockFunctionsToSchema({ schema: Schema, mocks }); 236 | +┊ ┊43┊const Schema = makeExecutableSchema({ typeDefs, resolvers }); 237 | ┊39┊44┊ 238 | ┊40┊45┊module.exports = { 239 | ┊41┊46┊ Schema, 240 | ``` 241 | 242 | [}]: # 243 | 244 | 245 | ## Making fewer calls to GitHub 246 | 247 | - So our schema is working great but it has two apparent issues. One it is somewhat slow and is depending on GitHub's API 248 | to give quick responses. Second, it queries GitHub a bunch of times for each GraphQL query. If we have circular follow 249 | dependencies it will even query more than once to get the same user profile. We will now fix those problems to some 250 | extent using very simple tool from facebook called `dataloader`. 251 | 252 | - We will import `DataLoader` from the `dataloader` package on our github-connector.js file. 253 | ```javascript 254 | const DataLoader = require('dataloader'); 255 | ``` 256 | 257 | - The `DataLoader` constructor needs a function that will be able to mass load any of the objects it is required. We will 258 | also set our data loader to avoid batching requests as the GitHub API does not support batching. 259 | 260 | ```javascript 261 | new DataLoader(this.fetchAll.bind(this), { batch: false }) 262 | ``` 263 | 264 | - To implement fetchAll we just need to use `fetch` as done before on `getFromGithub` for each url we receive. After that, 265 | use `Promise.all()` to create a single promise and return that. Make sure to print each call to fetch so we would know 266 | when our data loader is using it's cache and when it's not. 267 | 268 | - We also need to change `getFromGithub` to use our data loader instead of fetch. 269 | 270 | [{]: (diffStep 4.5) 271 | 272 | #### Step 4.5: Adding a data loader to reduce number of requests to GitHub 273 | 274 | ##### Changed server/src/github-connector.js 275 | ```diff 276 | @@ -1,36 +1,48 @@ 277 | ┊ 1┊ 1┊const fetch = require('node-fetch'); 278 | +┊ ┊ 2┊const DataLoader = require('dataloader'); 279 | ┊ 2┊ 3┊ 280 | ┊ 3┊ 4┊class GithubConnector { 281 | -┊ 4┊ ┊ constructor( accessToken ) { 282 | +┊ ┊ 5┊ constructor(accessToken) { 283 | ┊ 5┊ 6┊ this.accessToken = accessToken; 284 | +┊ ┊ 7┊ this.dataLoader = new DataLoader(this.fetchAll.bind(this), { batch: false }); 285 | ┊ 6┊ 8┊ } 286 | ┊ 7┊ 9┊ 287 | -┊ 8┊ ┊ getUserForLogin( login ) { 288 | +┊ ┊10┊ getUserForLogin(login) { 289 | ┊ 9┊11┊ return this.getFromGithub(`/users/${login}`); 290 | ┊10┊12┊ } 291 | ┊11┊13┊ 292 | -┊12┊ ┊ getFollowingForLogin( login, page, perPage ) { 293 | +┊ ┊14┊ getFollowingForLogin(login, page, perPage) { 294 | ┊13┊15┊ return this.getFromGithub(`/users/${login}/following`, page, perPage); 295 | ┊14┊16┊ } 296 | ┊15┊17┊ 297 | -┊16┊ ┊ getFromGithub( relativeUrl, page, perPage ) { 298 | +┊ ┊18┊ getFromGithub(relativeUrl, page, perPage) { 299 | ┊17┊19┊ const url = `https://api.github.com${relativeUrl}?access_token=${this.accessToken}`; 300 | -┊18┊ ┊ return fetch(this.paginate(url, page, perPage)).then(res => res.json()); 301 | +┊ ┊20┊ 302 | +┊ ┊21┊ return this.dataLoader.load(this.paginate(url, page, perPage)); 303 | ┊19┊22┊ } 304 | ┊20┊23┊ 305 | -┊21┊ ┊ paginate( url, page, perPage ) { 306 | +┊ ┊24┊ paginate(url, page, perPage) { 307 | ┊22┊25┊ let transformed = url.indexOf('?') !== -1 ? url : url + '?'; 308 | ┊23┊26┊ 309 | -┊24┊ ┊ if ( page ) { 310 | +┊ ┊27┊ if (page) { 311 | ┊25┊28┊ transformed = `${transformed}&page=${page}` 312 | ┊26┊29┊ } 313 | ┊27┊30┊ 314 | -┊28┊ ┊ if ( perPage ) { 315 | +┊ ┊31┊ if (perPage) { 316 | ┊29┊32┊ transformed = `${transformed}&per_page=${perPage}` 317 | ┊30┊33┊ } 318 | ┊31┊34┊ 319 | ┊32┊35┊ return transformed; 320 | ┊33┊36┊ } 321 | +┊ ┊37┊ 322 | +┊ ┊38┊ fetchAll(urls) { 323 | +┊ ┊39┊ return Promise.all( 324 | +┊ ┊40┊ urls.map(url => { 325 | +┊ ┊41┊ console.log('Fetching Url', url); 326 | +┊ ┊42┊ return fetch(url).then(res => res.json()) 327 | +┊ ┊43┊ }) 328 | +┊ ┊44┊ ); 329 | +┊ ┊45┊ } 330 | ┊34┊46┊} 331 | ┊35┊47┊ 332 | ┊36┊48┊module.exports = { 333 | ``` 334 | 335 | [}]: # 336 | 337 | > Note that we don't invalidate the data loader's cache. Facebook suggests we would invalidate when ever a mutation is 338 | > done. This a simple call to `clearAll` method and you can see an example for it on step 5. 339 | 340 | - You should now see that whenever you query you GraphQL API, it will get the data the first time from GitHub, causing 341 | a long response time and on the second time it will load the data from memory with a fraction of the time. 342 | 343 | [{]: (navStep) 344 | 345 | | [< Previous Step](step3.md) | [Next Step >](step5.md) | 346 | |:--------------------------------|--------------------------------:| 347 | 348 | [}]: # 349 | -------------------------------------------------------------------------------- /.tortilla/manuals/views/step2.md: -------------------------------------------------------------------------------- 1 | # Step 2: Writing a GraphQL schema 2 | 3 | In this step we will create a GraphQL server, we will implement it with NodeJS and Express. 4 | 5 | > My preference is to use Yarn as package manager - but you can also use NPM. 6 | 7 | Let's start by creating a NodeJS project: 8 | 9 | $ mkdir server 10 | $ cd server 11 | $ yarn init 12 | 13 | Great, now let's add the dependencies we need: 14 | 15 | $ yarn add graphql express graphql-tools graphql-server-express body-parser casual dataloader morgan node-fetch 16 | $ yarn add -D nodemon 17 | 18 | Now let's add some scripts to our `package.json`: 19 | 20 | ```json 21 | "scripts": { 22 | "start": "node src/index.js", 23 | "watch": "nodemon src/index.js" 24 | }, 25 | ``` 26 | 27 | Your final `package.json` should look like that: 28 | 29 | [{]: (diffStep 2.1 files="server/package.json") 30 | 31 | #### Step 2.1: Basic NodeJS server 32 | 33 | ##### Added server/package.json 34 | ```diff 35 | @@ -0,0 +1,32 @@ 36 | +┊ ┊ 1┊{ 37 | +┊ ┊ 2┊ "name": "workshop-server", 38 | +┊ ┊ 3┊ "version": "1.0.0", 39 | +┊ ┊ 4┊ "description": "GraphQL Basic server", 40 | +┊ ┊ 5┊ "main": "index.js", 41 | +┊ ┊ 6┊ "license": "MIT", 42 | +┊ ┊ 7┊ "scripts": { 43 | +┊ ┊ 8┊ "start": "node src/index.js", 44 | +┊ ┊ 9┊ "watch": "nodemon src/index.js" 45 | +┊ ┊10┊ }, 46 | +┊ ┊11┊ "keywords": [ 47 | +┊ ┊12┊ "tutorial", 48 | +┊ ┊13┊ "graphql", 49 | +┊ ┊14┊ "apollo", 50 | +┊ ┊15┊ "server", 51 | +┊ ┊16┊ "express" 52 | +┊ ┊17┊ ], 53 | +┊ ┊18┊ "dependencies": { 54 | +┊ ┊19┊ "body-parser": "^1.17.2", 55 | +┊ ┊20┊ "casual": "^1.5.14", 56 | +┊ ┊21┊ "dataloader": "^1.3.0", 57 | +┊ ┊22┊ "express": "^4.15.3", 58 | +┊ ┊23┊ "graphql": "^0.10.3", 59 | +┊ ┊24┊ "graphql-server-express": "^0.9.0", 60 | +┊ ┊25┊ "graphql-tools": "^1.0.0", 61 | +┊ ┊26┊ "morgan": "^1.8.2", 62 | +┊ ┊27┊ "node-fetch": "^1.7.1" 63 | +┊ ┊28┊ }, 64 | +┊ ┊29┊ "devDependencies": { 65 | +┊ ┊30┊ "nodemon": "^1.11.0" 66 | +┊ ┊31┊ } 67 | +┊ ┊32┊} 68 | ``` 69 | 70 | [}]: # 71 | 72 | Now, after learning to work with Github's GraphQL explorer, we now want to try and learn how it's done. 73 | 74 | Our implementation will be much more simpler but it will eventually help us understand how to wrap every REST Api with GraphQL endpoint. 75 | 76 | 77 | ### Write you GraphQL schema 78 | 79 | Create a new file named schema.js. We would write our schema inside a single ES6 template string. 80 | 81 | ```js 82 | const typeDefs = ` 83 | ... our schema goes here .... 84 | `; 85 | ``` 86 | 87 | Every great schema starts with a schema declaration. It will have two fields, query of type Query and mutation of type Mutation. 88 | 89 | ```graphql 90 | schema { 91 | query: Query 92 | mutation: Mutation 93 | } 94 | ``` 95 | 96 | Next we define our Query type. We will add only one field me of type User, because this is the only thing the interests us at the moment. 97 | 98 | ```graphql 99 | type Query { 100 | me: User 101 | } 102 | ``` 103 | 104 | Now define our Mutation type. Add a field called follow. This field will get a mandatory argument called userId of the type ID. It will return the type User as well. 105 | 106 | ```graphql 107 | type Mutation { 108 | follow(userId: ID!): User 109 | } 110 | ``` 111 | 112 | > Note how we used the exclamation mark (!) to define a mandatory type. 113 | 114 | All we have left is to define our User type. A user will have id, login, name, followerCount and a list of followers. followers can accept optional skip and limit arguments to control the returned items on the followers list. We will give skip a default value of 0 and limit default value of 10. 115 | 116 | ```graphql 117 | type User { 118 | id: ID! 119 | login: String! 120 | name: String 121 | followerCount: Int 122 | followers(skip: Int = 0, limit: Int = 10): [User] 123 | } 124 | ``` 125 | 126 | Now we use apollo to parse that schema and make an executable schema out of it. 127 | 128 | Import the `makeExecutableSchema` from `graphql-tools` package, and use it to create your GraphQL schema object. 129 | 130 | This is how you file should look like: 131 | 132 | [{]: (diffStep 2.2) 133 | 134 | #### Step 2.2: Added basic GraphQL schema 135 | 136 | ##### Added server/src/schema.js 137 | ```diff 138 | @@ -0,0 +1,30 @@ 139 | +┊ ┊ 1┊const { makeExecutableSchema } = require('graphql-tools'); 140 | +┊ ┊ 2┊ 141 | +┊ ┊ 3┊const typeDefs = ` 142 | +┊ ┊ 4┊ schema { 143 | +┊ ┊ 5┊ query: Query 144 | +┊ ┊ 6┊ mutation: Mutation 145 | +┊ ┊ 7┊ } 146 | +┊ ┊ 8┊ 147 | +┊ ┊ 9┊ type Query { 148 | +┊ ┊10┊ me: User 149 | +┊ ┊11┊ } 150 | +┊ ┊12┊ 151 | +┊ ┊13┊ type Mutation { 152 | +┊ ┊14┊ follow(login: String!): User 153 | +┊ ┊15┊ } 154 | +┊ ┊16┊ 155 | +┊ ┊17┊ type User { 156 | +┊ ┊18┊ id: ID! 157 | +┊ ┊19┊ login: String! 158 | +┊ ┊20┊ name: String 159 | +┊ ┊21┊ followingCount: Int 160 | +┊ ┊22┊ following(page: Int = 0, perPage: Int = 10): [User] 161 | +┊ ┊23┊ } 162 | +┊ ┊24┊`; 163 | +┊ ┊25┊ 164 | +┊ ┊26┊const Schema = makeExecutableSchema({ typeDefs }); 165 | +┊ ┊27┊ 166 | +┊ ┊28┊module.exports = { 167 | +┊ ┊29┊ Schema, 168 | +┊ ┊30┊}; 169 | ``` 170 | 171 | [}]: # 172 | 173 | ### Create a GraphQL endpoint with your schema 174 | 175 | - Create a new file named index.js. This of course, will be our server's entry point. 176 | - Import `express`, `body-parser` and our `graphqlExpress` middleware. 177 | 178 | ```javascript 179 | const express = require('express'); 180 | const bodyParser = require('body-parser'); 181 | const {graphqlExpress} = require('graphql-server-express'); 182 | ``` 183 | 184 | - Import our `Schema` object. 185 | ```javascript 186 | const {Schema} = require('./schema'); 187 | ``` 188 | 189 | - Now we create our `express` app, and add our `bodyParser` and `graphqlExpress` middleware on `/graphql` path. 190 | 191 | ```javascript 192 | const app = express(); 193 | 194 | app.use('/graphql', bodyParser.json(), graphqlExpress({ 195 | schema: Schema, 196 | })); 197 | ``` 198 | 199 | - Lastly we need to tell `express` to start listening on some port. 200 | ```javascript 201 | app.listen(3001); 202 | ``` 203 | 204 | **Now we can go ahead and start the app by typing `npm start` in our project directory.** 205 | 206 | > If it works without errors, close it using Ctrl + C and run `npm run watch` to make our server restart when we change our files. 207 | 208 | - So now we have a GrpahQL endpoint but we don't know how to explore the API. To do just that, we will add `graphiqlExpress` middleware. 209 | ```javascript 210 | // Import graphiqlExpress from the same package 211 | const {graphqlExpress, graphiqlExpress} = require('graphql-server-express'); 212 | 213 | // ... Just before calling the listen method 214 | 215 | app.use('/graphiql', graphiqlExpress({ 216 | endpointURL: '/graphql', // This will tell the graphiql interface where to run queries 217 | })); 218 | ``` 219 | 220 | This is how your file should looks like: 221 | 222 | [{]: (diffStep 2.3) 223 | 224 | #### Step 2.3: Expose GraphQL endpoint 225 | 226 | ##### Added server/src/index.js 227 | ```diff 228 | @@ -0,0 +1,20 @@ 229 | +┊ ┊ 1┊const express = require('express'); 230 | +┊ ┊ 2┊const bodyParser = require('body-parser'); 231 | +┊ ┊ 3┊const morgan = require('morgan'); 232 | +┊ ┊ 4┊const { graphqlExpress, graphiqlExpress } = require('graphql-server-express'); 233 | +┊ ┊ 5┊ 234 | +┊ ┊ 6┊const { Schema } = require('./schema'); 235 | +┊ ┊ 7┊ 236 | +┊ ┊ 8┊const app = express(); 237 | +┊ ┊ 9┊ 238 | +┊ ┊10┊app.use(morgan('tiny')); 239 | +┊ ┊11┊ 240 | +┊ ┊12┊app.use('/graphql', bodyParser.json(), graphqlExpress({ 241 | +┊ ┊13┊ schema: Schema, 242 | +┊ ┊14┊})); 243 | +┊ ┊15┊ 244 | +┊ ┊16┊app.use('/graphiql', graphiqlExpress({ 245 | +┊ ┊17┊ endpointURL: '/graphql', 246 | +┊ ┊18┊})); 247 | +┊ ┊19┊ 248 | +┊ ┊20┊app.listen(3001); 249 | ``` 250 | 251 | [}]: # 252 | 253 | - Now open your browser on http://localhost:3001/graphiql and start exploring the schema we've just wrote! 254 | 255 | - Try to run the following `me` query. 256 | 257 | ```graphql 258 | query { 259 | me { 260 | login 261 | name 262 | followerCount 263 | followers(limit: 5) { 264 | login 265 | name 266 | } 267 | } 268 | } 269 | ``` 270 | 271 | - You will get the following response: 272 | ```json 273 | { 274 | "data": { 275 | "me": null 276 | } 277 | } 278 | ``` 279 | 280 | ### Adding GraphQL Mocks 281 | 282 | Our schema does not know how to resolve `me` or any other field for that matter. 283 | We need to provide it with proper resolvers but until we get to do that, 284 | there is one more very cool feature to apollo which is generating mock resolvers. 285 | 286 | - Import from `graphql-tools` the function `addMockFunctionsToSchema` on our schema.js file. 287 | 288 | ```javascript 289 | const {makeExecutableSchema, addMockFunctionsToSchema} = require('graphql-tools'); 290 | ``` 291 | 292 | - Now call this function right before the export of our `Schema` object. 293 | 294 | ```javascript 295 | addMockFunctionsToSchema({schema: Schema}); 296 | ``` 297 | 298 | - Go back to our grahpiql tool on http://localhost:3001/graphiql and run the `me` query again. 299 | 300 | - This time you will receive a response of the following structure: 301 | ```json 302 | { 303 | "data": { 304 | "me": { 305 | "login": "Hello World", 306 | "name": "Hello World", 307 | "followerCount": -96, 308 | "followers": [ 309 | { 310 | "login": "Hello World", 311 | "name": "Hello World" 312 | }, 313 | { 314 | "login": "Hello World", 315 | "name": "Hello World" 316 | } 317 | ] 318 | } 319 | } 320 | } 321 | ``` 322 | 323 | So we can see that our schema now knows how to return data. We can also see that the data is quite genric and sometimes doesn't make sense. 324 | In order to change that, we use a package called `casual` to tweak some of the mocked data. 325 | 326 | - Import `casual` to our schema.js file and create a `mocks` object. 327 | Pass that `mocks` object to the `addMockFunctionsToSchema` function. 328 | ```javascript 329 | const casual = require('casual'); 330 | // .... 331 | const mocks = {}; 332 | 333 | addMockFunctionsToSchema({schema: Schema, mocks}); 334 | ``` 335 | 336 | - First let's make `followerCount` to be a positive number. 337 | ```javascript 338 | const mocks = { 339 | User: () => ({ 340 | followerCount: () => casual.integer(0), // start from 0 341 | }), 342 | }; 343 | ``` 344 | 345 | - Now let's get `name` and `login` fields return fitting strings. 346 | ```javascript 347 | const mocks = { 348 | User: () => ({ 349 | login: () => casual.username, 350 | name: () => casual.name, 351 | 352 | followerCount: () => casual.integer(0), 353 | }), 354 | }; 355 | ``` 356 | 357 | - Lastly, we will use `MockedList` from `graphql-tools`, to make followers return a list of users that it's length corresponds to the given `limit` argument. 358 | 359 | ```javascript 360 | const {makeExecutableSchema, addMockFunctionsToSchema, MockList} = require('graphql-tools'); 361 | 362 | // .... 363 | 364 | const mocks = { 365 | User: () => ({ 366 | login: () => casual.username, 367 | name: () => casual.name, 368 | followerCount: () => casual.integer(0), 369 | 370 | followers: (_, args) => new MockList(args.limit), 371 | }), 372 | }; 373 | ``` 374 | 375 | - Now run the `me` query again. You should get a more sensible result. 376 | ```json 377 | { 378 | "data": { 379 | "follow": { 380 | "login": "Haleigh.Kutch", 381 | "name": "Dr. Marlen Smith", 382 | "followerCount": 182, 383 | "followers": [ 384 | { 385 | "login": "Solon_Hirthe", 386 | "name": "Mrs. Jamie Roberts" 387 | }, 388 | { 389 | "login": "Wyman_Arnold", 390 | "name": "Dr. Alisa Price" 391 | }, 392 | { 393 | "login": "Mabelle_Donnelly", 394 | "name": "Ms. Monica Bosco" 395 | }, 396 | { 397 | "login": "Wiegand_Keira", 398 | "name": "Miss Emilia McDermott" 399 | }, 400 | { 401 | "login": "Duncan.Hickle", 402 | "name": "Mrs. Jacinto Reinger" 403 | } 404 | ] 405 | } 406 | } 407 | } 408 | ``` 409 | 410 | This is how your schema file should look like: 411 | 412 | [{]: (diffStep 2.4) 413 | 414 | #### Step 2.4: Added GraphQL data mocks 415 | 416 | ##### Changed server/src/schema.js 417 | ```diff 418 | @@ -1,4 +1,5 @@ 419 | -┊1┊ ┊const { makeExecutableSchema } = require('graphql-tools'); 420 | +┊ ┊1┊const { makeExecutableSchema, addMockFunctionsToSchema, MockList } = require('graphql-tools'); 421 | +┊ ┊2┊const casual = require('casual'); 422 | ┊2┊3┊ 423 | ┊3┊4┊const typeDefs = ` 424 | ┊4┊5┊ schema { 425 | ``` 426 | ```diff 427 | @@ -25,6 +26,17 @@ 428 | ┊25┊26┊ 429 | ┊26┊27┊const Schema = makeExecutableSchema({ typeDefs }); 430 | ┊27┊28┊ 431 | +┊ ┊29┊const mocks = { 432 | +┊ ┊30┊ User: () => ({ 433 | +┊ ┊31┊ login: () => casual.username, 434 | +┊ ┊32┊ name: () => casual.name, 435 | +┊ ┊33┊ followingCount: () => casual.integer(0), 436 | +┊ ┊34┊ following: (_, { perPage }) => new MockList(perPage), 437 | +┊ ┊35┊ }), 438 | +┊ ┊36┊}; 439 | +┊ ┊37┊ 440 | +┊ ┊38┊addMockFunctionsToSchema({ schema: Schema, mocks }); 441 | +┊ ┊39┊ 442 | ┊28┊40┊module.exports = { 443 | ┊29┊41┊ Schema, 444 | ┊30┊42┊}; 445 | ``` 446 | 447 | [}]: # 448 | 449 | ### CORS 450 | 451 | Because we will separate our client and server and run them in different ports and instances, we need to make sure our server support [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS). 452 | 453 | To add CORS, install `cors` from NPM: 454 | 455 | $ cd server 456 | $ yarn add cors 457 | 458 | Now, use it with your Express instance: 459 | 460 | [{]: (diffStep 2.5 files="server/src/index.js") 461 | 462 | #### Step 2.5: Added cors 463 | 464 | ##### Changed server/src/index.js 465 | ```diff 466 | @@ -1,12 +1,14 @@ 467 | ┊ 1┊ 1┊const express = require('express'); 468 | ┊ 2┊ 2┊const bodyParser = require('body-parser'); 469 | ┊ 3┊ 3┊const morgan = require('morgan'); 470 | +┊ ┊ 4┊const cors = require('cors'); 471 | ┊ 4┊ 5┊const { graphqlExpress, graphiqlExpress } = require('graphql-server-express'); 472 | ┊ 5┊ 6┊ 473 | ┊ 6┊ 7┊const { Schema } = require('./schema'); 474 | ┊ 7┊ 8┊ 475 | ┊ 8┊ 9┊const app = express(); 476 | ┊ 9┊10┊ 477 | +┊ ┊11┊app.use(cors()); 478 | ┊10┊12┊app.use(morgan('tiny')); 479 | ┊11┊13┊ 480 | ┊12┊14┊app.use('/graphql', bodyParser.json(), graphqlExpress({ 481 | ``` 482 | 483 | [}]: # 484 | 485 | [{]: (navStep) 486 | 487 | | [< Previous Step](step1.md) | [Next Step >](step3.md) | 488 | |:--------------------------------|--------------------------------:| 489 | 490 | [}]: # 491 | -------------------------------------------------------------------------------- /client/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 |
    3 |

    4 | Welcome to GraphQL & Angular Workshop! 5 |

    6 | 7 |
    8 | 9 |

    Follow

    10 | 11 | 12 |

    Following:

    13 | 14 | --------------------------------------------------------------------------------