Browse Source

add Subscriptions

dev
Christian Ziermann 4 years ago
parent
commit
6a66462c55
34 changed files with 822 additions and 110 deletions
  1. +1
    -1
      README.md
  2. +54
    -1
      backend/graphql.schema.json
  3. +14
    -0
      backend/src/generated/graphql.ts
  4. +15
    -6
      backend/src/index.ts
  5. +18
    -0
      backend/src/resolver/pizza-resolver.ts
  6. +20
    -2
      backend/src/resolver/topping-resolver.ts
  7. +5
    -0
      backend/src/schema/pizza.ts
  8. +1
    -1
      backend/tsconfig.json
  9. +20
    -10
      frontend/package-lock.json
  10. +2
    -0
      frontend/package.json
  11. +6
    -0
      frontend/src/app/app.component.html
  12. +60
    -10
      frontend/src/app/app.module.ts
  13. +6
    -0
      frontend/src/app/pizza-create/pizza-create-topping-subscription.graphql
  14. +6
    -0
      frontend/src/app/pizza-create/pizza-create-toppings.graphql
  15. +36
    -0
      frontend/src/app/pizza-create/pizza-create.component.html
  16. +0
    -0
      frontend/src/app/pizza-create/pizza-create.component.scss
  17. +25
    -0
      frontend/src/app/pizza-create/pizza-create.component.spec.ts
  18. +69
    -0
      frontend/src/app/pizza-create/pizza-create.component.ts
  19. +5
    -0
      frontend/src/app/pizza-create/pizza-create.graphql
  20. +9
    -0
      frontend/src/app/pizza-list-with-topping/pizza-list-with-topping-subscription.graphql
  21. +32
    -30
      frontend/src/app/pizza-list-with-topping/pizza-list-with-topping.component.html
  22. +10
    -8
      frontend/src/app/pizza-list-with-topping/pizza-list-with-topping.component.ts
  23. +6
    -0
      frontend/src/app/pizza-list/pizza-list-subscription.graphql
  24. +26
    -24
      frontend/src/app/pizza-list/pizza-list.component.html
  25. +13
    -9
      frontend/src/app/pizza-list/pizza-list.component.ts
  26. +13
    -0
      frontend/src/app/topping-create/topping-create.component.html
  27. +0
    -0
      frontend/src/app/topping-create/topping-create.component.scss
  28. +25
    -0
      frontend/src/app/topping-create/topping-create.component.spec.ts
  29. +49
    -0
      frontend/src/app/topping-create/topping-create.component.ts
  30. +5
    -0
      frontend/src/app/topping-create/topping-create.graphql
  31. +216
    -0
      frontend/src/generated/graphql.ts
  32. +37
    -7
      frontend/src/graphql.module.ts
  33. +16
    -0
      frontend/src/styles.scss
  34. +2
    -1
      presentation/index.html

+ 1
- 1
README.md View File

