From d73e0449d9582c50f41e1ca815d5d16aa0f0adb0 Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Mon, 14 Sep 2026 14:55:41 +0800 Subject: [PATCH 1/2] Give custom fields a public id MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every other resource in the platform is addressed by a prefixed public id; a custom field was addressed by its uuid, so any API handing one out exposed an internal identifier. FleetOps' inspection API is the case in point — a submission names the field each answer belongs to, and had nothing but a uuid to name it with. Existing rows are backfilled, and the column is nullable and indexed rather than unique-and-required so the migration is safe on a table that is already populated. A field created on the fly through setCustomField() is saved quietly, which skips the hook that mints the id, so that path mints its own. --- ...0_add_public_id_to_custom_fields_table.php | 46 +++++++++++++++++++ src/Models/CustomField.php | 11 ++++- src/Traits/HasCustomFields.php | 7 ++- tests/Unit/Models/RecordModelsTest.php | 39 ++++++++++++++++ tests/Unit/Traits/HasCustomFieldsTest.php | 1 + 5 files changed, 102 insertions(+), 2 deletions(-) create mode 100644 migrations/2026_09_14_000000_add_public_id_to_custom_fields_table.php diff --git a/migrations/2026_09_14_000000_add_public_id_to_custom_fields_table.php b/migrations/2026_09_14_000000_add_public_id_to_custom_fields_table.php new file mode 100644 index 00000000..78f861e1 --- /dev/null +++ b/migrations/2026_09_14_000000_add_public_id_to_custom_fields_table.php @@ -0,0 +1,46 @@ +string('public_id', 191)->nullable()->after('uuid')->index(); + }); + + CustomField::withTrashed()->whereNull('public_id')->get()->each(function (CustomField $field) { + $field->update(['public_id' => CustomField::generatePublicId('custom_field')]); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + if (!Schema::hasTable('custom_fields') || !Schema::hasColumn('custom_fields', 'public_id')) { + return; + } + + Schema::table('custom_fields', function (Blueprint $table) { + $table->dropIndex(['public_id']); + $table->dropColumn(['public_id']); + }); + } +}; diff --git a/src/Models/CustomField.php b/src/Models/CustomField.php index 1f0d72e1..3796f864 100644 --- a/src/Models/CustomField.php +++ b/src/Models/CustomField.php @@ -5,11 +5,13 @@ use Fleetbase\Casts\Json; use Fleetbase\Casts\PolymorphicType; use Fleetbase\Traits\HasApiModelBehavior; +use Fleetbase\Traits\HasPublicId; use Fleetbase\Traits\HasUuid; class CustomField extends Model { use HasUuid; + use HasPublicId; use HasApiModelBehavior; /** @@ -19,12 +21,19 @@ class CustomField extends Model */ protected $table = 'custom_fields'; + /** + * The type of public Id to generate. + * + * @var string + */ + protected $publicIdType = 'custom_field'; + /** * The attributes that are mass assignable. * * @var array */ - protected $fillable = ['company_uuid', 'category_uuid', 'subject_uuid', 'subject_type', 'name', 'label', 'type', 'for', 'component', 'options', 'required', 'editable', 'default_value', 'validation_rules', 'meta', 'description', 'help_text', 'order']; + protected $fillable = ['public_id', 'company_uuid', 'category_uuid', 'subject_uuid', 'subject_type', 'name', 'label', 'type', 'for', 'component', 'options', 'required', 'editable', 'default_value', 'validation_rules', 'meta', 'description', 'help_text', 'order']; /** * The attributes that are guarded. diff --git a/src/Traits/HasCustomFields.php b/src/Traits/HasCustomFields.php index 7852c84a..e770a48f 100644 --- a/src/Traits/HasCustomFields.php +++ b/src/Traits/HasCustomFields.php @@ -317,7 +317,12 @@ public function setCustomFieldValue(string|CustomField $fieldOrKey, mixed $value 'subject_uuid' => $this->getAttribute('uuid'), 'company_uuid' => $this->getAttribute('company_uuid') ?? session('company'), ]); - $field->forceFill(['uuid' => CustomField::generateUuid()]); + // A quiet save skips the `creating` hook that mints a public id, + // so a field created on the fly would be the only one without one. + $field->forceFill([ + 'uuid' => CustomField::generateUuid(), + 'public_id' => CustomField::generatePublicId('custom_field'), + ]); method_exists($field, 'saveQuietly') ? $field->saveQuietly() : $field->save(); // bust definition cache for subsequent lookups $this->customFieldCache = []; diff --git a/tests/Unit/Models/RecordModelsTest.php b/tests/Unit/Models/RecordModelsTest.php index dce43c1d..e0351db8 100644 --- a/tests/Unit/Models/RecordModelsTest.php +++ b/tests/Unit/Models/RecordModelsTest.php @@ -126,6 +126,30 @@ public function clear(): bool $table->timestamps(); $table->softDeletes(); }); + $schema->create('custom_fields', function ($table) { + $table->string('uuid')->primary(); + $table->string('public_id')->nullable()->unique(); + $table->string('company_uuid')->nullable(); + $table->string('category_uuid')->nullable(); + $table->string('subject_uuid')->nullable(); + $table->string('subject_type')->nullable(); + $table->string('name')->nullable(); + $table->string('label')->nullable(); + $table->string('type')->nullable(); + $table->string('for')->nullable(); + $table->string('component')->nullable(); + $table->text('options')->nullable(); + $table->boolean('required')->default(false); + $table->boolean('editable')->default(true); + $table->text('default_value')->nullable(); + $table->text('validation_rules')->nullable(); + $table->text('meta')->nullable(); + $table->text('description')->nullable(); + $table->text('help_text')->nullable(); + $table->integer('order')->default(0); + $table->timestamps(); + $table->softDeletes(); + }); $schema->create('user_devices', function ($table) { $table->string('uuid')->primary(); $table->string('public_id')->nullable()->unique(); @@ -249,6 +273,21 @@ public function clear(): bool ]); }); +it('generates custom field public ids', function () { + record_models_database(); + + $field = CustomField::query()->create([ + 'company_uuid' => 'company-1', + 'name' => 'brakes', + 'label' => 'Brakes', + 'type' => 'pass-fail', + ]); + + expect($field->public_id)->toStartWith('custom_field_') + ->and($field->public_id)->toHaveLength(strlen('custom_field_') + 10) + ->and($field->uuid)->not->toBeNull(); +}); + it('casts custom field configuration values and keeps relationship keys stable', function () { record_models_database(); diff --git a/tests/Unit/Traits/HasCustomFieldsTest.php b/tests/Unit/Traits/HasCustomFieldsTest.php index ac1fc872..de8b5d70 100644 --- a/tests/Unit/Traits/HasCustomFieldsTest.php +++ b/tests/Unit/Traits/HasCustomFieldsTest.php @@ -119,6 +119,7 @@ function has_custom_fields_database(string $routeUri = 'int/v1/subjects'): HasCu }); $schema->create('custom_fields', function ($table) { $table->string('uuid')->primary(); + $table->string('public_id')->nullable(); $table->string('company_uuid')->nullable(); $table->string('category_uuid')->nullable(); $table->string('subject_uuid')->nullable(); From 546d962fa8ab077fe1b36a0f672a6832be35851b Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Mon, 14 Sep 2026 17:15:43 +0800 Subject: [PATCH 2/2] Prepare the v1.6.62 release metadata MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The release branch carried the feature and nothing else: composer.json still read 1.6.61 and RELEASE.md still described v1.6.61's IAM authorization work. release.yml refuses the tag unless both agree with the branch name, and its RELEASE.md check exists precisely to catch the previous release's notes left in place — which is what these were. composer.json is this repository's single source of the version, and RELEASE.md now opens with the version it describes. --- RELEASE.md | 16 +++++++++------- composer.json | 2 +- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index c398fce9..03da73fc 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,15 +1,17 @@ -# v1.6.61 — Faster IAM user authorization loading +# v1.6.62 — Custom fields get a public id ## Improvements -- Reduce repeated database queries when listing IAM users by loading roles, policies, and permissions in batches and reading each user's primary role once. -- Match authorization to each user's company membership, including users belonging to multiple companies and system administrators viewing users across companies. User response fields remain unchanged. +- Give every custom field a public id, so an API that hands one out names it the way the rest of the platform names a resource rather than exposing an internal uuid. `CustomField` takes `HasPublicId` with the `custom_field` prefix, and `public_id` becomes fillable. +- Mint an id on the one path that would otherwise miss it: `HasCustomFields::setCustomField()` saves a field it creates on the fly with `saveQuietly()`, which skips the hook that assigns the id. ## Reliability -- Add database-backed coverage for company isolation, missing and deleted memberships, recovery after a membership was initially absent, and matching responses between lazy and eager loading. -- Enable PHP CI and Postman checks for `release/v*` branches and support release tagging from both `release/v*` and `dev-v*` branches. +- Backfill existing rows in the migration, and add the column as nullable and indexed rather than unique-and-required, so it is safe on an already-populated `custom_fields` table. +- Cover id generation for `CustomField`, and add the column to the in-memory schemas whose saves now probe it for uniqueness. -No database migration or configuration change is required. +This is platform-wide: every custom field gains a public id, not only those used by inspections. Nothing reads the new column yet — `withCustomFields()`'s public projection emits field names and is unchanged — so the change is additive for existing consumers. -Changes: [#251](https://github.com/fleetbase/core-api/pull/251), [#250](https://github.com/fleetbase/core-api/pull/250), and release-branch CI updates in [#252](https://github.com/fleetbase/core-api/pull/252). +A database migration is required. No configuration change is needed. + +Changes: [#254](https://github.com/fleetbase/core-api/pull/254). diff --git a/composer.json b/composer.json index 9d9aa28d..ecfbb64a 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "fleetbase/core-api", - "version": "1.6.61", + "version": "1.6.62", "description": "Core Framework and Resources for Fleetbase API", "keywords": [ "fleetbase",