@@ -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 | |||
``` | |||
@@ -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", |
@@ -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>; | |||
}; | |||
@@ -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}`); | |||
}); |
@@ -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; | |||
}; | |||
@@ -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; | |||
}; |
@@ -43,4 +43,9 @@ export const pizzaSchema = gql` | |||
createTopping(createToppingDto: ChangeToppingDto): Topping! | |||
updateTopping(toppingId: ID!, updatedToppingDto: ChangeToppingDto!): Topping! | |||
} | |||
type Subscription { | |||
pizzasChanged: [Pizza]! | |||
toppingsChanged: [Topping]! | |||
} | |||
`; |
@@ -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. */, |
@@ -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" | |||
} |
@@ -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" | |||
}, |
@@ -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> |
@@ -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 | |||
// }); | |||
} | |||
} |
@@ -0,0 +1,6 @@ | |||
subscription ToppingChanged { | |||
toppingsChanged { | |||
id | |||
name | |||
} | |||
} |
@@ -0,0 +1,6 @@ | |||
query ListToppingsForPizzaCreate { | |||
listTopping { | |||
id | |||
name | |||
} | |||
} |
@@ -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 +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(); | |||
}); | |||
}); |
@@ -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); | |||
} | |||
} |
@@ -0,0 +1,5 @@ | |||
mutation CreatePizza($createPizzaDto: ChangePizzaDto) { | |||
createPizza (createPizzaDto: $createPizzaDto) { | |||
name | |||
} | |||
} |
@@ -0,0 +1,9 @@ | |||
subscription PizzaWithToppingChanged { | |||
pizzasChanged { | |||
id, | |||
name | |||
toppings { | |||
name | |||
} | |||
} | |||
} |
@@ -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> |
@@ -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 { |
@@ -0,0 +1,6 @@ | |||
subscription PizzaChanged { | |||
pizzasChanged { | |||
id | |||
name | |||
} | |||
} |
@@ -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> |
@@ -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 { |
@@ -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 +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(); | |||
}); | |||
}); |
@@ -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 | |||
) { | |||
} | |||
} |
@@ -0,0 +1,5 @@ | |||
mutation CreateTopping($createToppingDto: ChangeToppingDto) { | |||
createTopping (createToppingDto: $createToppingDto) { | |||
name | |||
} | |||
} |
@@ -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; | |||
} |
@@ -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 { } |
@@ -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%; | |||
} |
@@ -272,7 +272,8 @@ | |||
</code> | |||
</pre> | |||
</section> | |||
</section> | |||
<section> | |||
<section> | |||
<img | |||
src="/assets/apollo-logo.png" |