@@ -54,7 +54,7 @@ npx tsc --init --rootDir src --outDir build \
```

```
npm install --save-dev ts-node nodemon rimraf uuid @types/uuid
npm install --save-dev ts-node nodemon rimraf uuid @types/uuid apollo-link-ws
```



+ 54
- 1
backend/graphql.schema.json View File

@@ -7,7 +7,9 @@
"mutationType": {
"name": "Mutation"
},
"subscriptionType": null,
"subscriptionType": {
"name": "Subscription"
},
"types": [
{
"kind": "OBJECT",
@@ -547,6 +549,57 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "Subscription",
"description": null,
"fields": [
{
"name": "pizzasChanged",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "Pizza",
"ofType": null
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "toppingsChanged",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "Topping",
"ofType": null
}
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "__Schema",

+ 14
- 0
backend/src/generated/graphql.ts View File

@@ -96,6 +96,12 @@ export type MutationUpdateToppingArgs = {
updatedToppingDto: ChangeToppingDto;
};

export type Subscription = {
__typename?: 'Subscription';
pizzasChanged: Array<Maybe<Pizza>>;
toppingsChanged: Array<Maybe<Topping>>;
};



export type ResolverTypeWrapper<T> = Promise<T> | T;
@@ -183,6 +189,7 @@ export type ResolversTypes = {
ChangePizzaDto: ChangePizzaDto;
ChangeToppingDto: ChangeToppingDto;
Mutation: ResolverTypeWrapper<{}>;
Subscription: ResolverTypeWrapper<{}>;
};

/** Mapping between all available schema types and the resolvers parents */
@@ -196,6 +203,7 @@ export type ResolversParentTypes = {
ChangePizzaDto: ChangePizzaDto;
ChangeToppingDto: ChangeToppingDto;
Mutation: {};
Subscription: {};
};

export type PizzaResolvers<ContextType = any, ParentType extends ResolversParentTypes['Pizza'] = ResolversParentTypes['Pizza']> = {
@@ -227,11 +235,17 @@ export type MutationResolvers<ContextType = any, ParentType extends ResolversPar
updateTopping?: Resolver<ResolversTypes['Topping'], ParentType, ContextType, RequireFields<MutationUpdateToppingArgs, 'toppingId' | 'updatedToppingDto'>>;
};

export type SubscriptionResolvers<ContextType = any, ParentType extends ResolversParentTypes['Subscription'] = ResolversParentTypes['Subscription']> = {
pizzasChanged?: SubscriptionResolver<Array<Maybe<ResolversTypes['Pizza']>>, "pizzasChanged", ParentType, ContextType>;
toppingsChanged?: SubscriptionResolver<Array<Maybe<ResolversTypes['Topping']>>, "toppingsChanged", ParentType, ContextType>;
};

export type Resolvers<ContextType = any> = {
Pizza?: PizzaResolvers<ContextType>;
Topping?: ToppingResolvers<ContextType>;
Query?: QueryResolvers<ContextType>;
Mutation?: MutationResolvers<ContextType>;
Subscription?: SubscriptionResolvers<ContextType>;
};



+ 15
- 6
backend/src/index.ts View File

@@ -1,11 +1,9 @@
import { ApolloServer } from "apollo-server/dist";
import { ApolloServer, PubSub } from "apollo-server/dist";
import { Resolvers, ChangePizzaDto, ChangeToppingDto } from "./generated/graphql";
import { pizzaSchema } from "./schema/pizza";
import { PizzaResolver } from "./resolver/pizza-resolver";
import { ToppingResolver } from "./resolver/topping-resolver";



const resolvers: Resolvers = {
Topping: {
id: (root, args, context) => {
@@ -59,14 +57,25 @@ const resolvers: Resolvers = {
updateTopping: (root, args, context) => {
return ToppingResolver.update(args.toppingId, args.updatedToppingDto as ChangeToppingDto);
},
},
Subscription: {
pizzasChanged: {
subscribe: () => PizzaResolver.pizzaEvent.asyncIterator(PizzaResolver.events.PIZZA_CHANGED)

},
toppingsChanged: {
subscribe: () => ToppingResolver.toppingEvent.asyncIterator(ToppingResolver.events.TOPPING_CHANGED)
},
}
}

const server = new ApolloServer({
typeDefs: pizzaSchema,
resolvers: resolvers as any
});
resolvers: resolvers as any,
}
);

server.listen().then(({ url }) => {
server.listen().then(({ url, subscriptionsUrl }) => {
console.log(`🚀 Server ready at ${url}`)
console.log(`🚀 Subscriptions ready at ${subscriptionsUrl}`);
});

+ 18
- 0
backend/src/resolver/pizza-resolver.ts View File

@@ -2,9 +2,15 @@ import { Topping, Pizza, ChangePizzaDto, Maybe } from "../generated/graphql";
import { PizzaList } from "../data/pizza-list";
import { v4 as uuidv4 } from 'uuid';
import { ToppingResolver } from "./topping-resolver";
import { PubSub } from "apollo-server";

export class PizzaResolver {

static pizzaEvent = new PubSub();
static events = {
PIZZA_CHANGED: "pizzaChanged",
}

static getById = (id: string): Pizza => {
return PizzaList.filter(pizza => pizza.id === id)[0];
};
@@ -18,14 +24,21 @@ export class PizzaResolver {
};

static create = (pizzaCreateDto: ChangePizzaDto): Pizza => {
// Get Topping Objects by Ids
const toppings: Topping[] = pizzaCreateDto.toppingIds
.map<string>((toppingId) => toppingId as string)
.map<Topping>((toppingId) => ToppingResolver.getById(toppingId));
// Init new Pizza Object
const pizza: Pizza = {
id: uuidv4(),
name: pizzaCreateDto.name,
toppings: toppings
}
// Add Pizza to 'Database'
PizzaList.push(pizza);
PizzaResolver.pizzaEvent.publish(PizzaResolver.events.PIZZA_CHANGED, {
pizzasChanged: PizzaList
});
console.log(`Create Pizza ...`, pizza)
return pizza;
};
@@ -42,7 +55,12 @@ export class PizzaResolver {
.map<Topping>((toppingId: string) => ToppingResolver.getById(toppingId));
pizza.name = pizzaUpdateDto.name;
pizza.toppings = toppings;
const pizzaIndex = PizzaList.indexOf(pizza);
PizzaList[pizzaIndex] = pizza;
console.log(`Update Pizza ...`, pizza)
PizzaResolver.pizzaEvent.publish(PizzaResolver.events.PIZZA_CHANGED, {
pizzasChanged: PizzaList
});
return pizza;
};


+ 20
- 2
backend/src/resolver/topping-resolver.ts View File

@@ -2,7 +2,16 @@ import { Topping, ChangeToppingDto, } from "../generated/graphql";
import { ToppingList } from "../data/topping-list";
import { v4 as uuidv4 } from 'uuid';
import { EEXIST } from "constants";
import { PubSub } from "apollo-server";


export class ToppingResolver {

static toppingEvent = new PubSub();
static events = {
TOPPING_CHANGED: "toppingChanged",
}

static getById = (id: string): Topping => {
return ToppingList.filter(topping => topping.id === id)[0];
};
@@ -20,7 +29,11 @@ export class ToppingResolver {
id: uuidv4(),
name: toppingCreateDto.name,
}
console.log(`Create Topping ...`, topping)
ToppingList.push(topping);
console.log(`Create Topping ...`, topping);
ToppingResolver.toppingEvent.publish(ToppingResolver.events.TOPPING_CHANGED, {
toppingsChanged: ToppingList
});
return topping;
};

@@ -32,7 +45,12 @@ export class ToppingResolver {
`No Topping found with id ${toppingId}`
)
}
topping.name = toppingUpdateDto.name;
topping.name = toppingUpdateDto.name;
const toppingIndex = ToppingList.indexOf(topping);
ToppingList[toppingIndex] = topping;
ToppingResolver.toppingEvent.publish(ToppingResolver.events.TOPPING_CHANGED, {
toppingsChanged: ToppingList
});
console.log(`Update Topping ...`, topping)
return topping;
};

+ 5
- 0
backend/src/schema/pizza.ts View File

@@ -43,4 +43,9 @@ export const pizzaSchema = gql`
createTopping(createToppingDto: ChangeToppingDto): Topping!
updateTopping(toppingId: ID!, updatedToppingDto: ChangeToppingDto!): Topping!
}

