Compare commits

...

5 Commits

Author SHA1 Message Date
321ffc3b7c Test querying all consumption entries and cars
Some checks failed
continuous-integration/drone/push Build is failing
2025-06-16 21:05:48 +02:00
0fa5b080d8 Add API models and clients manually 2025-06-16 21:05:07 +02:00
85052df8a5 Add descriptions for endpoints for use in openapi 2025-06-16 20:34:09 +02:00
bcbf76fda6 Specify API returns types for swagger 2025-06-16 20:28:37 +02:00
b989c43ec3 Revert to using manually created api classes 2025-06-16 19:54:45 +02:00
24 changed files with 183 additions and 31 deletions

View File

@@ -113,5 +113,8 @@
} }
} }
} }
},
"cli": {
"analytics": false
} }
} }

View File

@@ -0,0 +1,6 @@
import { InjectionToken } from "@angular/core";
/**
* The base path for all API requests, e.g. when using a proxy on the origin's address.
*/
export const API_BASE_PATH = new InjectionToken<string>('API_BASE_PATH');

View File

@@ -0,0 +1,35 @@
import {inject, Injectable} from "@angular/core";
import {HttpClient} from '@angular/common/http';
import {map, Observable} from 'rxjs';
import {API_BASE_PATH} from '../api-base-path';
@Injectable({
providedIn: 'root',
})
export class CarClient {
private readonly http = inject(HttpClient);
private readonly apiBasePath = inject(API_BASE_PATH, {optional: true});
getAll(): Observable<GetCarsResponse> {
return this.http.get<GetCarsResponse>(`${this.apiBasePath}/v1/cars`);
}
getSingle(id: string): Observable<Car> {
return this.http.get<Car>(`${this.apiBasePath}/v1/cars/${id}`);
}
create(request: CreateCarRequest): Observable<Car> {
return this.http.post<Car>(`${this.apiBasePath}/v1/cars`, request);
}
update(request: UpdateCarRequest): Observable<Car> {
return this.http.put<Car>(`${this.apiBasePath}/v1/cars`, request);
}
delete(id: string): Observable<void> {
return this.http.delete(`${this.apiBasePath}/v1/cars/${id}`)
.pipe(
map(_ => undefined)
);
}
}

View File

@@ -0,0 +1,4 @@
interface Car {
id: string;
name: string;
}

View File

@@ -0,0 +1,3 @@
interface CreateCarRequest {
name: string;
}

View File

@@ -0,0 +1,3 @@
interface GetCarsResponse {
cars: Car[];
}

View File

@@ -0,0 +1,3 @@
interface UpdateCarRequest {
name: string;
}

View File

@@ -0,0 +1,35 @@
import {inject, Injectable} from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {API_BASE_PATH} from '../api-base-path';
import {map, Observable} from 'rxjs';
@Injectable({
providedIn: 'root',
})
export class ConsumptionClient {
private readonly http = inject(HttpClient);
private readonly apiBasePath = inject(API_BASE_PATH, {optional: true});
getAll(): Observable<GetConsumptionEntriesResponse> {
return this.http.get<GetConsumptionEntriesResponse>(`${this.apiBasePath}/v1/consumptions`);
}
getSingle(id: string): Observable<ConsumptionEntry> {
return this.http.get<ConsumptionEntry>(`${this.apiBasePath}/v1/consumptions/${id}`);
}
create(request: CreateCarRequest): Observable<ConsumptionEntry> {
return this.http.post<ConsumptionEntry>(`${this.apiBasePath}/v1/consumptions`, request);
}
update(request: UpdateCarRequest): Observable<ConsumptionEntry> {
return this.http.put<ConsumptionEntry>(`${this.apiBasePath}/v1/consumptions`, request);
}
delete(id: string): Observable<void> {
return this.http.delete(`${this.apiBasePath}/v1/consumptions/${id}`)
.pipe(
map(_ => undefined),
);
}
}

View File

@@ -0,0 +1,7 @@
interface CreateConsumptionEntry {
dateTime: string;
distance: number;
amount: number;
ignoreInCalculation: boolean;
carId: string;
}

View File

@@ -0,0 +1,7 @@
interface UpdateConsumptionEntry {
dateTime: string;
distance: number;
amount: number;
ignoreInCalculation: boolean;
carId: string;
}

View File

