@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 CHANGED
@@ -1,14 +1,13 @@
1
- > v0.6.63 ~ "Fleet and driver relationship display fixes"
1
+ > v0.6.64 ~ "Order responses no longer fail when a vehicle is assigned"
2
2
 
3
3
  ---
4
4
  ## Bug Fixes
5
- - **Driver vendor names display correctly.** The Drivers table now uses the `vendor_name` value included in the list response instead of reading an unloaded `vendor` relationship.
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 regression assertion for the vendor column used by the Drivers table.
11
- - Added route coverage for the relationship names requested by the Fleet index, edit and details pages.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fleetbase/fleetops-api",
3
- "version": "0.6.63",
3
+ "version": "0.6.64",
4
4
  "description": "Fleet & Transport Management Extension for Fleetbase",
5
5
  "keywords": [
6
6
  "fleetbase-extension",
package/extension.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "Fleet-Ops",
3
- "version": "0.6.63",
3
+ "version": "0.6.64",
4
4
  "description": "Fleet & Transport Management Extension for Fleetbase",
5
5
  "repository": "https://github.com/fleetbase/fleetops",
6
6
  "license": "AGPL-3.0-or-later",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fleetbase/fleetops-engine",
3
- "version": "0.6.63",
3
+ "version": "0.6.64",
4
4
  "description": "Fleet & Transport Management Extension for Fleetbase",
5
5
  "fleetbase": {
6
6
  "route": "fleet-ops"
@@ -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 has already normalised, mapped and allowlisted `with` /
26
- * `expand` by the time a resource runs, so this is a plain read.
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
- return array_values(array_filter(array_map('strval', $with), 'strlen'));
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
+ });