type Subscription {
pizzasChanged: [Pizza]!
toppingsChanged: [Topping]!
}
`;

+ 1
- 1
backend/tsconfig.json View File

@@ -7,7 +7,7 @@
] /* Specify library files to be included in the compilation. */,
"allowJs": true /* Allow javascript files to be compiled. */,
"outDir": "build" /* Redirect output structure to the directory. */,
"rootDir": "../srceal.js/assets/src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */,
"rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */,
"strict": true /* Enable all strict type-checking options. */,
"skipLibCheck": true,
"noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */,

+ 20
- 10
frontend/package-lock.json View File

@@ -3281,6 +3281,22 @@
}
}
},
"apollo-link-ws": {
"version": "1.0.20",
"resolved": "https://registry.npmjs.org/apollo-link-ws/-/apollo-link-ws-1.0.20.tgz",
"integrity": "sha512-mjSFPlQxmoLArpHBeUb2Xj+2HDYeTaJqFGOqQ+I8NVJxgL9lJe84PDWcPah/yMLv3rB7QgBDSuZ0xoRFBPlySw==",
"requires": {
"apollo-link": "^1.2.14",
"tslib": "^1.9.3"
},
"dependencies": {
"tslib": {
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz",
"integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q=="
}
}
},
"apollo-utilities": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/apollo-utilities/-/apollo-utilities-1.3.4.tgz",
@@ -3486,8 +3502,7 @@
"async-limiter": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz",
"integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==",
"dev": true
"integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ=="
},
"asynckit": {
"version": "0.4.0",
@@ -3648,8 +3663,7 @@
"backo2": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz",
"integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=",
"dev": true
"integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc="
},
"balanced-match": {
"version": "1.0.0",
@@ -8240,8 +8254,7 @@
"iterall": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/iterall/-/iterall-1.3.0.tgz",
"integrity": "sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg==",
"dev": true
"integrity": "sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg=="
},
"jasmine": {
"version": "2.8.0",
@@ -14214,7 +14227,6 @@
"version": "0.9.17",
"resolved": "https://registry.npmjs.org/subscriptions-transport-ws/-/subscriptions-transport-ws-0.9.17.tgz",
"integrity": "sha512-hNHi2N80PBz4T0V0QhnnsMGvG3XDFDS9mS6BhZ3R12T6EBywC8d/uJscsga0cVO4DKtXCkCRrWm2sOYrbOdhEA==",
"dev": true,
"requires": {
"backo2": "^1.0.2",
"eventemitter3": "^3.1.0",
@@ -14226,14 +14238,12 @@
"eventemitter3": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz",
"integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==",
"dev": true
"integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q=="
},
"ws": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz",
"integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==",
"dev": true,
"requires": {
"async-limiter": "~1.0.0"
}

+ 2
- 0
frontend/package.json View File

@@ -27,9 +27,11 @@
"apollo-cache-inmemory": "^1.6.0",
"apollo-client": "^2.6.0",
"apollo-link": "^1.2.11",
"apollo-link-ws": "^1.0.20",
"graphql": "^15.3.0",
"graphql-tag": "^2.10.0",
"rxjs": "~6.5.5",
"subscriptions-transport-ws": "^0.9.17",
"tslib": "^2.0.0",
"zone.js": "~0.10.3"
},

+ 6
- 0
frontend/src/app/app.component.html View File

@@ -15,6 +15,12 @@
<mat-sidenav-content>
<app-pizza-list></app-pizza-list>
<app-pizza-list-with-topping></app-pizza-list-with-topping>

<div class=row>
<app-pizza-create class="col-6"></app-pizza-create>
<app-topping-create class="col-6"></app-topping-create>
</div>

<mat-toolbar class="footer"></mat-toolbar>
</mat-sidenav-content>
</mat-sidenav-container>

+ 60
- 10
frontend/src/app/app.module.ts View File

@@ -1,32 +1,46 @@
import { BrowserModule } from '@angular/platform-browser';
import { CommonModule } from '@angular/common';
import { HttpClientModule } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatListModule } from '@angular/material/list';
import { MatMenuModule } from '@angular/material/menu';
import { MatSelectModule } from '@angular/material/select';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatTableModule } from '@angular/material/table';
import { MatTabsModule } from '@angular/material/tabs';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatButtonModule } from '@angular/material/button';
import { AppComponent } from './app.component';
import { GraphQLModule } from '../graphql.module';
import { HttpClientModule } from '@angular/common/http';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { PizzaListComponent } from './pizza-list/pizza-list.component';
import { Apollo } from 'apollo-angular';
import { HttpLink } from 'apollo-angular-link-http';
import { GraphQLModule } from '../graphql.module';
import { AppComponent } from './app.component';
import { PizzaCreateComponent } from './pizza-create/pizza-create.component';
import { PizzaListWithToppingComponent } from './pizza-list-with-topping/pizza-list-with-topping.component';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { PizzaListComponent } from './pizza-list/pizza-list.component';
import { ToppingCreateComponent } from './topping-create/topping-create.component';



@NgModule({
declarations: [
AppComponent,
PizzaListComponent,
PizzaListWithToppingComponent,
PizzaCreateComponent,
ToppingCreateComponent,
],
imports: [
MatSnackBarModule,
MatSelectModule,
MatFormFieldModule,
MatCheckboxModule,
MatCardModule,
MatInputModule,
@@ -41,7 +55,7 @@ import { FormsModule } from '@angular/forms';
BrowserModule,
HttpClientModule,
CommonModule,
FormsModule,
ReactiveFormsModule,
GraphQLModule,
BrowserModule,
GraphQLModule,
@@ -51,4 +65,40 @@ import { FormsModule } from '@angular/forms';
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
export class AppModule {
constructor(
apollo: Apollo,
httpLink: HttpLink
) {
// // Create an http link:
// const http = httpLink.create({
// uri: 'http://localhost:4000'
// });

// // Create a WebSocket link:
// const ws = new WebSocketLink({
// uri: `ws://localhost:4000/`,
// options: {
// reconnect: true
// }
// });