@@ -7,6 +7,7 @@ import { includeBearerTokenInterceptor } from 'keycloak-angular';
import { providePrimeNG } from 'primeng/config'; import { providePrimeNG } from 'primeng/config';
import { routes } from './app.routes'; import { routes } from './app.routes';
import { provideKeycloakAngular } from './auth/auth.config'; import { provideKeycloakAngular } from './auth/auth.config';
import {API_BASE_PATH} from './api/api-base-path';
export const appConfig: ApplicationConfig = { export const appConfig: ApplicationConfig = {
providers: [ providers: [
@@ -21,5 +22,9 @@ export const appConfig: ApplicationConfig = {
}, },
ripple: true ripple: true
}), }),
{
provide: API_BASE_PATH,
useValue: '/api'
}
] ]
}; };

View File

@@ -1,23 +1,31 @@
import { CommonModule } from '@angular/common'; import {AsyncPipe, CommonModule} from '@angular/common';
import { Component, inject } from '@angular/core'; import { Component, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { ReactiveFormsModule } from '@angular/forms';
import { RouterLink } from '@angular/router'; import { RouterLink } from '@angular/router';
import { MessageService } from 'primeng/api';
import { ButtonModule } from 'primeng/button'; import { ButtonModule } from 'primeng/button';
import { DataViewModule } from 'primeng/dataview'; import { DataViewModule } from 'primeng/dataview';
import { ScrollTopModule } from 'primeng/scrolltop';
import { SelectModule } from 'primeng/select'; import { SelectModule } from 'primeng/select';
import { ScrollTopModule } from 'primeng/scrolltop';
import { SkeletonModule } from 'primeng/skeleton'; import { SkeletonModule } from 'primeng/skeleton';
import { import {
BehaviorSubject,
combineLatest,
map, map,
Observable, Observable,
tap of,
startWith,
tap,
} from 'rxjs'; } from 'rxjs';
import { Client, GetConsumptions_ResponseDto } from '../../../shared/api/swagger.generated'; import { HttpClient } from '@angular/common/http';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import {CarClient} from '../../../api/cars/car-client';
import {ConsumptionClient} from '../../../api/consumptions/consumption-client';
@Component({ @Component({
selector: 'app-entries', selector: 'app-entries',
imports: [ imports: [
AsyncPipe,
ButtonModule, ButtonModule,
CommonModule, CommonModule,
DataViewModule, DataViewModule,
@@ -31,24 +39,29 @@ import { Client, GetConsumptions_ResponseDto } from '../../../shared/api/swagger
styleUrl: './entries.component.scss' styleUrl: './entries.component.scss'
}) })
export class EntriesComponent { export class EntriesComponent {
private readonly client = inject(Client); private readonly consumptionClient = inject(ConsumptionClient);
private readonly carClient = inject(CarClient);
protected readonly consumptionEntries$: Observable<GetConsumptions_ResponseDto[] | undefined>; protected readonly consumptionEntries$: Observable<ConsumptionEntry[]>;
protected readonly cars$: Observable<Car[]>;
protected readonly rowsPerPageDefaultOption = 25;
protected readonly rowsPerPageOptions = [10, 25, 50, 100];
protected readonly currentPageReportTemplate = '{currentPage} / {totalPages}';
protected readonly skeletonsIterationSource = Array(10).fill(0);
constructor() { constructor() {
this.consumptionEntries$ = this.client.consumptionsGET() this.consumptionEntries$ = this.consumptionClient.getAll()
.pipe( .pipe(
takeUntilDestroyed(), takeUntilDestroyed(),
tap((response) => { tap((response) => {
console.log('Entries response:', response); console.log('Entries response:', response);
}), }),
map((response) => response.consumptions) map(response => response.consumptions)
) );
this.cars$ = this.carClient.getAll()
.pipe(
takeUntilDestroyed(),
tap((response) => {
console.log('Cars response:', response);
}),
map(response => response.cars)
);
} }
} }

View File

@@ -16,7 +16,10 @@ public static class CreateCar
{ {
return builder return builder
.MapPost("cars", Endpoint) .MapPost("cars", Endpoint)
.WithTags("Cars"); .WithTags("Cars")
.WithDescription("Creates a new car")
.Produces<Response>(201)
.ProducesValidationProblem();
} }
public class Validator : AbstractValidator<Request> public class Validator : AbstractValidator<Request>

View File

@@ -8,7 +8,10 @@ public static class DeleteCar
{ {
return builder return builder
.MapDelete("cars/{id:guid}", Endpoint) .MapDelete("cars/{id:guid}", Endpoint)
.WithTags("Cars"); .WithTags("Cars")
.WithDescription("Deletes a car by ID")
.Produces(204)
.Produces(404);
} }
public static async Task<IResult> Endpoint( public static async Task<IResult> Endpoint(

View File

@@ -1,4 +1,5 @@
using Vegasco.Server.Api.Persistence; using Microsoft.AspNetCore.Http.HttpResults;
using Vegasco.Server.Api.Persistence;
namespace Vegasco.Server.Api.Cars; namespace Vegasco.Server.Api.Cars;
@@ -10,7 +11,10 @@ public static class GetCar
{ {
return builder return builder
.MapGet("cars/{id:guid}", Endpoint) .MapGet("cars/{id:guid}", Endpoint)
.WithTags("Cars"); .WithDescription("Returns a single car by ID")
.WithTags("Cars")
.Produces<Response>()
.Produces(404);
} }
private static async Task<IResult> Endpoint( private static async Task<IResult> Endpoint(

View File

@@ -25,10 +25,11 @@ public static class GetCars
return builder return builder
.MapGet("cars", Endpoint) .MapGet("cars", Endpoint)
.WithDescription("Returns all cars") .WithDescription("Returns all cars")
.WithTags("Cars"); .WithTags("Cars")
.Produces<ApiResponse>();
} }
private static async Task<Ok<ApiResponse>> Endpoint( private static async Task<IResult> Endpoint(
[AsParameters] Request request, [AsParameters] Request request,
ApplicationDbContext dbContext, ApplicationDbContext dbContext,
CancellationToken cancellationToken) CancellationToken cancellationToken)

View File

@@ -15,7 +15,11 @@ public static class UpdateCar
{ {
return builder return builder
.MapPut("cars/{id:guid}", Endpoint) .MapPut("cars/{id:guid}", Endpoint)
.WithTags("Cars"); .WithTags("Cars")
.WithDescription("Updates a car by ID")
.Produces<Response>()
.ProducesValidationProblem()
.Produces(404);
} }
public class Validator : AbstractValidator<Request> public class Validator : AbstractValidator<Request>

View File

@@ -16,7 +16,9 @@ public static class CreateConsumption
{ {
return builder return builder
.MapPost("consumptions", Endpoint) .MapPost("consumptions", Endpoint)
.WithTags("Consumptions"); .WithTags("Consumptions")
.WithDescription("Creates a new consumption entry")
.Produces<Response>(201);
} }
public class Validator : AbstractValidator<Request> public class Validator : AbstractValidator<Request>

View File

@@ -8,7 +8,10 @@ public static class DeleteConsumption
{ {
return builder return builder
.MapDelete("consumptions/{id:guid}", Endpoint) .MapDelete("consumptions/{id:guid}", Endpoint)
.WithTags("Consumptions"); .WithTags("Consumptions")
.WithDescription("Deletes a consumption entry by ID")
.Produces(204)
.Produces(404);
} }
private static async Task<IResult> Endpoint( private static async Task<IResult> Endpoint(

View File

@@ -10,7 +10,10 @@ public static class GetConsumption
{ {
return builder return builder
.MapGet("consumptions/{id:guid}", Endpoint) .MapGet("consumptions/{id:guid}", Endpoint)
.WithTags("Consumptions"); .WithTags("Consumptions")
.WithDescription("Returns a single consumption entry by ID")
.Produces<Response>()
.Produces(404);
} }
private static async Task<IResult> Endpoint( private static async Task<IResult> Endpoint(

View File

@@ -31,7 +31,8 @@ public static class GetConsumptions
return builder return builder
.MapGet("consumptions", Endpoint) .MapGet("consumptions", Endpoint)
.WithDescription("Returns all consumption entries") .WithDescription("Returns all consumption entries")
.WithTags("Consumptions"); .WithTags("Consumptions")
.Produces<ApiResponse>();
} }
private static async Task<Ok<ApiResponse>> Endpoint( private static async Task<Ok<ApiResponse>> Endpoint(
@@ -44,7 +45,7 @@ public static class GetConsumptions
new ResponseDto(x.Id.Value, x.DateTime, x.Distance, x.Amount, x.IgnoreInCalculation, x.CarId.Value)) new ResponseDto(x.Id.Value, x.DateTime, x.Distance, x.Amount, x.IgnoreInCalculation, x.CarId.Value))
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
var apiResponse = new ApiResponse ApiResponse apiResponse = new()
{ {
Consumptions = consumptions Consumptions = consumptions
}; };

View File

@@ -15,7 +15,11 @@ public static class UpdateConsumption
{ {
return builder return builder
.MapPut("consumptions/{id:guid}", Endpoint) .MapPut("consumptions/{id:guid}", Endpoint)
.WithTags("Consumptions"); .WithTags("Consumptions")
.WithDescription("Updates a consumption entry by ID")
.Produces<Response>()
.ProducesValidationProblem()
.Produces(404);
} }
public class Validator : AbstractValidator<Request> public class Validator : AbstractValidator<Request>