Add support for etag and not modified resources

This commit is contained in:
Matteo Gheza 2023-10-23 08:08:10 +02:00
parent 14183e390c
commit 235a70160b
10 changed files with 120 additions and 22 deletions

View File

@ -16,10 +16,20 @@ class AlertController extends Controller
public function index()
{
return response()->json(
Alert::with('crew.user')
->where('closed', false)
->orderBy('created_at', 'desc')
->get()
request()->query('full', false) ?
Alert::with(['crew.user' => [
"name",
"username",
"chief",
"driver"
]])
->where('closed', false)
->orderBy('created_at', 'desc')
->get()
:
Alert::where('closed', false)
->orderBy('created_at', 'desc')
->get()
);
}
@ -113,10 +123,15 @@ class AlertController extends Controller
*/
public function show(Request $request, $id)
{
User::where('id', $request->user()->id)->update(['last_access' => now()]);
return response()->json(
Alert::with('crew.user')->find($id)
Alert::with(['crew.user' => [
"name",
"username",
"chief",
"driver"
]])
->where('id', $id)
->first()
);
}

View File

@ -12,6 +12,7 @@
"laravel/framework": "^10.0",
"laravel/sanctum": "^3.2",
"laravel/tinker": "^2.8",
"matthewbdaly/laravel-etag-middleware": "^1.3",
"santigarcor/laratrust": "^7.2",
"sentry/sentry-laravel": "^3.7"
},