// // using the ability to split links, you can send data to each link
// // depending on what kind of operation is being sent
// const link = split(
// // split based on operation type
// ({ query }) => {
// const { kind } = getMainDefinition(query);
// return kind === 'OperationDefinition';
// },
// ws,
// http,
// );

// apollo.create({
// link,
// cache: null
// });
}

}

+ 6
- 0
frontend/src/app/pizza-create/pizza-create-topping-subscription.graphql View File

@@ -0,0 +1,6 @@
subscription ToppingChanged {
toppingsChanged {
id
name
}
}

+ 6
- 0
frontend/src/app/pizza-create/pizza-create-toppings.graphql View File

@@ -0,0 +1,6 @@
query ListToppingsForPizzaCreate {
listTopping {
id
name
}
}

+ 36
- 0
frontend/src/app/pizza-create/pizza-create.component.html View File

@@ -0,0 +1,36 @@
<div class="container">
<form [formGroup]="pizzaCreateForm" (ngSubmit)="create()" class="form">
<h1>Create Pizza</h1>
<mat-form-field class="form-element">
<mat-label>
Name
</mat-label>
<input matInput type="text" formControlName="pizzaName" />
</mat-form-field>

<mat-form-field appearance="fill">
<mat-label>Toppings</mat-label>
<mat-select
name="selectedToppings"
[formControl]="pizzaToppings"
multiple
>
<mat-select-trigger>
{{ pizzaToppings.value ? pizzaToppings.value[0]?.name : "" }}
<span
*ngIf="pizzaToppings.value?.length > 1"
class="example-additional-selection"
>
(+{{ pizzaToppings.value?.length - 1 }}
{{ pizzaToppings.value?.length === 2 ? "other" : "others" }})
</span>
</mat-select-trigger>
<mat-option *ngFor="let topping of toppings" [value]="topping">{{
topping.name
}}</mat-option>
</mat-select>
</mat-form-field>
<br>
<button mat-button type="submit">Submit</button>
</form>
</div>

+ 0
- 0
frontend/src/app/pizza-create/pizza-create.component.scss View File


+ 25
- 0
frontend/src/app/pizza-create/pizza-create.component.spec.ts View File

@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';

import { PizzaCreateComponent } from './pizza-create.component';

describe('PizzaCreateComponent', () => {
let component: PizzaCreateComponent;
let fixture: ComponentFixture<PizzaCreateComponent>;

beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ PizzaCreateComponent ]
})
.compileComponents();
}));

