diff --git a/backend/src/application.ts b/backend/src/application.ts index bd62260..fc233c3 100644 --- a/backend/src/application.ts +++ b/backend/src/application.ts @@ -10,9 +10,11 @@ import { UserServiceBindings } from '@loopback/authentication-jwt'; import { SzakdolgozatUserService } from './services'; -import { GraphQLBindings, GraphQLServer } from '@loopback/graphql'; -import { UserResolver } from './graphql-resolvers/user-resolver'; +import { GraphQLBindings, GraphQLServer, GraphQLServerOptions } from '@loopback/graphql'; +import { UserResolver } from './graphql-resolvers/user.resolver'; import { SzakdolgozatAuthChecker } from './szakdolgozat-auth-checker'; +import { CourseResolver } from './graphql-resolvers/course.resolver'; +import { SubjectResolver } from './graphql-resolvers/subject.resolver'; export { ApplicationConfig }; @@ -25,10 +27,15 @@ export class SzakdolgozatBackendApplication extends BootMixin( super(options); const server = this.server(GraphQLServer); - this.configure(server.key).to({host: process.env.HOST ?? '0.0.0.0', port: process.env.PORT ?? 3000}); + this.configure(server.key).to( { + host: process.env.HOST ?? '0.0.0.0', + port: process.env.PORT ?? 3000 + }); this.getServer(GraphQLServer).then(s => { this.gqlServer = s; s.resolver(UserResolver); + s.resolver(CourseResolver); + s.resolver(SubjectResolver); }); // Authentication diff --git a/backend/src/graphql-resolvers/course.resolver.ts b/backend/src/graphql-resolvers/course.resolver.ts new file mode 100644 index 0000000..a2b38b2 --- /dev/null +++ b/backend/src/graphql-resolvers/course.resolver.ts @@ -0,0 +1,31 @@ +import { repository } from '@loopback/repository'; +import { CourseRepository } from '../repositories'; +import { arg, ID, Int, mutation, query, resolver } from '@loopback/graphql'; +import { Course } from '../models'; +import { CourseUpdateInput } from '../graphql-types/input/course-update.input'; +import { listResponse, ListResponse } from '../graphql-types/list'; +import { CourseList } from '../graphql-types/course'; + +@resolver(of => Course) +export class CourseResolver { + constructor( + @repository('CourseRepository') private courseRepo: CourseRepository + ) { + } + + @query(returns => CourseList) + async courses(@arg('offset', returns => Int) offset: number, @arg('limit', returns => Int) limit: number): Promise> { + return listResponse(this.courseRepo, offset, limit, CourseList); + } + + @query(returns => Course) + async course(@arg('id', returns => ID) id: number): Promise { + return await this.courseRepo.findById(id); + } + + @mutation(returns => Boolean) + async courseUpdate(@arg('course') input: CourseUpdateInput): Promise { + await this.courseRepo.updateById(input.id, input); + return true; + } +} diff --git a/backend/src/graphql-resolvers/subject.resolver.ts b/backend/src/graphql-resolvers/subject.resolver.ts new file mode 100644 index 0000000..4ef2eac --- /dev/null +++ b/backend/src/graphql-resolvers/subject.resolver.ts @@ -0,0 +1,29 @@ +import { arg, ID, Int, mutation, query, resolver } from '@loopback/graphql'; +import { Subject } from '../models'; +import { SubjectRepository } from '../repositories'; +import { repository } from '@loopback/repository'; +import { listResponse, ListResponse } from '../graphql-types/list'; +import { SubjectList } from '../graphql-types/subject'; +import { SubjectUpdateInput } from '../graphql-types/input/subject-update.input'; + +@resolver(of => Subject) +export class SubjectResolver { + constructor(@repository('SubjectRepository') private subjectRepo: SubjectRepository) { + } + + @query(returns => SubjectList) + async subjects(@arg('offset', returns => Int) offset: number, @arg('limit', returns => Int) limit: number): Promise> { + return listResponse(this.subjectRepo, offset, limit, SubjectList); + } + + @query(returns => Subject) + async subject(@arg('id', returns => ID) id: number): Promise { + return await this.subjectRepo.findById(id); + } + + @mutation(returns => Boolean) + async subjectUpdate(@arg('subject') input: SubjectUpdateInput): Promise { + await this.subjectRepo.updateById(input.id, input); + return true; + } +} diff --git a/backend/src/graphql-resolvers/user-resolver.ts b/backend/src/graphql-resolvers/user.resolver.ts similarity index 89% rename from backend/src/graphql-resolvers/user-resolver.ts rename to backend/src/graphql-resolvers/user.resolver.ts index 070dad3..aaf231c 100644 --- a/backend/src/graphql-resolvers/user-resolver.ts +++ b/backend/src/graphql-resolvers/user.resolver.ts @@ -1,4 +1,4 @@ -import { arg, authorized, GraphQLBindings, Int, mutation, query, resolver, ResolverData } from '@loopback/graphql'; +import { arg, authorized, GraphQLBindings, ID, mutation, query, resolver, ResolverData } from '@loopback/graphql'; import { User } from '../models'; import { repository } from '@loopback/repository'; import { RevTokenRepository, UserRepository } from '../repositories'; @@ -63,19 +63,19 @@ export class UserResolver { @authorized() @query(returns => [User]) - async find(user: Partial): Promise { + async findUser(user: Partial): Promise { return this.userRepository.find({}); //TODO } @authorized() - @query(returns => User) - async findById(@arg('id', returns => Int) id: number): Promise { + @query(returns => User, {name: 'user'}) + async findById(@arg('id') id: number): Promise { return this.userRepository.findById(id); } @authorized() @mutation(returns => Boolean) - async updateById(@arg('id', returns => Int) id: number, @arg('user') user: UserUpdateInput): Promise { + async userUpdate(@arg('id', returns => ID) id: number, @arg('user') user: UserUpdateInput): Promise { if (id === +this.user?.id) { //TODO: this.user const loggedInUser = await this.userService.findUserById(this.user.id); if (user.isAdmin !== undefined && loggedInUser.isAdmin !== user.isAdmin) { @@ -88,7 +88,7 @@ export class UserResolver { @authorized() @mutation(returns => Boolean) - async deleteById(id: number): Promise { + async userDelete(id: number): Promise { await this.userRepository.deleteById(id); return true; } diff --git a/backend/src/graphql-types/course.ts b/backend/src/graphql-types/course.ts new file mode 100644 index 0000000..4fa678a --- /dev/null +++ b/backend/src/graphql-types/course.ts @@ -0,0 +1,11 @@ +import { ListResponse } from './list'; +import { Course } from '../models'; +import { field, Int, objectType } from '@loopback/graphql'; + +@objectType() +export class CourseList implements ListResponse { + @field(returns => Int) + count: number; + @field(returns => [Course]) + list: Course[]; +} diff --git a/backend/src/graphql-types/input/course-update.input.ts b/backend/src/graphql-types/input/course-update.input.ts new file mode 100644 index 0000000..8daf4ff --- /dev/null +++ b/backend/src/graphql-types/input/course-update.input.ts @@ -0,0 +1,15 @@ +import { field, ID, inputType } from '@loopback/graphql'; +import { Course } from '../../models'; +import { DataObject } from '@loopback/repository'; + +@inputType() +export class CourseUpdateInput implements Pick, 'id' | 'semester' | 'alias' | 'subjectId'> { + @field(returns => ID) + id: number; + @field() + semester?: string; + @field() + alias?: string; + @field() + subjectId?: number; +} diff --git a/backend/src/graphql-types/input/subject-update.input.ts b/backend/src/graphql-types/input/subject-update.input.ts new file mode 100644 index 0000000..2c39aa9 --- /dev/null +++ b/backend/src/graphql-types/input/subject-update.input.ts @@ -0,0 +1,13 @@ +import { field, ID, inputType } from '@loopback/graphql'; +import { DataObject } from '@loopback/repository'; +import { Subject } from '../../models'; + +@inputType() +export class SubjectUpdateInput implements Pick, 'id' | 'name' | 'description'> { + @field(returns => ID) + id: number; + @field() + name?: string; + @field() + description?: string; +} diff --git a/backend/src/graphql-types/list.ts b/backend/src/graphql-types/list.ts index a1c6117..ba22e1e 100644 --- a/backend/src/graphql-types/list.ts +++ b/backend/src/graphql-types/list.ts @@ -1,4 +1,5 @@ -import { field, inputType, objectType } from '@loopback/graphql'; +import { ClassType, field, inputType } from '@loopback/graphql'; +import { DefaultCrudRepository, Entity } from '@loopback/repository'; @inputType() export class ListInput { @@ -8,10 +9,14 @@ export class ListInput { limit: number; } -@objectType() -export class ListResponse { - @field() +export interface ListResponse { count: number; - @field() list: T[]; } + +export async function listResponse>(repo: DefaultCrudRepository, offset: number, limit: number, listType: ClassType) { + const list = new listType(); + list.list = await repo.find({offset, limit}); + list.count = (await repo.count()).count; + return list; +} diff --git a/backend/src/graphql-types/subject.ts b/backend/src/graphql-types/subject.ts new file mode 100644 index 0000000..0e0b0df --- /dev/null +++ b/backend/src/graphql-types/subject.ts @@ -0,0 +1,11 @@ +import { ListResponse } from './list'; +import { Subject } from '../models'; +import { field, Int, objectType } from '@loopback/graphql'; + +@objectType() +export class SubjectList implements ListResponse { + @field(returns => Int) + count: number; + @field(returns => [Subject]) + list: Subject[]; +} diff --git a/backend/src/models/course.model.ts b/backend/src/models/course.model.ts index 2d38e8c..4fd8bd0 100644 --- a/backend/src/models/course.model.ts +++ b/backend/src/models/course.model.ts @@ -3,29 +3,35 @@ import { Subject } from './subject.model'; import { User } from './user.model'; import { CourseUser } from './course-user.model'; import { Requirement } from './requirement.model'; +import { field, objectType } from '@loopback/graphql'; @model() +@objectType() export class Course extends Entity { @property({ type: 'number', id: true, generated: true, }) + @field() id?: number; @property({ type: 'string', required: true, }) + @field() semester: string; @property({ type: 'string', required: true, }) + @field() alias: string; @belongsTo(() => Subject) + @field() subjectId: number; @hasMany(() => User, {through: {model: () => CourseUser}}) diff --git a/backend/src/models/subject.model.ts b/backend/src/models/subject.model.ts index c813b3f..f09677b 100644 --- a/backend/src/models/subject.model.ts +++ b/backend/src/models/subject.model.ts @@ -1,24 +1,29 @@ -import { Entity, model, property, hasMany } from '@loopback/repository'; +import { Entity, hasMany, model, property } from '@loopback/repository'; import { Course } from './course.model'; +import { field, objectType } from '@loopback/graphql'; @model() +@objectType() export class Subject extends Entity { @property({ type: 'number', id: true, generated: true, }) + @field() id?: number; @property({ type: 'string', required: true, }) + @field() name: string; @property({ type: 'string', }) + @field() description?: string; @hasMany(() => Course) diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index 5b16571..f1e3d01 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -33,8 +33,7 @@ export class AppComponent implements OnInit { private activeRouteTitle: string; constructor(private breakpointObserver: BreakpointObserver, public loginService: LoginService, private api: ApiService, - private router: Router, private login: LoginService, private activeRoute: ActivatedRoute, - private title: Title) { + private router: Router, private activeRoute: ActivatedRoute, private title: Title) { } ngOnInit(): void { @@ -115,12 +114,12 @@ export class AppComponent implements OnInit { } async logout(): Promise { - await this.api.logout(); + await this.loginService.logout(); await this.router.navigate(['/']); } getMenuItems(): MenuItem[] { - return this.menu.filter(item => item.requiredRole === 'admin' ? this.login.user?.isAdmin : true); // TODO: Roles + return this.menu.filter(item => item.requiredRole === 'admin' ? this.loginService.user?.isAdmin : true); // TODO: Roles } routeActivated($event: any): void { diff --git a/frontend/src/app/auth/login.service.ts b/frontend/src/app/auth/login.service.ts index fe63f7b..3af62b6 100644 --- a/frontend/src/app/auth/login.service.ts +++ b/frontend/src/app/auth/login.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { environment } from '../../environments/environment'; -import { LoginGQL, UserResult } from '../services/graphql'; +import { LoginGQL, LogoutGQL, UserResult } from '../services/graphql'; @Injectable({ providedIn: 'root' @@ -19,7 +19,7 @@ export class LoginService { return this.userP; } - constructor(private http: HttpClient, private loginService: LoginGQL) { + constructor(private http: HttpClient, private loginGQL: LoginGQL, private logoutGQL: LogoutGQL) { this.tokenP = window.localStorage.getItem('token'); this.userP = JSON.parse(window.localStorage.getItem('user')); } @@ -30,7 +30,7 @@ export class LoginService { async login(email: string, password: string): Promise { try { - const resp = await this.loginService.mutate({email, password}).toPromise(); + const resp = await this.loginGQL.mutate({email, password}).toPromise(); this.tokenP = resp.data.login.token; this.userP = resp.data.login.user; window.localStorage.setItem('token', this.tokenP); @@ -50,4 +50,9 @@ export class LoginService { window.localStorage.removeItem('token'); window.localStorage.removeItem('user'); } + + async logout(): Promise { + await this.logoutGQL.mutate().toPromise(); + this.deleteToken(); + } } diff --git a/frontend/src/app/graphql/subject.graphql b/frontend/src/app/graphql/subject.graphql new file mode 100644 index 0000000..9e661f2 --- /dev/null +++ b/frontend/src/app/graphql/subject.graphql @@ -0,0 +1,18 @@ +query SubjectList($limit: Int!, $offset: Int!) { + subjects(limit: $limit, offset: $offset) { + list { + name + id + description + } + count + } +} + +query Subject($id: ID!) { + subject(id: $id) { + id + name + description + } +} diff --git a/frontend/src/app/graphql/user.graphql b/frontend/src/app/graphql/user.graphql index 099136e..0b4f7e9 100644 --- a/frontend/src/app/graphql/user.graphql +++ b/frontend/src/app/graphql/user.graphql @@ -9,3 +9,7 @@ mutation Login($email: String!, $password: String!) { } } } + +mutation Logout { + logout +} diff --git a/frontend/src/app/services/api.service.ts b/frontend/src/app/services/api.service.ts index 596062d..f520e2c 100644 --- a/frontend/src/app/services/api.service.ts +++ b/frontend/src/app/services/api.service.ts @@ -42,9 +42,4 @@ export class ApiService { requestItemCount(url: string): Promise { return this.request('get', url + '/count', {}).then(count => count.count); } - - async logout(): Promise { - await this.request('post', '/users/logout', ''); - this.loginService.deleteToken(); - } } diff --git a/frontend/src/app/shared-components/edit/edit.component.ts b/frontend/src/app/shared-components/edit/edit.component.ts index 85e7012..b093211 100644 --- a/frontend/src/app/shared-components/edit/edit.component.ts +++ b/frontend/src/app/shared-components/edit/edit.component.ts @@ -1,23 +1,23 @@ -import { Component, Input, OnInit, Type } from '@angular/core'; +import { Component, Input, OnInit } from '@angular/core'; import { ApiService } from '../../services/api.service'; import { ActivatedRoute, Router } from '@angular/router'; -import { Model } from '../../model/model'; import { FormBuilder, FormControl, FormGroup } from '@angular/forms'; +import { Query } from 'apollo-angular'; @Component({ selector: 'app-edit', templateUrl: './edit.component.html', styleUrls: ['./edit.component.css'] }) -export class EditComponent implements OnInit { +export class EditComponent implements OnInit { item?: T; creating = false; isLoading = true; - @Input() apiPath: string; + @Input() gql: Query; @Input() fields: { title: string, name: keyof T, readonly?: (item: T) => boolean }[]; - @Input() itemType: Type; + @Input() itemType: T; /** * Beküldés előtt extra adat hozzáadása */ @@ -32,7 +32,7 @@ export class EditComponent implements OnInit { window.localStorage.removeItem(this.router.url); const url = this.route.snapshot.url; if (!this.item && url[url.length - 1].path !== 'new') { - this.item = await this.api.request('get', this.apiPath + '/' + this.route.snapshot.url[this.route.snapshot.url.length - 1], {}); + //this.item = await this.api.request('get', this.apiPath + '/' + this.route.snapshot.url[this.route.snapshot.url.length - 1], {}); - TODO } this.formGroup = this.fb.group(this.fields.reduce((pv, cv) => { const control = new FormControl(); @@ -44,7 +44,7 @@ export class EditComponent implements OnInit { if (this.item) { this.formGroup.patchValue(this.item); } else { - this.item = new this.itemType(); + this.item = {} as T; this.creating = true; } this.isLoading = false; @@ -55,9 +55,9 @@ export class EditComponent implements OnInit { const value = Object.assign({}, this.formGroup.value, this.beforeSubmit(this.item) ?? {}); try { if (this.item && !this.creating) { - await this.api.request('patch', this.apiPath + '/' + this.item.id, value); + //await this.api.request('patch', this.apiPath + '/' + this.item.id, value); - TODO } else { - await this.api.request('post', this.apiPath, value); + //await this.api.request('post', this.apiPath, value); - TODO } await this.router.navigate(['..'], {relativeTo: this.route}); } catch (e) { diff --git a/frontend/src/app/shared-components/list/list.component.ts b/frontend/src/app/shared-components/list/list.component.ts index 80c7dcf..076d570 100644 --- a/frontend/src/app/shared-components/list/list.component.ts +++ b/frontend/src/app/shared-components/list/list.component.ts @@ -3,16 +3,17 @@ import { PageEvent } from '@angular/material/paginator'; import { PaginationData } from '../../utility/pagination-data'; import { ApiService } from '../../services/api.service'; import { Router } from '@angular/router'; +import { Query } from 'apollo-angular'; @Component({ selector: 'app-list', templateUrl: './list.component.html', styleUrls: ['./list.component.css'] }) -export class ListComponent implements OnInit { +export class ListComponent implements OnInit { - @Input() apiPath: string; - @Input() itemType: T; + @Input() gql: Query; + @Input() itemType: T; // TODO: Remove @Input() columns: { title: string, prop: keyof T }[]; @Input() allowNew = false; @Input() customActions: { icon: string, label: string, action: (model: T) => void }[] = []; @@ -36,19 +37,19 @@ export class ListComponent implements OnInit { async getItems(limit: number, page: number): Promise { try { this.loading = true; - if (!this.paginationData.total) { - this.paginationData.total = await this.api.requestItemCount(this.apiPath); - } + const {data} = await this.gql.fetch({limit, offset: (page - 1) * limit}).toPromise(); // TODO: Watch + const key = Object.keys(data).filter(k => k !== '__typename')[0]; + this.paginationData.total = data[key].count; this.paginationData.page = page; this.paginationData.limit = limit; - this.items = await this.api.requestPage(this.apiPath, limit, page); + this.items = data[key].list; } finally { this.loading = false; } } async editItem(item: T): Promise { - window.localStorage.setItem(this.router.url + '/' + item.id, JSON.stringify(item)); + window.localStorage.setItem(this.router.url + '/' + item.id, JSON.stringify(item)); // TODO: Apollo cache await this.router.navigate([this.router.url, item.id]); } diff --git a/frontend/src/app/subjects/subject-edit/courses/course-edit/course-edit.component.html b/frontend/src/app/subjects/subject-edit/courses/course-edit/course-edit.component.html index 08f66e1..3290f4d 100644 --- a/frontend/src/app/subjects/subject-edit/courses/course-edit/course-edit.component.html +++ b/frontend/src/app/subjects/subject-edit/courses/course-edit/course-edit.component.html @@ -1,4 +1,4 @@ - +]"> --> diff --git a/frontend/src/app/subjects/subject-edit/courses/course-edit/course-edit.component.ts b/frontend/src/app/subjects/subject-edit/courses/course-edit/course-edit.component.ts index 9928108..50f3e9b 100644 --- a/frontend/src/app/subjects/subject-edit/courses/course-edit/course-edit.component.ts +++ b/frontend/src/app/subjects/subject-edit/courses/course-edit/course-edit.component.ts @@ -7,7 +7,6 @@ import { ActivatedRoute } from '@angular/router'; styleUrls: ['./course-edit.component.css'] }) export class CourseEditComponent implements OnInit { - itemType: Course; beforeSubmit = () => ({subjectId: +this.route.snapshot.params.subjectId}); constructor(private route: ActivatedRoute) { diff --git a/frontend/src/app/subjects/subject-edit/courses/course-list/course-list.component.html b/frontend/src/app/subjects/subject-edit/courses/course-list/course-list.component.html index a21996c..5dbdcfb 100644 --- a/frontend/src/app/subjects/subject-edit/courses/course-list/course-list.component.html +++ b/frontend/src/app/subjects/subject-edit/courses/course-list/course-list.component.html @@ -1,4 +1,4 @@ - diff --git a/frontend/src/app/subjects/subject-edit/courses/course-list/course-list.component.ts b/frontend/src/app/subjects/subject-edit/courses/course-list/course-list.component.ts index 7cf0f43..a4a7296 100644 --- a/frontend/src/app/subjects/subject-edit/courses/course-list/course-list.component.ts +++ b/frontend/src/app/subjects/subject-edit/courses/course-list/course-list.component.ts @@ -1,8 +1,7 @@ -import { Component, OnInit, ViewChild } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; -import { Course } from '../../../../model/course.model'; -import { ListComponent } from '../../../../shared-components/list/list.component'; import { CustomTitleComponent } from '../../../../app.component'; +import { Course, SubjectGQL } from '../../../../services/graphql'; @Component({ selector: 'app-courses', @@ -11,10 +10,9 @@ import { CustomTitleComponent } from '../../../../app.component'; }) export class CourseListComponent implements OnInit, CustomTitleComponent { subjectId: string; - itemType = Course; - @ViewChild('list') list: ListComponent; + itemType: Course; - constructor(route: ActivatedRoute) { + constructor(route: ActivatedRoute, public listGQL: SubjectGQL) { this.subjectId = route.snapshot.params.subjectId; } @@ -22,6 +20,6 @@ export class CourseListComponent implements OnInit, CustomTitleComponent { } getPageTitle(): string { - return 'Custom title'; //TODO + return 'Custom title'; //TODO: SubjectGQL } } diff --git a/frontend/src/app/subjects/subject-edit/subject-edit.component.html b/frontend/src/app/subjects/subject-edit/subject-edit.component.html index 5c4a466..6fb21a5 100644 --- a/frontend/src/app/subjects/subject-edit/subject-edit.component.html +++ b/frontend/src/app/subjects/subject-edit/subject-edit.component.html @@ -1,4 +1,4 @@ - diff --git a/frontend/src/app/subjects/subject-edit/subject-edit.component.ts b/frontend/src/app/subjects/subject-edit/subject-edit.component.ts index 7f78116..218035b 100644 --- a/frontend/src/app/subjects/subject-edit/subject-edit.component.ts +++ b/frontend/src/app/subjects/subject-edit/subject-edit.component.ts @@ -1,5 +1,5 @@ import { Component, OnInit } from '@angular/core'; -import { Subject } from '../../model/subject.model'; +import { Subject, SubjectGQL } from '../../services/graphql'; @Component({ selector: 'app-subject-edit', @@ -7,9 +7,10 @@ import { Subject } from '../../model/subject.model'; styleUrls: ['./subject-edit.component.css'] }) export class SubjectEditComponent implements OnInit { - itemType = Subject; + itemType: Subject; - constructor() { } + constructor(public itemGQL: SubjectGQL) { + } ngOnInit(): void { } diff --git a/frontend/src/app/subjects/subject-list/subject-list.component.html b/frontend/src/app/subjects/subject-list/subject-list.component.html index 2695271..81dfa54 100644 --- a/frontend/src/app/subjects/subject-list/subject-list.component.html +++ b/frontend/src/app/subjects/subject-list/subject-list.component.html @@ -1,4 +1,4 @@ -