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", 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();