beforeEach(() => {
fixture = TestBed.createComponent(PizzaCreateComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});

+ 69
- 0
frontend/src/app/pizza-create/pizza-create.component.ts View File

@@ -0,0 +1,69 @@
import { Component, OnInit } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { map } from 'rxjs/operators';
import { ChangePizzaDto, CreatePizzaGQL, ListToppingsForPizzaCreateGQL, Topping, ToppingChangedGQL } from 'src/generated/graphql';
import { MatSnackBar } from '@angular/material/snack-bar';

@Component({
selector: 'app-pizza-create',
templateUrl: './pizza-create.component.html',
styleUrls: ['./pizza-create.component.scss']
})
export class PizzaCreateComponent implements OnInit {


pizzaName = new FormControl('');
pizzaToppings = new FormControl([]);
pizzaCreateForm = new FormGroup({
pizzaName: this.pizzaName,
pizzaToppings: this.pizzaToppings,
});

toppings: Topping[];

create(): void {
if (!this.pizzaCreateForm.valid) {
return;
}
const createPizzaDto: ChangePizzaDto = {
name: this.pizzaName.value,
toppingIds: this.pizzaToppings.value.map((topping: Topping) => topping.id)
};
this.createPizza
.mutate({ createPizzaDto })
.subscribe((res) => {
this.snackBar.open(
`created Pizza ${res.data.createPizza.name}`,
'ok',
{
duration: 1000
}
);
});
this.pizzaName.setValue(null);
this.pizzaToppings.setValue([]);
}

ngOnInit(): void {
}

constructor(
listToppings: ListToppingsForPizzaCreateGQL,
protected createPizza: CreatePizzaGQL,
toppingChanged: ToppingChangedGQL,
private snackBar: MatSnackBar
) {
listToppings
.watch()
.valueChanges
.pipe(map(result => result.data.listTopping as Topping[]))
.subscribe(toppings => this.toppings = toppings);
toppingChanged
.subscribe()
.pipe(map(event => event.data.toppingsChanged as Topping[]))
.subscribe(toppings => this.toppings = toppings);
}



}

+ 5
- 0
frontend/src/app/pizza-create/pizza-create.graphql View File

@@ -0,0 +1,5 @@
mutation CreatePizza($createPizzaDto: ChangePizzaDto) {
createPizza (createPizzaDto: $createPizzaDto) {
name
}
}

+ 9
- 0
frontend/src/app/pizza-list-with-topping/pizza-list-with-topping-subscription.graphql View File

@@ -0,0 +1,9 @@
subscription PizzaWithToppingChanged {
pizzasChanged {
id,
name
toppings {
name
}
}
}

+ 32
- 30
frontend/src/app/pizza-list-with-topping/pizza-list-with-topping.component.html View File

@@ -1,34 +1,36 @@
<mat-toolbar>
<mat-toolbar-row>
<h1>Pizzas With topping</h1>
</mat-toolbar-row>
</mat-toolbar>
<div class="container">
<mat-toolbar>
<mat-toolbar-row>
<h1>Pizzas With topping</h1>
</mat-toolbar-row>
</mat-toolbar>

<table mat-table [dataSource]="pizzas">
<ng-container matColumnDef="id">
<th class="id" mat-header-cell *matHeaderCellDef>ID</th>
<td class="id" mat-cell *matCellDef="let pizza; let i = index">
{{ pizza.id }}
</td>
</ng-container>
<table mat-table [dataSource]="pizzas">
<ng-container matColumnDef="id">
<th class="id" mat-header-cell *matHeaderCellDef>ID</th>
<td class="id" mat-cell *matCellDef="let pizza; let i = index">
{{ pizza.id }}
</td>
</ng-container>

<ng-container matColumnDef="name">
<th class="name" mat-header-cell *matHeaderCellDef>Name</th>
<td class="name" mat-cell *matCellDef="let pizza; let i = index">
{{ pizza.name }}
</td>
</ng-container>
<ng-container matColumnDef="name">
<th class="name" mat-header-cell *matHeaderCellDef>Name</th>
<td class="name" mat-cell *matCellDef="let pizza; let i = index">
{{ pizza.name }}
</td>
</ng-container>

<ng-container matColumnDef="toppings">
<th class="toppings" mat-header-cell *matHeaderCellDef>Name</th>
<td class="toppings" mat-cell *matCellDef="let pizza; let i = index">
{{ getToppingListString(pizza.toppings) }}
</td>
</ng-container>
<ng-container matColumnDef="toppings">
<th class="toppings" mat-header-cell *matHeaderCellDef>Name</th>
<td class="toppings" mat-cell *matCellDef="let pizza; let i = index">
{{ getToppingListString(pizza.toppings) }}
</td>
</ng-container>

<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
<ng-template #nopizzaSelected>
no pizza selected, please select a pizza to see events!
</ng-template>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
<ng-template #nopizzaSelected>
no pizza selected, please select a pizza to see events!
</ng-template>
</div>

+ 10
- 8
frontend/src/app/pizza-list-with-topping/pizza-list-with-topping.component.ts View File

@@ -1,7 +1,7 @@
import { Component, OnInit } from '@angular/core';
import { Pizza, ListPizzaGQL, ListPizzaWithToppingGQL, Topping } from 'src/generated/graphql';
import { Observable } from 'rxjs';
import { Subject } from 'rxjs';
import { map } from 'rxjs/operators';
import { ListPizzaWithToppingGQL, Pizza, PizzaWithToppingChangedGQL, Topping } from 'src/generated/graphql';


@Component({
@@ -18,15 +18,17 @@ export class PizzaListWithToppingComponent implements OnInit {
'toppings'
];

pizzas: Observable<Pizza[]>;
pizzas = new Subject<Pizza[]>();



constructor(listPizzaWithToppings: ListPizzaWithToppingGQL) {
this.pizzas = listPizzaWithToppings
constructor(listPizzaWithToppings: ListPizzaWithToppingGQL, pizzaChanged: PizzaWithToppingChangedGQL) {
listPizzaWithToppings
.watch()
.valueChanges
.pipe(map(result => result.data.listPizza as Pizza[]));
.pipe(map(result => result.data.listPizza as Pizza[]))
.subscribe((pizzas => this.pizzas.next(pizzas)));
pizzaChanged.subscribe().pipe(map(event => event.data.pizzasChanged as Pizza[]))
.subscribe((pizzas => this.pizzas.next(pizzas)));

}

ngOnInit(): void {

+ 6
- 0
frontend/src/app/pizza-list/pizza-list-subscription.graphql View File

@@ -0,0 +1,6 @@
subscription PizzaChanged {
pizzasChanged {
id
name
}
}

+ 26
- 24
frontend/src/app/pizza-list/pizza-list.component.html View File

@@ -1,27 +1,29 @@
<mat-toolbar>
<mat-toolbar-row>
<h1>Pizza</h1>
</mat-toolbar-row>
</mat-toolbar>
<div class="container">
<mat-toolbar>
<mat-toolbar-row>
<h1>Pizza</h1>
</mat-toolbar-row>
</mat-toolbar>

<table mat-table [dataSource]="pizzas">
<ng-container matColumnDef="id">
<th class="id" mat-header-cell *matHeaderCellDef>ID</th>
<td class="id" mat-cell *matCellDef="let pizza; let i = index">
{{ pizza.id }}
</td>
</ng-container>
<table mat-table [dataSource]="pizzas">
<ng-container matColumnDef="id">
<th class="id" mat-header-cell *matHeaderCellDef>ID</th>
<td class="id" mat-cell *matCellDef="let pizza; let i = index">
{{ pizza.id }}
</td>
</ng-container>

<ng-container matColumnDef="name">
<th class="name" mat-header-cell *matHeaderCellDef>Name</th>
<td class="name" mat-cell *matCellDef="let pizza; let i = index">
{{ pizza.name }}
</td>
</ng-container>
<ng-container matColumnDef="name">
<th class="name" mat-header-cell *matHeaderCellDef>Name</th>
<td class="name" mat-cell *matCellDef="let pizza; let i = index">
{{ pizza.name }}
</td>
</ng-container>

<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
<ng-template #nopizzaSelected>
no pizza selected, please select a pizza to see events!
</ng-template>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
<ng-template #nopizzaSelected>
no pizza selected, please select a pizza to see events!
</ng-template>
</div>

+ 13
- 9
frontend/src/app/pizza-list/pizza-list.component.ts View File

@@ -1,6 +1,6 @@
import { Component, OnInit } from '@angular/core';
import { Pizza, ListPizzaGQL } from 'src/generated/graphql';
import { Observable } from 'rxjs';
import { Component, OnInit, DebugElement } from '@angular/core';
import { Pizza, ListPizzaGQL, PizzaChangedGQL } from 'src/generated/graphql';
import { Observable, race, combineLatest, concat, Subject } from 'rxjs';
import { map } from 'rxjs/operators';


@@ -17,13 +17,17 @@ export class PizzaListComponent implements OnInit {
'name',
];

pizzas: Observable<Pizza[]>;
pizzas = new Subject<Pizza[]>();

constructor(listPizza: ListPizzaGQL) {
this.pizzas = listPizza
.watch()
.valueChanges
.pipe(map(result => result.data.listPizza as Pizza[]));
constructor(listPizza: ListPizzaGQL, pizzaChanged: PizzaChangedGQL) {

listPizza
.watch()
.valueChanges
.pipe(map(result => result.data.listPizza as Pizza[]))
.subscribe((pizzas => this.pizzas.next(pizzas)));
pizzaChanged.subscribe().pipe(map(event => event.data.pizzasChanged as Pizza[]))
.subscribe((pizzas => this.pizzas.next(pizzas)));
}

ngOnInit(): void {

+ 13
- 0
frontend/src/app/topping-create/topping-create.component.html View File

@@ -0,0 +1,13 @@
<div class="container">
<form [formGroup]="toppingCreateForm" (ngSubmit)="create()" class="form">
<h1>Create Topping</h1>
<mat-form-field class="form-element">
<mat-label>
Name
</mat-label>
<input matInput type="text" formControlName="toppingName" />
</mat-form-field>
<br>
<button mat-button type="submit">Submit</button>
</form>
</div>

+ 0
- 0
frontend/src/app/topping-create/topping-create.component.scss View File


+ 25
- 0
frontend/src/app/topping-create/topping-create.component.spec.ts View File

@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';

import { ToppingCreateComponent } from './topping-create.component';

describe('ToppingCreateComponent', () => {
let component: ToppingCreateComponent;
let fixture: ComponentFixture<ToppingCreateComponent>;

beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ ToppingCreateComponent ]
})
.compileComponents();
}));

beforeEach(() => {
fixture = TestBed.createComponent(ToppingCreateComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});

+ 49
- 0
frontend/src/app/topping-create/topping-create.component.ts View File

@@ -0,0 +1,49 @@
import { Component, OnInit } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { ChangeToppingDto, CreateToppingGQL } from 'src/generated/graphql';
import { MatSnackBar } from '@angular/material/snack-bar';

@Component({
selector: 'app-topping-create',
templateUrl: './topping-create.component.html',
styleUrls: ['./topping-create.component.scss']
})
export class ToppingCreateComponent implements OnInit {

toppingName = new FormControl('');
toppingCreateForm = new FormGroup({
toppingName: this.toppingName,
});


create(): void {
if (!this.toppingCreateForm.valid) {
return;
}
const createToppingDto: ChangeToppingDto = {
name: this.toppingName.value,
};
this.createToping
.mutate({ createToppingDto })
.subscribe((res) => {
this.snackBar.open(
`created Topping ${res.data.createTopping.name}`,
'ok',
{
duration: 1000
}
);
});
this.toppingName.setValue(null);
}

ngOnInit(): void {
}

constructor(
protected createToping: CreateToppingGQL,
private snackBar: MatSnackBar
) {
}

}

+ 5
- 0
frontend/src/app/topping-create/topping-create.graphql View File

@@ -0,0 +1,5 @@
mutation CreateTopping($createToppingDto: ChangeToppingDto) {
createTopping (createToppingDto: $createToppingDto) {
name
}
}

+ 216
- 0
frontend/src/generated/graphql.ts View File

@@ -59,12 +59,107 @@ export type QueryGetToppingByNameArgs = {
toppingName: Scalars['ID'];
};

export type ChangePizzaDto = {
name: Scalars['String'];
toppingIds: Array<Maybe<Scalars['ID']>>;
};

export type ChangeToppingDto = {
name: Scalars['String'];
};

export type Mutation = {
__typename?: 'Mutation';
createPizza: Pizza;
updatePizza: Pizza;
createTopping: Topping;
updateTopping: Topping;
};


export type MutationCreatePizzaArgs = {
createPizzaDto?: Maybe<ChangePizzaDto>;
};


export type MutationUpdatePizzaArgs = {
pizzaId: Scalars['ID'];
updatedPizzaDto: ChangePizzaDto;
};


export type MutationCreateToppingArgs = {
createToppingDto?: Maybe<ChangeToppingDto>;
};


export type MutationUpdateToppingArgs = {
toppingId: Scalars['ID'];
updatedToppingDto: ChangeToppingDto;
};

export type Subscription = {
__typename?: 'Subscription';
pizzasChanged: Array<Maybe<Pizza>>;
toppingsChanged: Array<Maybe<Topping>>;
};

export enum CacheControlScope {
Public = 'PUBLIC',
Private = 'PRIVATE'
}


export type ToppingChangedSubscriptionVariables = Exact<{ [key: string]: never; }>;


export type ToppingChangedSubscription = (
{ __typename?: 'Subscription' }
& { toppingsChanged: Array<Maybe<(
{ __typename?: 'Topping' }
& Pick<Topping, 'id' | 'name'>
)>> }
);

export type ListToppingsForPizzaCreateQueryVariables = Exact<{ [key: string]: never; }>;


export type ListToppingsForPizzaCreateQuery = (
{ __typename?: 'Query' }
& { listTopping: Array<Maybe<(
{ __typename?: 'Topping' }
& Pick<Topping, 'id' | 'name'>
)>> }
);

export type CreatePizzaMutationVariables = Exact<{
createPizzaDto?: Maybe<ChangePizzaDto>;
}>;


export type CreatePizzaMutation = (
{ __typename?: 'Mutation' }
& { createPizza: (
{ __typename?: 'Pizza' }
& Pick<Pizza, 'name'>
) }
);

export type PizzaWithToppingChangedSubscriptionVariables = Exact<{ [key: string]: never; }>;


export type PizzaWithToppingChangedSubscription = (
{ __typename?: 'Subscription' }
& { pizzasChanged: Array<Maybe<(
{ __typename?: 'Pizza' }
& Pick<Pizza, 'id' | 'name'>
& { toppings: Array<(
{ __typename?: 'Topping' }
& Pick<Topping, 'name'>
)> }
)>> }
);

export type ListPizzaWithToppingQueryVariables = Exact<{ [key: string]: never; }>;


@@ -80,6 +175,17 @@ export type ListPizzaWithToppingQuery = (
)>> }
);

export type PizzaChangedSubscriptionVariables = Exact<{ [key: string]: never; }>;


export type PizzaChangedSubscription = (
{ __typename?: 'Subscription' }
& { pizzasChanged: Array<Maybe<(
{ __typename?: 'Pizza' }
& Pick<Pizza, 'id' | 'name'>
)>> }
);

export type ListPizzaQueryVariables = Exact<{ [key: string]: never; }>;


@@ -91,6 +197,85 @@ export type ListPizzaQuery = (
)>> }
);

