From c4cbfbde05cc7067d2762c2cf70be81426138e5a Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Sun, 19 Apr 2026 21:52:31 -0400 Subject: [PATCH 1/2] fix: prevent orchestrator_priority null constraint violation on order creation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a user creates an order via the order/form component without filling in the orchestrator constraints section, the frontend sends orchestrator_priority as null (or omits it entirely). Both controllers use request->only([..., 'orchestrator_priority']), which returns null for keys absent from the request. That explicit null is then forwarded to Order::create() / Order::update(), bypassing MySQL's column-level default of 50 and raising: SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'orchestrator_priority' cannot be null Root cause ---------- The migration defines the column as NOT NULL with a DB default of 50, but Eloquent's INSERT statement includes every key present in the input array — even when the value is null — so the DB default is never used. Fix (defence-in-depth, three layers) -------------------------------------- 1. Order model — $attributes default: Added protected $attributes = ['orchestrator_priority' => 50] so that any new Order instance starts with a valid value before any input is applied. 2. Order model — setOrchestratorPriorityAttribute mutator: Coerces null or non-numeric values to 50 at the model layer, ensuring the constraint can never be violated regardless of the call site. 3. Controllers — explicit null-guard before create/update: Both Api\v1\OrderController (create + update) and Internal\v1\OrderController (createRecord) now check whether orchestrator_priority is missing or non-numeric and set it to 50 before passing $input to Eloquent. This mirrors the existing pattern used for 'type' and 'status'. 4. Migration — make column nullable: Added a new migration (2026_04_20_000001) that alters the column to nullable on existing installations that have already run the original migration. The application layer guarantees 50 is always written, so NULL will not appear in practice; nullable is a belt-and-suspenders safety net. --- ...ator_priority_nullable_on_orders_table.php | 43 +++++++++++++++++++ .../Controllers/Api/v1/OrderController.php | 12 ++++++ .../Internal/v1/OrderController.php | 6 +++ server/src/Models/Order.php | 26 +++++++++++ 4 files changed, 87 insertions(+) create mode 100644 server/migrations/2026_04_20_000001_make_orchestrator_priority_nullable_on_orders_table.php diff --git a/server/migrations/2026_04_20_000001_make_orchestrator_priority_nullable_on_orders_table.php b/server/migrations/2026_04_20_000001_make_orchestrator_priority_nullable_on_orders_table.php new file mode 100644 index 00000000..b1cd91fb --- /dev/null +++ b/server/migrations/2026_04_20_000001_make_orchestrator_priority_nullable_on_orders_table.php @@ -0,0 +1,43 @@ +unsignedTinyInteger('orchestrator_priority')->default(50)->nullable()->change(); + }); + } + + public function down(): void + { + // First back-fill any NULLs so the NOT NULL constraint can be restored. + \Illuminate\Support\Facades\DB::table('orders') + ->whereNull('orchestrator_priority') + ->update(['orchestrator_priority' => 50]); + + Schema::table('orders', function (Blueprint $table) { + $table->unsignedTinyInteger('orchestrator_priority')->default(50)->nullable(false)->change(); + }); + } +}; diff --git a/server/src/Http/Controllers/Api/v1/OrderController.php b/server/src/Http/Controllers/Api/v1/OrderController.php index 77d45153..8c5e46c7 100644 --- a/server/src/Http/Controllers/Api/v1/OrderController.php +++ b/server/src/Http/Controllers/Api/v1/OrderController.php @@ -299,6 +299,12 @@ public function create(CreateOrderRequest $request) $input['adhoc'] = Utils::isTrue($input['adhoc']) ? 1 : 0; } + // Ensure orchestrator_priority is never null — the column is NOT NULL + // and the DB default is bypassed when Eloquent receives an explicit null. + if (!isset($input['orchestrator_priority']) || !is_numeric($input['orchestrator_priority'])) { + $input['orchestrator_priority'] = 50; + } + if (!isset($input['payload_uuid'])) { return response()->apiError('Attempted to attach invalid payload to order.'); } @@ -527,6 +533,12 @@ public function update($id, UpdateOrderRequest $request) $order->dispatch(); } + // Ensure orchestrator_priority is never null on update either — + // only apply the default when the key was explicitly sent as null/empty. + if (array_key_exists('orchestrator_priority', $input) && !is_numeric($input['orchestrator_priority'])) { + $input['orchestrator_priority'] = 50; + } + // update the order $order->update($input); $order->flushAttributesCache(); diff --git a/server/src/Http/Controllers/Internal/v1/OrderController.php b/server/src/Http/Controllers/Internal/v1/OrderController.php index df8b20ca..d3efaf3f 100644 --- a/server/src/Http/Controllers/Internal/v1/OrderController.php +++ b/server/src/Http/Controllers/Internal/v1/OrderController.php @@ -126,6 +126,12 @@ function ($request, &$input) { $input['order_config_uuid'] = $defaultOrderConfig->uuid; } } + + // Ensure orchestrator_priority is never null — the column is NOT NULL + // and the DB default is bypassed when Eloquent receives an explicit null. + if (!isset($input['orchestrator_priority']) || !is_numeric($input['orchestrator_priority'])) { + $input['orchestrator_priority'] = 50; + } }, function (&$request, Order &$order, &$requestInput) { $input = $request->input('order'); diff --git a/server/src/Models/Order.php b/server/src/Models/Order.php index ecf38b34..1fc75503 100644 --- a/server/src/Models/Order.php +++ b/server/src/Models/Order.php @@ -219,6 +219,20 @@ class Order extends Model 'orchestrator_priority' => 'integer', ]; + /** + * The model's default attribute values. + * + * Ensures that `orchestrator_priority` is never persisted as NULL even when + * the caller omits the field entirely (e.g. the order/form component does + * not require the user to fill in orchestrator constraints). The value + * mirrors the database column default defined in the migration. + * + * @var array + */ + protected $attributes = [ + 'orchestrator_priority' => 50, + ]; + /** * The attributes excluded from the model's JSON form. * @@ -619,6 +633,18 @@ public function getUpdatedByNameAttribute() return data_get($this, 'updatedBy.name'); } + /** + * Set the orchestrator_priority attribute. + * + * Coerces null or non-numeric values to the default priority of 50 so that + * the NOT NULL database constraint is never violated when a user submits + * the order form without filling in the orchestrator constraints section. + */ + public function setOrchestratorPriorityAttribute($value): void + { + $this->attributes['orchestrator_priority'] = is_numeric($value) ? (int) $value : 50; + } + /** * Set the order type attribute, which defaults to `default`. */ From a47125073f94755b58c02580716dd6990c04153a Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Mon, 20 Apr 2026 16:56:23 +0800 Subject: [PATCH 2/2] fix virtual routing on reload; bump to v0.6.41 --- addon/routes/operations.js | 24 +++++++++++++++++++++++- composer.json | 2 +- extension.json | 2 +- package.json | 2 +- 4 files changed, 26 insertions(+), 4 deletions(-) diff --git a/addon/routes/operations.js b/addon/routes/operations.js index 47e75a4a..674b464d 100644 --- a/addon/routes/operations.js +++ b/addon/routes/operations.js @@ -1,3 +1,25 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class OperationsRoute extends Route {} +export default class OperationsRoute extends Route { + @service('universe/menu-service') menuService; + @service universe; + + beforeModel(transition) { + if (transition.intent && transition.intent.url) { + // Here we will check if it's an actual virtual route instead of operations + const intendedUrl = transition.intent.url; + const intentSegments = intendedUrl.split('/'); + // Needs to match for section and slug + const section = intentSegments[2]; + const slug = intentSegments[3]; + // This is not a operations route check menu service for a virtual registration match + if (section !== 'operations') { + const menuItem = this.menuService.lookupMenuItem('engine:fleet-ops', slug, null, section); + if (menuItem) { + return this.universe.transitionMenuItem('console.fleet-ops.virtual', menuItem); + } + } + } + } +} diff --git a/composer.json b/composer.json index 1fe655a8..24d241ad 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "fleetbase/fleetops-api", - "version": "0.6.40", + "version": "0.6.41", "description": "Fleet & Transport Management Extension for Fleetbase", "keywords": [ "fleetbase-extension", diff --git a/extension.json b/extension.json index ca0441c4..2d4518a3 100644 --- a/extension.json +++ b/extension.json @@ -1,6 +1,6 @@ { "name": "Fleet-Ops", - "version": "0.6.40", + "version": "0.6.41", "description": "Fleet & Transport Management Extension for Fleetbase", "repository": "https://github.com/fleetbase/fleetops", "license": "AGPL-3.0-or-later", diff --git a/package.json b/package.json index cca3d89f..7c6db5f6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@fleetbase/fleetops-engine", - "version": "0.6.40", + "version": "0.6.41", "description": "Fleet & Transport Management Extension for Fleetbase", "fleetbase": { "route": "fleet-ops"