57
backend/composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "f099d4ac47fd2d3bbc07fe77412bb9bd",
"content-hash": "f8b683b55dd63e861103c15d13fed172",
"packages": [
{
"name": "brick/math",
@ -2202,6 +2202,61 @@
],
"time": "2023-08-05T12:09:49+00:00"
},
{
"name": "matthewbdaly/laravel-etag-middleware",
"version": "1.3.6",
"source": {
"type": "git",
"url": "https://github.com/matthewbdaly/laravel-etag-middleware.git",
"reference": "048cb22f5849f695f134e7e13cb52b4c21ef6fd4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/matthewbdaly/laravel-etag-middleware/zipball/048cb22f5849f695f134e7e13cb52b4c21ef6fd4",
"reference": "048cb22f5849f695f134e7e13cb52b4c21ef6fd4",
"shasum": ""
},
"require": {
"illuminate/http": "^5.5|^6.0|^7.0|^8.0|^9.0|^10.0",
"illuminate/support": "^5.5|^6.0|^7.0|^8.0|^9.0|^10.0"
},
"require-dev": {
"mockery/mockery": "^1.0",
"php-coveralls/php-coveralls": "^2.1",
"phpunit/phpunit": "^4.8|^5.2|^9.0",
"squizlabs/php_codesniffer": "^3.7",
"vimeo/psalm": "^5.8"
},
"type": "library",
"autoload": {
"psr-4": {
"Matthewbdaly\\ETagMiddleware\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Matthew Daly",
"email": "450801+matthewbdaly@users.noreply.github.com"
}
],
"description": "A Laravel middleware for adding ETags to HTTP requests to improve response times",
"keywords": [
"ETags",
"Etag",
"http",
"laravel",
"middleware"
],
"support": {
"issues": "https://github.com/matthewbdaly/laravel-etag-middleware/issues",
"source": "https://github.com/matthewbdaly/laravel-etag-middleware/tree/1.3.6"
},
"time": "2023-03-20T16:12:09+00:00"
},
{
"name": "monolog/monolog",
"version": "3.4.0",

View File

@ -14,6 +14,7 @@ use App\Http\Controllers\ServiceTypeController;
use App\Http\Controllers\TrainingController;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Artisan;
use \Matthewbdaly\ETagMiddleware\ETag;
/*
|--------------------------------------------------------------------------
@ -44,16 +45,16 @@ Route::middleware('auth:sanctum')->group( function () {
Route::get('/schedules', [ScheduleSlotsController::class, 'index']);
Route::post('/schedules', [ScheduleSlotsController::class, 'store']);
Route::get('/availability', [AvailabilityController::class, 'get']);
Route::get('/availability', [AvailabilityController::class, 'get'])->middleware(ETag::class);
Route::post('/availability', [AvailabilityController::class, 'updateAvailability']);
Route::post('/manual_mode', [AvailabilityController::class, 'updateAvailabilityManualMode']);
Route::get('/alerts', [AlertController::class, 'index']);
Route::get('/alerts', [AlertController::class, 'index'])->middleware(ETag::class);
Route::post('/alerts', [AlertController::class, 'store']);
Route::get('/alerts/{id}', [AlertController::class, 'show']);
Route::get('/alerts/{id}', [AlertController::class, 'show'])->middleware(ETag::class);
Route::patch('/alerts/{id}', [AlertController::class, 'update']);
Route::get('/services', [ServiceController::class, 'index']);
Route::get('/services', [ServiceController::class, 'index'])->middleware(ETag::class);
Route::post('/services', [ServiceController::class, 'createOrUpdate']);
Route::get('/services/{id}', [ServiceController::class, 'show']);
Route::delete('/services/{id}', [ServiceController::class, 'destroy']);
@ -63,12 +64,12 @@ Route::middleware('auth:sanctum')->group( function () {
Route::get('/places/search', [PlacesController::class, 'search']);
Route::get('/places/{id}', [PlacesController::class, 'show']);
Route::get('/trainings', [TrainingController::class, 'index']);
Route::get('/trainings', [TrainingController::class, 'index'])->middleware(ETag::class);
Route::post('/trainings', [TrainingController::class, 'createOrUpdate']);
Route::get('/trainings/{id}', [TrainingController::class, 'show']);
Route::delete('/trainings/{id}', [TrainingController::class, 'destroy']);
Route::get('/logs', [LogsController::class, 'index']);
Route::get('/logs', [LogsController::class, 'index'])->middleware(ETag::class);
Route::post('/telegram_login_token', [TelegramController::class, 'loginToken']);

View File

@ -59,7 +59,8 @@
"scripts": [],
"serviceWorker": true,
"ngswConfigPath": "ngsw-config.json",
"sourceMap": true
"sourceMap": true,
"allowedCommonJsDependencies": ["crypto-js"]
},
"configurations": {
"production": {

View File

@ -26,6 +26,8 @@ export class ModalAlertComponent implements OnInit, OnDestroy {
alertClosed = 0;
private etag = "";
constructor(
public bsModalRef: BsModalRef,
private api: ApiClientService,
@ -36,7 +38,9 @@ export class ModalAlertComponent implements OnInit, OnDestroy {
loadResponsesData() {
//TODO: do not update data if not changed. Support for content hash in response header?
this.api.get(`alerts/${this.id}`).then((response) => {
this.api.get(`alerts/${this.id}`, {}, this.etag).then((response) => {
if(this.api.isLastSame) return;
this.etag = this.api.lastEtag;
console.log(response, this.alertClosed, response.closed);
if(this.alertClosed !== response.closed) this.alertClosed = response.closed;
if(!isEqual(this.crewUsers, response.crew)) this.crewUsers = response.crew;

View File

@ -60,6 +60,7 @@ export class TableComponent implements OnInit, OnDestroy {
public data: any = [];
public displayedData: any = [];
public originalData: any = [];
private etag: string = "";
public loadDataInterval: NodeJS.Timer | undefined = undefined;
@ -87,7 +88,9 @@ export class TableComponent implements OnInit, OnDestroy {
loadTableData() {
if(!this.sourceType) this.sourceType = "list";
this.api.get(this.sourceType).then((data: any) => {
this.api.get(this.sourceType, {}, this.etag).then((data: any) => {
if(this.api.isLastSame) return;
this.etag = this.api.lastEtag;
this.data = data.filter((row: any) => typeof row.hidden !== 'undefined' ? !row.hidden : true);
this.originalData = this.data;
this.totalElements = this.data.length;

View File

@ -25,6 +25,8 @@ export class ListComponent implements OnInit, OnDestroy {
public alertLoading = false;
private etag = "";
constructor(
public api: ApiClientService,
public auth: AuthService,
@ -35,7 +37,9 @@ export class ListComponent implements OnInit, OnDestroy {
}
loadAvailability() {
this.api.get("availability").then((response) => {
this.api.get("availability", {}, this.etag).then((response) => {
if(this.api.isLastSame) return;
this.etag = this.api.lastEtag;
this.available = response.available;
this.manual_mode = response.manual_mode;
}).catch((err) => {

View File

@ -8,6 +8,9 @@ import { Subject } from "rxjs";
export class ApiClientService {
private apiRoot = 'api/';
public lastEtag = "";
public isLastSame = false;
public alertsChanged = new Subject<void>();
public availableUsers: undefined | number = undefined;
@ -20,12 +23,20 @@ export class ApiClientService {
return this.apiRoot + endpoint;
}
public get(endpoint: string, data: any = {}) {
public get(endpoint: string, data: any = {}, etag: string = "") {
return new Promise<any>((resolve, reject) => {
this.http.get(this.apiEndpoint(endpoint), {
params: new HttpParams({ fromObject: data })
params: new HttpParams({ fromObject: data }),
observe: 'response',
headers: (etag !== "" && etag !== null) ? {
'If-None-Match': etag
} : {}
}).subscribe({
next: (v) => resolve(v),
next: (v: any) => {
this.lastEtag = v.headers.get("etag");
this.isLastSame = etag === this.lastEtag;
resolve(v.body);
},
error: (e) => reject(e)
});
});

View File

@ -21,6 +21,7 @@ export class AppComponent {
public loadingRoute = false;
private loadAlertsInterval: NodeJS.Timer | undefined = undefined;
public alerts = [];
private alertsEtag = "";
constructor(
public auth: AuthService,
@ -37,8 +38,10 @@ export class AppComponent {
loadAlerts() {
if(this.auth.profile) {
this.api.get("alerts").then((response) => {
this.api.get("alerts", {}, this.alertsEtag).then((response) => {
if(this.api.isLastSame) return;
this.alerts = response;
this.alertsEtag = this.api.lastEtag;
});
}
}