export type CreateToppingMutationVariables = Exact<{
createToppingDto?: Maybe<ChangeToppingDto>;
}>;


export type CreateToppingMutation = (
{ __typename?: 'Mutation' }
& { createTopping: (
{ __typename?: 'Topping' }
& Pick<Topping, 'name'>
) }
);

export const ToppingChangedDocument = gql`
subscription ToppingChanged {
toppingsChanged {
id
name
}
}
`;

@Injectable({
providedIn: 'root'
})
export class ToppingChangedGQL extends Apollo.Subscription<ToppingChangedSubscription, ToppingChangedSubscriptionVariables> {
document = ToppingChangedDocument;
}
export const ListToppingsForPizzaCreateDocument = gql`
query ListToppingsForPizzaCreate {
listTopping {
id
name
}
}
`;

@Injectable({
providedIn: 'root'
})
export class ListToppingsForPizzaCreateGQL extends Apollo.Query<ListToppingsForPizzaCreateQuery, ListToppingsForPizzaCreateQueryVariables> {
document = ListToppingsForPizzaCreateDocument;
}
export const CreatePizzaDocument = gql`
mutation CreatePizza($createPizzaDto: ChangePizzaDto) {
createPizza(createPizzaDto: $createPizzaDto) {
name
}
}
`;

