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 | 
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 | 
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