@fleetbase/fleetops-engine 0.6.63 → 0.6.64
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/RELEASE.md +4 -5
- package/composer.json +1 -1
- package/extension.json +1 -1
- package/package.json +1 -1
- package/server/src/Http/Resources/v1/Concerns/ResolvesPublicRelationFields.php +29 -3
- package/server/src/Http/Resources/v1/Fleet.php +3 -1
- package/server/tests/Feature/Http/Api/VehiclePublicContractTest.php +23 -0
- package/server/tests/Unit/Http/Resources/FleetResourceTest.php +17 -0
- package/server/tests/VehicleResourceContractsTest.php +39 -0
package/RELEASE.md
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
> v0.6.
|
|
1
|
+
> v0.6.64 ~ "Order responses no longer fail when a vehicle is assigned"
|
|
2
2
|
|
|
3
3
|
---
|
|
4
4
|
## Bug Fixes
|
|
5
|
-
- **
|
|
6
|
-
- **Fleet pages load hierarchy relationships correctly.** Internal Fleet-Ops requests now use the Fleet model's Eloquent relationship names, preventing the `RelationNotFoundException` raised when opening the Fleets route.
|
|
5
|
+
- **Orders with an assigned vehicle serialize again when expansions are requested.** The Vehicle and Fleet resources read the request's `with` list and loaded it onto the model they wrap. That is correct on their own endpoints, where the controller has already allowlisted the list, but both resources are also rendered nested inside Order and Driver responses. There the same request carries the parent's expansions, so `GET /v1/orders?with=payload` on an order with a vehicle assigned failed during serialisation with `RelationNotFoundException: Call to undefined relationship [payload] on model Vehicle`. Navigator order fetches and the console scheduler both hit this. Nested resources now keep only the relations the wrapped model actually defines; direct endpoint behaviour is unchanged.
|
|
7
6
|
|
|
8
7
|
---
|
|
9
8
|
## Testing
|
|
10
|
-
- Added a
|
|
11
|
-
- Added
|
|
9
|
+
- Added resource-level regressions for parent expansions reaching a nested Vehicle or Fleet resource, for string and empty `with` forms, and for resources wrapping plain data.
|
|
10
|
+
- Added end-to-end checks resolving a real vehicle and a real fleet against an order-shaped request.
|
|
12
11
|
|
|
13
12
|
---
|
|
14
13
|
## Need help?
|
package/composer.json
CHANGED
package/extension.json
CHANGED
package/package.json
CHANGED
|
@@ -22,8 +22,13 @@ trait ResolvesPublicRelationFields
|
|
|
22
22
|
/**
|
|
23
23
|
* The relations the caller asked for, as real Eloquent relation names.
|
|
24
24
|
*
|
|
25
|
-
* The controller
|
|
26
|
-
*
|
|
25
|
+
* The controller normalises, maps and allowlists `with` / `expand` for the
|
|
26
|
+
* resource it returns. That guarantee does not travel: this resource is also
|
|
27
|
+
* rendered nested inside Order and Driver responses, where the request's
|
|
28
|
+
* `with` names the parent's relations (`payload`, `driverAssigned.vehicle`).
|
|
29
|
+
* A nested resource re-reads the same request, so only relations the wrapped
|
|
30
|
+
* model actually defines are kept — a parent's expansion must never reach
|
|
31
|
+
* this model's loader, where it raises RelationNotFoundException.
|
|
27
32
|
*
|
|
28
33
|
* @return array<int, string>
|
|
29
34
|
*/
|
|
@@ -39,7 +44,28 @@ trait ResolvesPublicRelationFields
|
|
|
39
44
|
return [];
|
|
40
45
|
}
|
|
41
46
|
|
|
42
|
-
|
|
47
|
+
$with = array_values(array_filter(array_map('strval', $with), 'strlen'));
|
|
48
|
+
|
|
49
|
+
return array_values(array_filter($with, fn (string $relation): bool => $this->definesRelation($relation)));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Whether the wrapped model defines the root of a relation path.
|
|
54
|
+
*
|
|
55
|
+
* Only the first segment is checked: nested segments belong to the related
|
|
56
|
+
* model, and Eloquent validates those itself once the root exists.
|
|
57
|
+
*/
|
|
58
|
+
private function definesRelation(string $relation): bool
|
|
59
|
+
{
|
|
60
|
+
$resource = $this->resource;
|
|
61
|
+
|
|
62
|
+
if (!is_object($resource) || !method_exists($resource, 'loadMissing')) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
$root = explode('.', $relation, 2)[0];
|
|
67
|
+
|
|
68
|
+
return $root !== '' && method_exists($resource, $root);
|
|
43
69
|
}
|
|
44
70
|
|
|
45
71
|
/**
|
|
@@ -24,7 +24,9 @@ class Fleet extends FleetbaseResource
|
|
|
24
24
|
// dropped anything outside the public allowlist, so they are safe to
|
|
25
25
|
// hand to the loader. `subfleets` used to arrive here spelled exactly
|
|
26
26
|
// that and reach `load('subfleets')`, which is not the relation —
|
|
27
|
-
// the documented expansion raised instead of resolving.
|
|
27
|
+
// the documented expansion raised instead of resolving. Relations the
|
|
28
|
+
// fleet does not define are dropped as well: nested under another
|
|
29
|
+
// response the request's `with` describes the parent, not the fleet.
|
|
28
30
|
$with = $this->requestedRelations($request);
|
|
29
31
|
|
|
30
32
|
if ($with !== []) {
|
|
@@ -452,3 +452,26 @@ test('an unsupported expansion on retrieve cannot reach eloquent', function () {
|
|
|
452
452
|
->and($payload)->not->toHaveKey('not_a_relation')
|
|
453
453
|
->and($request->input('with'))->toBe(['vendor']);
|
|
454
454
|
});
|
|
455
|
+
|
|
456
|
+
test('a vehicle nested in an order response ignores the order expansions', function () {
|
|
457
|
+
fleetopsVehicleContractBoot();
|
|
458
|
+
|
|
459
|
+
$controller = new VehicleController();
|
|
460
|
+
$createRequest = fleetopsVehicleContractRequest(CreateVehicleRequest::class, 'POST', ['make' => 'Ford', 'driver' => 'driver_contract1']);
|
|
461
|
+
$created = $controller->create($createRequest)->resolve($createRequest);
|
|
462
|
+
$vehicle = Vehicle::where('public_id', $created['id'])->firstOrFail();
|
|
463
|
+
|
|
464
|
+
// An order fetch expands the order's own relations. The Order resource nests
|
|
465
|
+
// the assigned vehicle as a full Vehicle resource, which re-reads the same
|
|
466
|
+
// request: `payload` is not a vehicle relation and used to reach load(),
|
|
467
|
+
// answering the whole order request with a RelationNotFoundException.
|
|
468
|
+
$orderRequest = fleetopsVehicleContractRequest(Request::class, 'GET', ['with' => ['payload', 'driverAssigned.vehicle', 'driver']], 'v1/orders');
|
|
469
|
+
$payload = (new Fleetbase\FleetOps\Http\Resources\v1\Vehicle($vehicle))->resolve($orderRequest);
|
|
470
|
+
|
|
471
|
+
expect($payload['id'])->toBe($created['id'])
|
|
472
|
+
->and($payload)->not->toHaveKey('payload')
|
|
473
|
+
->and($vehicle->relationLoaded('payload'))->toBeFalse()
|
|
474
|
+
// The vehicle's own expansion in the same list still resolves.
|
|
475
|
+
->and($payload['driver'])->toBeObject()
|
|
476
|
+
->and($payload['driver_id'])->toBe('driver_contract1');
|
|
477
|
+
});
|
|
@@ -144,3 +144,20 @@ test('fleet resource accepts the explicit nested expansion spelling', function (
|
|
|
144
144
|
expect($subFleet->relationLoaded('drivers'))->toBeTrue()
|
|
145
145
|
->and($subFleet->relationLoaded('vehicles'))->toBeFalse();
|
|
146
146
|
});
|
|
147
|
+
|
|
148
|
+
test('fleet resource ignores expansions that describe a parent resource', function () {
|
|
149
|
+
fleetopsFleetResourceBoot();
|
|
150
|
+
|
|
151
|
+
$fleet = Fleet::where('uuid', 'fleet-parent-1')->first();
|
|
152
|
+
|
|
153
|
+
// Rendered inside another response, the request's `with` is the parent's
|
|
154
|
+
// list. `payload` is not a fleet relation and used to raise from load();
|
|
155
|
+
// the fleet's own relation in the same list still loads.
|
|
156
|
+
$request = Request::create('/v1/orders', 'GET', ['with' => ['payload', 'drivers']]);
|
|
157
|
+
$resolved = (new FleetResource($fleet))->resolve($request);
|
|
158
|
+
|
|
159
|
+
expect($resolved['name'])->toBe('Parent Fleet')
|
|
160
|
+
->and($resolved)->not->toHaveKey('payload')
|
|
161
|
+
->and($fleet->relationLoaded('payload'))->toBeFalse()
|
|
162
|
+
->and($fleet->relationLoaded('drivers'))->toBeTrue();
|
|
163
|
+
});
|
|
@@ -311,3 +311,42 @@ test('vehicle without driver resource omits driver relationships and serializes
|
|
|
311
311
|
])
|
|
312
312
|
->and($webhook)->not->toHaveKey('driver');
|
|
313
313
|
});
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Resolve the request's `with` list the way a nested resource does.
|
|
317
|
+
*
|
|
318
|
+
* @return array<int, string>
|
|
319
|
+
*/
|
|
320
|
+
function fleetopsVehicleResourceRequestedRelations(mixed $resource, array $query): array
|
|
321
|
+
{
|
|
322
|
+
$request = Request::create('/api/v1/fleet-ops/orders', 'GET', $query);
|
|
323
|
+
$method = new ReflectionMethod(VehicleResource::class, 'requestedRelations');
|
|
324
|
+
$method->setAccessible(true);
|
|
325
|
+
|
|
326
|
+
return $method->invoke(new VehicleResource($resource), $request);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
test('requested relations keep only what the wrapped model defines', function () {
|
|
330
|
+
$vehicle = new class(['uuid' => 'vehicle-uuid']) extends FleetOpsVehicleResourceFixture {
|
|
331
|
+
public function driver(): void
|
|
332
|
+
{
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
// Nested under an Order, the request's `with` describes the order:
|
|
337
|
+
// `payload` and `driverAssigned.vehicle` are not vehicle relations and must
|
|
338
|
+
// never reach this model's loader. The vehicle's own relations still pass,
|
|
339
|
+
// including a nested path whose root the vehicle defines.
|
|
340
|
+
expect(fleetopsVehicleResourceRequestedRelations($vehicle, ['with' => ['payload', 'driverAssigned.vehicle', 'driver', 'driver.user', '']]))->toBe(['driver', 'driver.user'])
|
|
341
|
+
->and(fleetopsVehicleResourceRequestedRelations($vehicle, ['with' => 'payload,driver']))->toBe(['driver'])
|
|
342
|
+
->and(fleetopsVehicleResourceRequestedRelations($vehicle, ['with' => ['.driver']]))->toBe([])
|
|
343
|
+
->and(fleetopsVehicleResourceRequestedRelations($vehicle, []))->toBe([]);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
test('requested relations are empty when the resource cannot load any', function () {
|
|
347
|
+
// Webhook fixtures and compact serializers hand the resource plain data;
|
|
348
|
+
// there is no loader to feed, so nothing is requested from it.
|
|
349
|
+
expect(fleetopsVehicleResourceRequestedRelations(['uuid' => 'vehicle-uuid'], ['with' => ['driver']]))->toBe([])
|
|
350
|
+
->and(fleetopsVehicleResourceRequestedRelations((object) ['uuid' => 'vehicle-uuid'], ['with' => ['driver']]))->toBe([])
|
|
351
|
+
->and(fleetopsVehicleResourceRequestedRelations(fleetopsVehicleResourceFixture(), ['with' => ['driver']]))->toBe([]);
|
|
352
|
+
});
|