@Injectable({
providedIn: 'root'
})
export class CreatePizzaGQL extends Apollo.Mutation<CreatePizzaMutation, CreatePizzaMutationVariables> {
document = CreatePizzaDocument;
}
export const PizzaWithToppingChangedDocument = gql`
subscription PizzaWithToppingChanged {
pizzasChanged {
id
name
toppings {
name
}
}
}
`;

@Injectable({
providedIn: 'root'
})
export class PizzaWithToppingChangedGQL extends Apollo.Subscription<PizzaWithToppingChangedSubscription, PizzaWithToppingChangedSubscriptionVariables> {
document = PizzaWithToppingChangedDocument;
}
export const ListPizzaWithToppingDocument = gql`
query ListPizzaWithTopping {
listPizza {
@@ -109,6 +294,22 @@ export const ListPizzaWithToppingDocument = gql`
export class ListPizzaWithToppingGQL extends Apollo.Query<ListPizzaWithToppingQuery, ListPizzaWithToppingQueryVariables> {
document = ListPizzaWithToppingDocument;
}
export const PizzaChangedDocument = gql`
subscription PizzaChanged {
pizzasChanged {
id
name
}
}
`;

@Injectable({
providedIn: 'root'
})
export class PizzaChangedGQL extends Apollo.Subscription<PizzaChangedSubscription, PizzaChangedSubscriptionVariables> {
document = PizzaChangedDocument;
}
export const ListPizzaDocument = gql`
query ListPizza {
@@ -125,4 +326,19 @@ export const ListPizzaDocument = gql`
export class ListPizzaGQL extends Apollo.Query<ListPizzaQuery, ListPizzaQueryVariables> {
document = ListPizzaDocument;
}
export const CreateToppingDocument = gql`
mutation CreateTopping($createToppingDto: ChangeToppingDto) {
createTopping(createToppingDto: $createToppingDto) {
name
}
}
`;

