Commit 0dd28f68 authored by Andres Käver's avatar Andres Käver

dtos in front and back, error handling

parent 9fa4c5da
......@@ -7,6 +7,7 @@
<ItemGroup>
<ProjectReference Include="..\Contracts.DAL.Base\Contracts.DAL.Base.csproj" />
<ProjectReference Include="..\Domain\Domain.csproj" />
<ProjectReference Include="..\PublicApi.DTO.v1\PublicApi.DTO.v1.csproj" />
</ItemGroup>
</Project>
......@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Threading.Tasks;
using Contracts.DAL.Base.Repositories;
using Domain;
using PublicApi.DTO.v1;
namespace Contracts.DAL.App.Repositories
{
......@@ -14,5 +15,10 @@ namespace Contracts.DAL.App.Repositories
Task<bool> ExistsAsync(Guid id, Guid? userId = null);
Task DeleteAsync(Guid id, Guid? userId = null);
// DTO methods
Task<IEnumerable<OwnerDTO>> DTOAllAsync(Guid? userId = null);
Task<OwnerDTO> DTOFirstOrDefaultAsync(Guid id, Guid? userId = null);
}
}
\ No newline at end of file
......@@ -6,6 +6,7 @@ using Contracts.DAL.App.Repositories;
using DAL.Base.EF.Repositories;
using Domain;
using Microsoft.EntityFrameworkCore;
using PublicApi.DTO.v1;
namespace DAL.App.EF.Repositories
{
......@@ -51,5 +52,43 @@ namespace DAL.App.EF.Repositories
var owner = await FirstOrDefaultAsync(id, userId);
base.Remove(owner);
}
// we need to do it on database level, to avoid unnecessary queries to db
public async Task<IEnumerable<OwnerDTO>> DTOAllAsync(Guid? userId = null)
{
var query = RepoDbSet.AsQueryable();
if (userId != null)
{
query = query.Where(o => o.AppUserId == userId);
}
return await query
.Select(o => new OwnerDTO()
{
Id = o.Id,
FirstName = o.FirstName,
LastName = o.LastName,
AnimalCount = o.Animals!.Count
})
.ToListAsync();
}
public async Task<OwnerDTO> DTOFirstOrDefaultAsync(Guid id, Guid? userId = null)
{
var query = RepoDbSet.Where(a => a.Id == id).AsQueryable();
if (userId != null)
{
query = query.Where(a => a.AppUserId == userId);
}
var ownerDTO = await query.Select(o => new OwnerDTO()
{
Id = o.Id,
FirstName = o.FirstName,
LastName = o.LastName,
AnimalCount = o.Animals!.Count
}).FirstOrDefaultAsync();
return ownerDTO;
}
}
}
\ No newline at end of file
using System;
using System.ComponentModel.DataAnnotations;
namespace PublicApi.DTO.v1
{
public class OwnerEditDTO
{
public Guid Id { get; set; }
[MinLength(1)] [MaxLength(64)] public string FirstName { get; set; } = default!;
[MinLength(1)] [MaxLength(64)] public string LastName { get; set; } = default!;
}
}
\ No newline at end of file
......@@ -11,6 +11,7 @@ using Domain;
using Extensions;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using PublicApi.DTO.v1;
namespace WebApp.ApiControllers
{
......@@ -28,17 +29,18 @@ namespace WebApp.ApiControllers
// GET: api/Owners
[HttpGet]
public async Task<ActionResult<IEnumerable<Owner>>> GetOwners()
public async Task<ActionResult<IEnumerable<OwnerDTO>>> GetOwners()
{
var owners = await _uow.Owners.AllAsync(User.UserGuidId());
return Ok(owners);
var ownerDTOs = await _uow.Owners.DTOAllAsync(User.UserGuidId());
return Ok(ownerDTOs);
}
// GET: api/Owners/5
[HttpGet("{id}")]
public async Task<ActionResult<Owner>> GetOwner(Guid id)
public async Task<ActionResult<OwnerDTO>> GetOwner(Guid id)
{
var owner = await _uow.Owners.FirstOrDefaultAsync(id, User.UserGuidId());
var owner = await _uow.Owners.DTOFirstOrDefaultAsync(id, User.UserGuidId());
if (owner == null)
{
......@@ -50,14 +52,22 @@ namespace WebApp.ApiControllers
// PUT: api/Owners/5
[HttpPut("{id}")]
public async Task<IActionResult> PutOwner(Guid id, Owner owner)
public async Task<IActionResult> PutOwner(Guid id, OwnerEditDTO OwnerEditDTO)
{
if (id != owner.Id)
if (id != OwnerEditDTO.Id)
{
return BadRequest();
}
var owner = await _uow.Owners.FirstOrDefaultAsync(OwnerEditDTO.Id, User.UserGuidId());
if (owner == null)
{
return BadRequest();
}
owner.AppUserId = User.UserGuidId();
owner.FirstName = OwnerEditDTO.FirstName;
owner.LastName = OwnerEditDTO.LastName;
_uow.Owners.Update(owner);
......@@ -80,9 +90,15 @@ namespace WebApp.ApiControllers
// POST: api/Owners
[HttpPost]
public async Task<ActionResult<Owner>> PostOwner(Owner owner)
public async Task<ActionResult<Owner>> PostOwner(OwnerCreateDTO ownerCreateDTO)
{
owner.AppUserId = User.UserGuidId();
var owner = new Owner
{
AppUserId = User.UserGuidId(),
FirstName = ownerCreateDTO.FirstName,
LastName = ownerCreateDTO.LastName
};
_uow.Owners.Add(owner);
await _uow.SaveChangesAsync();
......
......@@ -23,9 +23,9 @@ export class App {
{ route: ['owners', 'owners/index'], name: 'owners-index', moduleId: PLATFORM.moduleName('views/owners/index'), nav: true, title: 'Owners' },
{ route: ['owners/details/:id'], name: 'owners-details', moduleId: PLATFORM.moduleName('views/owners/details'), nav: false, title: 'Owners Details' },
{ route: ['owners/edit/:id'], name: 'owners-edit', moduleId: PLATFORM.moduleName('views/owners/edit'), nav: false, title: 'Owners Edit' },
{ route: ['owners/delete/:id'], name: 'owners-delete', moduleId: PLATFORM.moduleName('views/owners/delete'), nav: false, title: 'Owners Delete' },
{ route: ['owners/details/:id?'], name: 'owners-details', moduleId: PLATFORM.moduleName('views/owners/details'), nav: false, title: 'Owners Details' },
{ route: ['owners/edit/:id?'], name: 'owners-edit', moduleId: PLATFORM.moduleName('views/owners/edit'), nav: false, title: 'Owners Edit' },
{ route: ['owners/delete/:id?'], name: 'owners-delete', moduleId: PLATFORM.moduleName('views/owners/delete'), nav: false, title: 'Owners Delete' },
{ route: ['owners/create'], name: 'owners-create', moduleId: PLATFORM.moduleName('views/owners/create'), nav: false, title: 'Owners Create' },
{ route: ['animals', 'animals/index'], name: 'animals-index', moduleId: PLATFORM.moduleName('views/animals/index'), nav: true, title: 'Animals' },
......
<template>
<div show.bind="alertData" class="alert alert-${alertData.type} alert-dismissible fade show" role="alert">
${alertData.message}
<button show.bind="alertData.dismissable" type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
</template>
import { IAlertData } from './../types/IAlertData';
import { autoinject, bindable } from 'aurelia-framework';
@autoinject
export class Alert {
@bindable public alertData: IAlertData | null = null;
constructor() {
}
}
import { StringifyOptions } from "querystring";
export interface IOwner {
id: string;
firstName: string;
......
export interface IOwnerCreate {
firstName: string;
lastName: string;
}
export interface IOwnerEdit {
id: string;
firstName: string;
lastName: string;
}
import { autoinject } from 'aurelia-framework';
import { AppState } from 'state/app-state';
import { HttpClient, json } from 'aurelia-fetch-client';
import { IResponse } from 'domain/IResponse';
import { IFetchResponse } from 'types/IFetchResponse';
import { ILoginResponse } from 'domain/ILoginResponse';
@autoinject
......@@ -12,7 +12,7 @@ export class AccountService {
this.httpClient.baseUrl = this.appState.baseUrl;
}
async login(email: string, password: string): Promise<IResponse<ILoginResponse>> {
async login(email: string, password: string): Promise<IFetchResponse<ILoginResponse>> {
try {
const response = await this.httpClient.post('account/login', JSON.stringify({
email: email,
......@@ -26,7 +26,7 @@ export class AccountService {
const data = (await response.json()) as ILoginResponse;
return {
statusCode: response.status,
data: data as ILoginResponse
data: data
}
}
......
import { autoinject } from 'aurelia-framework';
import { HttpClient } from 'aurelia-fetch-client';
import { IOwner } from 'domain/IOwner';
import { AppState } from 'state/app-state';
import { IFetchResponse } from 'types/IFetchResponse';
import { IOwner } from 'domain/IOwner';
import { IOwnerEdit } from 'domain/IOwnerEdit';
import { IOwnerCreate } from 'domain/IOwnerCreate';
@autoinject
export class OwnerService {
......@@ -11,75 +14,156 @@ export class OwnerService {
private readonly _baseUrl = 'Owners';
getOwners(): Promise<IOwner[]> {
let x: RequestInit;
return this.httpClient
.fetch(this._baseUrl, {
cache: "no-store", headers: {
authorization: "Bearer " + this.appState.jwt
async getOwners(): Promise<IFetchResponse<IOwner[]>> {
try {
const response = await this.httpClient
.fetch(this._baseUrl, {
cache: "no-store",
headers: {
authorization: "Bearer " + this.appState.jwt
}
});
// happy case
if (response.status >= 200 && response.status < 300) {
const data = (await response.json()) as IOwner[];
return {
statusCode: response.status,
data: data
}
})
.then(response => response.json())
.then((data: IOwner[]) => data)
.catch(reason => {
console.error(reason);
return [];
});
}
// something went wrong
return {
statusCode: response.status,
errorMessage: response.statusText
}
} catch (reason) {
return {
statusCode: 0,
errorMessage: JSON.stringify(reason)
}
}
}
getOwner(id: string): Promise<IOwner | null> {
return this.httpClient
.fetch(this._baseUrl + '/' + id, { cache: "no-store" })
.then(response => response.json())
.then((data: IOwner) => data)
.catch(reason => {
console.error(reason);
return null;
});
async getOwner(id: string): Promise<IFetchResponse<IOwner>> {
try {
const response = await this.httpClient
.fetch(this._baseUrl + '/' + id, {
cache: "no-store",
headers: {
authorization: "Bearer " + this.appState.jwt
}
});
if (response.status >= 200 && response.status < 300) {
const data = (await response.json()) as IOwner;
return {
statusCode: response.status,
data: data
}
}
return {
statusCode: response.status,
errorMessage: response.statusText
}
} catch (reason) {
return {
statusCode: 0,
errorMessage: JSON.stringify(reason)
}
}
}
createOwner(owner: IOwner): Promise<string> {
return this.httpClient.post(this._baseUrl, JSON.stringify(owner), {
cache: 'no-store'
}).then(
response => {
console.log('createOwner response', response);
return response.statusText;
async createOwner(owner: IOwnerCreate): Promise<IFetchResponse<string>> {
try {
const response = await this.httpClient
.post(this._baseUrl, JSON.stringify(owner), {
cache: 'no-store',
headers: {
authorization: "Bearer " + this.appState.jwt
}
})
if (response.status >= 200 && response.status < 300) {
return {
statusCode: response.status
// no data
}
}
return {
statusCode: response.status,
errorMessage: response.statusText
}
}
catch (reason) {
return {
statusCode: 0,
errorMessage: JSON.stringify(reason)
}
).catch(reason => {
console.error(reason);
return JSON.stringify(reason);
});
}
}
updateOwner(owner: IOwner): Promise<string> {
return this.httpClient.put(this._baseUrl + '/' + owner.id, JSON.stringify(owner), {
cache: 'no-store'
}).then(
response => {
console.log('updateOwner response', response);
return response.statusText;
async updateOwner(owner: IOwnerEdit): Promise<IFetchResponse<string>> {
try {
const response = await this.httpClient
.put(this._baseUrl + '/' + owner.id, JSON.stringify(owner), {
cache: 'no-store',
headers: {
authorization: "Bearer " + this.appState.jwt
}
});
if (response.status >= 200 && response.status < 300) {
return {
statusCode: response.status
// no data
}
}
return {
statusCode: response.status,
errorMessage: response.statusText
}
}
catch (reason) {
return {
statusCode: 0,
errorMessage: JSON.stringify(reason)
}
).catch(reason => {
console.error(reason);
return JSON.stringify(reason);
});
}
}
deleteOwner(owner: IOwner): Promise<string> {
return this.httpClient.delete(this._baseUrl + '/' + owner.id, JSON.stringify(owner), {
cache: 'no-store'
}).then(
response => {
console.log('deleteOwner response', response);
return response.statusText;
async deleteOwner(id: string): Promise<IFetchResponse<string>> {
try {
const response = await this.httpClient
.delete(this._baseUrl + '/' + id, null, {
cache: 'no-store',
headers: {
authorization: "Bearer " + this.appState.jwt
}
});
if (response.status >= 200 && response.status < 300) {
return {
statusCode: response.status
// no data
}
}
return {
statusCode: response.status,
errorMessage: response.statusText
}
}
catch (reason) {
return {
statusCode: 0,
errorMessage: JSON.stringify(reason)
}
).catch(reason => {
console.error(reason);
return JSON.stringify(reason);
});
}
}
}
......@@ -6,7 +6,18 @@ export class AppState {
// JavaScript Object Notation Web Token
// to keep track of logged in status
public jwt: string | null = null;
// https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage
get jwt():string | null {
return localStorage.getItem('jwt');
}
set jwt(value: string | null){
if (value){
localStorage.setItem('jwt', value);
} else {
localStorage.removeItem('jwt');
}
}
}
export enum AlertType {
Primary = 'primary',
Secondary = 'secondary',
Success = 'success',
Danger = 'danger',
Warning = 'warning',
Info = 'info',
Light = 'light',
Dark = 'dark',
}
import { AlertType } from "./AlertType";
export interface IAlertData {
message: string;
dismissable?: boolean;
type: AlertType
}
export interface IResponse<TData> {
export interface IFetchResponse<TData> {
statusCode: number;
errorMessage?: string; // can be undefined
data?: TData
......
......@@ -17,6 +17,8 @@
<label for="Input_Password">Password</label>
<input class="form-control" type="password" value.bind="_password" />
</div>
<!--
<div class="form-group">
<div class="checkbox">
<label for="Input_RememberMe">
......@@ -26,6 +28,8 @@
</label>
</div>
</div>
-->
<div class="form-group">
<button type="submit" class="btn btn-primary">Log in</button>
</div>
......
<template>
<require from="../../components/alert"></require>
<alert alert-data.bind="_alert"></alert>
<h1>Create</h1>
<h4>Owner</h4>
<hr />
......
......@@ -2,9 +2,13 @@ import { autoinject } from 'aurelia-framework';
import { RouteConfig, NavigationInstruction, Router } from 'aurelia-router';
import { OwnerService } from 'service/owner-service';
import { IOwner } from 'domain/IOwner';
import { IAlertData } from 'types/IAlertData';
import { AlertType } from 'types/AlertType';
@autoinject
export class OwnersCreate {
private _alert: IAlertData | null = null;
_firstName = "";
_lastName = ""
......@@ -24,11 +28,22 @@ export class OwnersCreate {
onSubmit(event: Event) {
console.log(event);
this.ownerService
.createOwner({ firstName: this._firstName, lastName: this._lastName, animalCount: 0, id: '' })
.then((resp) => {
console.log('redirect?', resp);
this.router.navigateToRoute('owners-index', {});
});
.createOwner({ firstName: this._firstName, lastName: this._lastName })
.then(
response => {
if (response.statusCode >= 200 && response.statusCode < 300) {
this._alert = null;
this.router.navigateToRoute('owners-index', {});
} else {
// show error message
this._alert = {
message: response.statusCode.toString() + ' - ' + response.errorMessage,
type: AlertType.Danger,
dismissable: true,
}
}
}
);
event.preventDefault();
}
......
<template>
<require from="../../components/alert"></require>
<alert alert-data.bind="_alert"></alert>
<h1>Delete</h1>
......
......@@ -2,10 +2,14 @@ import { autoinject } from 'aurelia-framework';
import { RouteConfig, NavigationInstruction, Router } from 'aurelia-router';
import { OwnerService } from 'service/owner-service';
import { IOwner } from 'domain/IOwner';
import { IAlertData } from 'types/IAlertData';
import { AlertType } from 'types/AlertType';
@autoinject
export class OwnersDelete {
private _owner: IOwner | null = null;
private _alert: IAlertData | null = null;
private _owner?: IOwner;
constructor(private ownerService: OwnerService, private router: Router) {
......@@ -19,18 +23,42 @@ export class OwnersDelete {
console.log(params);
if (params.id && typeof (params.id) == 'string') {
this.ownerService.getOwner(params.id).then(
data => this._owner = data
response => {
if (response.statusCode >= 200 && response.statusCode < 300) {
this._alert = null;
this._owner = response.data!;
} else {
// show error message
this._alert = {
message: response.statusCode.toString() + ' - ' + response.errorMessage,
type: AlertType.Danger,
dismissable: true,
};
this._owner = undefined;
}
}
);
}
}
onSubmit(event: Event) {
this.ownerService
.deleteOwner(this._owner!)
.then((resp) => {
console.log('redirect?', resp);
this.router.navigateToRoute('owners-index', {});
});
.deleteOwner(this._owner!.id)
.then(
response => {
if (response.statusCode >= 200 && response.statusCode < 300) {
this._alert = null;