Skip to content

Commit d73e044

Browse files
committed
Give custom fields a public id
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.
1 parent 4c763ce commit d73e044

5 files changed

Lines changed: 102 additions & 2 deletions

File tree

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
3+
use Fleetbase\Models\CustomField;
4+
use Illuminate\Database\Migrations\Migration;
5+
use Illuminate\Database\Schema\Blueprint;
6+
use Illuminate\Support\Facades\Schema;
7+
8+
return new class extends Migration {
9+
/**
10+
* Run the migrations.
11+
*
12+
* Custom fields were addressed by uuid alone, which left every API that
13+
* hands one out exposing an internal identifier where the rest of the
14+
* platform shows a public id. Existing rows are backfilled so nothing has
15+
* to cope with a field that has no id.
16+
*/
17+
public function up(): void
18+
{
19+
if (!Schema::hasTable('custom_fields') || Schema::hasColumn('custom_fields', 'public_id')) {
20+
return;
21+
}
22+
23+
Schema::table('custom_fields', function (Blueprint $table) {
24+
$table->string('public_id', 191)->nullable()->after('uuid')->index();
25+
});
26+
27+
CustomField::withTrashed()->whereNull('public_id')->get()->each(function (CustomField $field) {
28+
$field->update(['public_id' => CustomField::generatePublicId('custom_field')]);
29+
});
30+
}
31+
32+
/**
33+
* Reverse the migrations.
34+
*/
35+
public function down(): void
36+
{
37+
if (!Schema::hasTable('custom_fields') || !Schema::hasColumn('custom_fields', 'public_id')) {
38+
return;
39+
}
40+
41+
Schema::table('custom_fields', function (Blueprint $table) {
42+
$table->dropIndex(['public_id']);
43+
$table->dropColumn(['public_id']);
44+
});
45+
}
46+
};

‎src/Models/CustomField.php‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@
55
use Fleetbase\Casts\Json;
66
use Fleetbase\Casts\PolymorphicType;
77
use Fleetbase\Traits\HasApiModelBehavior;
8+
use Fleetbase\Traits\HasPublicId;
89
use Fleetbase\Traits\HasUuid;
910

1011
class CustomField extends Model
1112
{
1213
use HasUuid;
14+
use HasPublicId;
1315
use HasApiModelBehavior;
1416

1517
/**
@@ -19,12 +21,19 @@ class CustomField extends Model
1921
*/
2022
protected $table = 'custom_fields';
2123

24+
/**
25+
* The type of public Id to generate.
26+
*
27+
* @var string
28+
*/
29+
protected $publicIdType = 'custom_field';
30+
2231
/**
2332
* The attributes that are mass assignable.
2433
*
2534
* @var array
2635
*/
27-
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'];
36+
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'];
2837

2938
/**
3039
* The attributes that are guarded.

‎src/Traits/HasCustomFields.php‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,12 @@ public function setCustomFieldValue(string|CustomField $fieldOrKey, mixed $value
317317
'subject_uuid' => $this->getAttribute('uuid'),
318318
'company_uuid' => $this->getAttribute('company_uuid') ?? session('company'),
319319
]);
320-
$field->forceFill(['uuid' => CustomField::generateUuid()]);
320+
// A quiet save skips the `creating` hook that mints a public id,
321+
// so a field created on the fly would be the only one without one.
322+
$field->forceFill([
323+
'uuid' => CustomField::generateUuid(),
324+
'public_id' => CustomField::generatePublicId('custom_field'),
325+
]);
321326
method_exists($field, 'saveQuietly') ? $field->saveQuietly() : $field->save();
322327
// bust definition cache for subsequent lookups
323328
$this->customFieldCache = [];

‎tests/Unit/Models/RecordModelsTest.php‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,30 @@ public function clear(): bool
126126
$table->timestamps();
127127
$table->softDeletes();
128128
});
129+
$schema->create('custom_fields', function ($table) {
130+
$table->string('uuid')->primary();
131+
$table->string('public_id')->nullable()->unique();
132+
$table->string('company_uuid')->nullable();
133+
$table->string('category_uuid')->nullable();
134+
$table->string('subject_uuid')->nullable();
135+
$table->string('subject_type')->nullable();
136+
$table->string('name')->nullable();
137+
$table->string('label')->nullable();
138+
$table->string('type')->nullable();
139+
$table->string('for')->nullable();
140+
$table->string('component')->nullable();
141+
$table->text('options')->nullable();
142+
$table->boolean('required')->default(false);
143+
$table->boolean('editable')->default(true);
144+
$table->text('default_value')->nullable();
145+
$table->text('validation_rules')->nullable();
146+
$table->text('meta')->nullable();
147+
$table->text('description')->nullable();
148+
$table->text('help_text')->nullable();
149+
$table->integer('order')->default(0);
150+
$table->timestamps();
151+
$table->softDeletes();
152+
});
129153
$schema->create('user_devices', function ($table) {
130154
$table->string('uuid')->primary();
131155
$table->string('public_id')->nullable()->unique();
@@ -249,6 +273,21 @@ public function clear(): bool
249273
]);
250274
});
251275

276+
it('generates custom field public ids', function () {
277+
record_models_database();
278+
279+
$field = CustomField::query()->create([
280+
'company_uuid' => 'company-1',
281+
'name' => 'brakes',
282+
'label' => 'Brakes',
283+
'type' => 'pass-fail',
284+
]);
285+
286+
expect($field->public_id)->toStartWith('custom_field_')
287+
->and($field->public_id)->toHaveLength(strlen('custom_field_') + 10)
288+
->and($field->uuid)->not->toBeNull();
289+
});
290+
252291
it('casts custom field configuration values and keeps relationship keys stable', function () {
253292
record_models_database();
254293

‎tests/Unit/Traits/HasCustomFieldsTest.php‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ function has_custom_fields_database(string $routeUri = 'int/v1/subjects'): HasCu
119119
});
120120
$schema->create('custom_fields', function ($table) {
121121
$table->string('uuid')->primary();
122+
$table->string('public_id')->nullable();
122123
$table->string('company_uuid')->nullable();
123124
$table->string('category_uuid')->nullable();
124125
$table->string('subject_uuid')->nullable();

0 commit comments

Comments
 (0)