@Injectable({
providedIn: 'root'
})
export class CreateToppingGQL extends Apollo.Mutation<CreateToppingMutation, CreateToppingMutationVariables> {
document = CreateToppingDocument;
}

+ 37
- 7
frontend/src/graphql.module.ts View File

@@ -1,13 +1,43 @@
import {NgModule} from '@angular/core';
import {ApolloModule, APOLLO_OPTIONS} from 'apollo-angular';
import {HttpLinkModule, HttpLink} from 'apollo-angular-link-http';
import {InMemoryCache} from 'apollo-cache-inmemory';
import { NgModule } from '@angular/core';
import { ApolloModule, APOLLO_OPTIONS, Apollo } from 'apollo-angular';
import { HttpLink, HttpLinkModule } from 'apollo-angular-link-http';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { WebSocketLink } from 'apollo-link-ws';
import { split } from 'apollo-link';
import { getMainDefinition } from 'apollo-utilities';
import { OperationDefinitionNode } from 'graphql';


const uri = 'http://localhost:4000'; // <-- add the URL of the GraphQL server here
export function createApollo(httpLink: HttpLink) {
const wsUri = 'ws://localhost:4000/graphql'; // <-- add the URL of the GraphQL server here
export function createApollo(
httpLink: HttpLink
) {

const http = httpLink.create({
uri
});

const ws = new WebSocketLink({
uri: wsUri,
options: {
reconnect: true,
},
});
const link = split(
// split based on operation type
({ query }) => {
const { kind, operation } = getMainDefinition(query) as OperationDefinitionNode;
return kind === 'OperationDefinition' && operation === 'subscription';
},
ws,
http,
);

return {
link: httpLink.create({uri}),
link,
cache: new InMemoryCache(),

};
}

@@ -21,4 +51,4 @@ export function createApollo(httpLink: HttpLink) {
},
],
})
export class GraphQLModule {}
export class GraphQLModule { }

+ 16
- 0
frontend/src/styles.scss View File

@@ -318,3 +318,19 @@ h1.main-app-name {
table {
width: 100%;
}


.container {
padding: 10px;
}

.form {
min-width: 150px;
max-width: 500px;
width: 100%;
}

.form-element {
padding: 5px 0px 25px 2px;
width: 100%;
}

+ 2
- 1
presentation/index.html View File

@@ -272,7 +272,8 @@
</code>
</pre>
</section>

</section>
<section>
<section>
<img
src="/assets/apollo-logo.png"

Loading…
Cancel
Save