Add queries for subjects and courses, list subjects on frontend

Also added logout mutation
This commit is contained in:
Norbi Peti 2022-05-03 22:24:09 +02:00
parent 8471825e5d
commit a0bc7d3585
No known key found for this signature in database
GPG key ID: DBA4C4549A927E56
28 changed files with 218 additions and 66 deletions

View file

@ -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(<GraphQLServerOptions> {
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

View file

@ -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<ListResponse<Course>> {
return listResponse(this.courseRepo, offset, limit, CourseList);
}
@query(returns => Course)
async course(@arg('id', returns => ID) id: number): Promise<Course> {
return await this.courseRepo.findById(id);
}
@mutation(returns => Boolean)
async courseUpdate(@arg('course') input: CourseUpdateInput): Promise<boolean> {
await this.courseRepo.updateById(input.id, input);
return true;
}
}

View file

@ -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<ListResponse<Subject>> {
return listResponse(this.subjectRepo, offset, limit, SubjectList);
}
@query(returns => Subject)
async subject(@arg('id', returns => ID) id: number): Promise<Subject> {
return await this.subjectRepo.findById(id);
}
@mutation(returns => Boolean)
async subjectUpdate(@arg('subject') input: SubjectUpdateInput): Promise<boolean> {
await this.subjectRepo.updateById(input.id, input);
return true;
}
}

View file

@ -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<User>): Promise<User[]> {
async findUser(user: Partial<User>): Promise<User[]> {
return this.userRepository.find({}); //TODO
}
@authorized()
@query(returns => User)
async findById(@arg('id', returns => Int) id: number): Promise<User> {
@query(returns => User, {name: 'user'})
async findById(@arg('id') id: number): Promise<User> {
return this.userRepository.findById(id);
}
@authorized()
@mutation(returns => Boolean)
async updateById(@arg('id', returns => Int) id: number, @arg('user') user: UserUpdateInput): Promise<boolean> {
async userUpdate(@arg('id', returns => ID) id: number, @arg('user') user: UserUpdateInput): Promise<boolean> {
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<Boolean> {
async userDelete(id: number): Promise<Boolean> {
await this.userRepository.deleteById(id);
return true;
}

View file

@ -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<Course> {
@field(returns => Int)
count: number;
@field(returns => [Course])
list: Course[];
}

View file

@ -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<DataObject<Course>, 'id' | 'semester' | 'alias' | 'subjectId'> {
@field(returns => ID)
id: number;
@field()
semester?: string;
@field()
alias?: string;
@field()
subjectId?: number;
}

View file

@ -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<DataObject<Subject>, 'id' | 'name' | 'description'> {
@field(returns => ID)
id: number;
@field()
name?: string;
@field()
description?: string;
}

View file

@ -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<T> {
@field()
export interface ListResponse<T> {
count: number;
@field()
list: T[];
}
export async function listResponse<T extends Entity, U extends ListResponse<T>>(repo: DefaultCrudRepository<T, number>, offset: number, limit: number, listType: ClassType<U>) {
const list = new listType();
list.list = await repo.find({offset, limit});
list.count = (await repo.count()).count;
return list;
}

View file

@ -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<Subject> {
@field(returns => Int)
count: number;
@field(returns => [Subject])
list: Subject[];
}

View file

@ -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}})

View file

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

View file

@ -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<void> {
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 {

View file

@ -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<boolean> {
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<void> {
await this.logoutGQL.mutate().toPromise();
this.deleteToken();
}
}

View file

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

View file

@ -9,3 +9,7 @@ mutation Login($email: String!, $password: String!) {
}
}
}
mutation Logout {
logout
}

View file

@ -42,9 +42,4 @@ export class ApiService {
requestItemCount(url: string): Promise<number> {
return this.request('get', url + '/count', {}).then(count => count.count);
}
async logout(): Promise<void> {
await this.request('post', '/users/logout', '');
this.loginService.deleteToken();
}
}

View file

@ -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<T extends Model> implements OnInit {
export class EditComponent<T extends { id: number }, U> implements OnInit {
item?: T;
creating = false;
isLoading = true;
@Input() apiPath: string;
@Input() gql: Query<U, {}>;
@Input() fields: { title: string, name: keyof T, readonly?: (item: T) => boolean }[];
@Input() itemType: Type<T>;
@Input() itemType: T;
/**
* Beküldés előtt extra adat hozzáadása
*/
@ -32,7 +32,7 @@ export class EditComponent<T extends Model> 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<T extends Model> 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<T extends Model> 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) {

View file

@ -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<T extends { id: number }> implements OnInit {
export class ListComponent<T extends { id: number }, U extends { [entityName: string]: { count: number; list: T[] } }> implements OnInit {
@Input() apiPath: string;
@Input() itemType: T;
@Input() gql: Query<U, { limit: number; offset: number }>;
@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<T extends { id: number }> implements OnInit {
async getItems(limit: number, page: number): Promise<void> {
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<T>(this.apiPath, limit, page);
this.items = data[key].list;
} finally {
this.loading = false;
}
}
async editItem(item: T): Promise<void> {
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]);
}

View file

@ -1,4 +1,4 @@
<app-edit #edit [itemType]="itemType" apiPath="/courses" [beforeSubmit]="beforeSubmit" [fields]="[
<!-- <app-edit [beforeSubmit]="beforeSubmit" [fields]="[
{title: 'Félév', name: 'semester'},
{title: 'Név', name: 'alias'}
]"></app-edit>
]"></app-edit> -->

View file

@ -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) {

View file

@ -1,4 +1,4 @@
<app-list #list [apiPath]="'/subjects/'+subjectId+'/courses'" [itemType]="itemType" allowNew="true" [columns]="[
<app-list [itemType]="itemType" allowNew="true" [columns]="[
{prop: 'semester', title: 'Félév'},
{prop: 'alias', title: 'Név'}
]"></app-list>

View file

@ -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<Course>;
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
}
}

View file

@ -1,4 +1,4 @@
<app-edit [apiPath]="'/subjects'" [itemType]="itemType" [fields]="[
<app-edit [gql]="itemGQL" [itemType]="itemType" [fields]="[
{title: 'Név', name: 'name'},
{title: 'Leirás', name: 'description'}
]"></app-edit>

View file

@ -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 {
}

View file

@ -1,4 +1,4 @@
<app-list apiPath="/subjects" [itemType]="itemType" allowNew="true" [columns]="[
<app-list [gql]="listGQL" allowNew="true" [columns]="[
{title: 'Név', prop: 'name'},
{title: 'Leirás', prop: 'description'}
]" [customActions]="[

View file

@ -1,6 +1,6 @@
import { Component, OnInit } from '@angular/core';
import { Subject } from '../../model/subject.model';
import { ActivatedRoute, Router } from '@angular/router';
import { Subject, SubjectListGQL } from '../../services/graphql';
@Component({
selector: 'app-subject-list',
@ -8,9 +8,8 @@ import { ActivatedRoute, Router } from '@angular/router';
styleUrls: ['./subject-list.component.css']
})
export class SubjectListComponent implements OnInit {
itemType = Subject;
constructor(private route: ActivatedRoute, private router: Router) {
constructor(private route: ActivatedRoute, private router: Router, public listGQL: SubjectListGQL) {
}
ngOnInit(): void {

View file

@ -1,4 +1,4 @@
<app-edit [apiPath]="'/users'" [itemType]="itemType" [fields]="[
<app-edit [itemType]="itemType" [fields]="[
{title: 'E-mail', name: 'email'},
{title: 'Név', name: 'name'},
{title: 'Admin', name: 'isAdmin', readonly: isEditingSelf}

View file

@ -1,6 +1,6 @@
import { Component, OnInit } from '@angular/core';
import { User } from '../../model/user.model';
import { LoginService } from '../../auth/login.service';
import { UserResult } from '../../services/graphql';
@Component({
selector: 'app-user-edit',
@ -8,7 +8,7 @@ import { LoginService } from '../../auth/login.service';
styleUrls: ['./user-edit.component.css']
})
export class UserEditComponent implements OnInit {
itemType = User;
itemType: UserResult;
isEditingSelf = user => user.id === this.userService.user.id;
constructor(private userService: LoginService) { }