From 97177da8f326f5b0027cb7a5415688ec2e84b203 Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 16 Jan 2026 10:30:54 +0100 Subject: [PATCH] fix: normalize default values in schema for ruby 4.0 and rails 8 compatibility --- packages/_examples/demo/app/models/user.rb | 2 + .../20260114100000_add_defaults_to_users.rb | 8 ++ packages/_examples/demo/db/schema.rb | 36 +++++++- packages/_examples/warehouse/.gitignore | 2 + .../collection.rb | 2 +- .../parser/column.rb | 40 ++++----- .../spec/dummy/app/models/test_default.rb | 4 + .../20260113130000_create_test_defaults.rb | 13 +++ .../parser/column_default_values_spec.rb | 87 +++++++++++++++++++ 9 files changed, 172 insertions(+), 22 deletions(-) create mode 100644 packages/_examples/demo/db/migrate/20260114100000_add_defaults_to_users.rb create mode 100644 packages/forest_admin_datasource_active_record/spec/dummy/app/models/test_default.rb create mode 100644 packages/forest_admin_datasource_active_record/spec/dummy/db/migrate/20260113130000_create_test_defaults.rb create mode 100644 packages/forest_admin_datasource_active_record/spec/lib/forest_admin_datasource_active_record/parser/column_default_values_spec.rb diff --git a/packages/_examples/demo/app/models/user.rb b/packages/_examples/demo/app/models/user.rb index 379658a50..d44ade28d 100644 --- a/packages/_examples/demo/app/models/user.rb +++ b/packages/_examples/demo/app/models/user.rb @@ -1,2 +1,4 @@ class User < ApplicationRecord + enum :status, { inactive: 0, active: 1, archived: 2 } + enum :role, { user: 0, admin: 1, moderator: 2 } end diff --git a/packages/_examples/demo/db/migrate/20260114100000_add_defaults_to_users.rb b/packages/_examples/demo/db/migrate/20260114100000_add_defaults_to_users.rb new file mode 100644 index 000000000..d91b163c7 --- /dev/null +++ b/packages/_examples/demo/db/migrate/20260114100000_add_defaults_to_users.rb @@ -0,0 +1,8 @@ +class AddDefaultsToUsers < ActiveRecord::Migration[8.0] + def change + add_column :users, :active, :boolean, default: false + add_column :users, :verified, :boolean, default: true + add_column :users, :status, :integer, default: 0 + add_column :users, :role, :integer, default: 1 + end +end diff --git a/packages/_examples/demo/db/schema.rb b/packages/_examples/demo/db/schema.rb index 92fd6a8be..bc8f1cf45 100644 --- a/packages/_examples/demo/db/schema.rb +++ b/packages/_examples/demo/db/schema.rb @@ -10,10 +10,38 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_03_11_103551) do +ActiveRecord::Schema[8.0].define(version: 2026_01_14_100000) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" + create_table "active_storage_attachments", force: :cascade do |t| + t.string "name", null: false + t.string "record_type", null: false + t.bigint "record_id", null: false + t.bigint "blob_id", null: false + t.datetime "created_at", null: false + t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id" + t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true + end + + create_table "active_storage_blobs", force: :cascade do |t| + t.string "key", null: false + t.string "filename", null: false + t.string "content_type" + t.text "metadata" + t.string "service_name", null: false + t.bigint "byte_size", null: false + t.string "checksum" + t.datetime "created_at", null: false + t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true + end + + create_table "active_storage_variant_records", force: :cascade do |t| + t.bigint "blob_id", null: false + t.string "variation_digest", null: false + t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true + end + create_table "bookings", force: :cascade do |t| t.bigint "car_id", null: false t.integer "customer_id" @@ -66,8 +94,14 @@ t.string "password" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.boolean "active", default: false + t.boolean "verified", default: true + t.integer "status", default: 0 + t.integer "role", default: 1 end + add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" + add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" add_foreign_key "bookings", "cars" add_foreign_key "cars", "categories" add_foreign_key "cars_checks", "cars" diff --git a/packages/_examples/warehouse/.gitignore b/packages/_examples/warehouse/.gitignore index 49b79a913..ff5b9ff0c 100644 --- a/packages/_examples/warehouse/.gitignore +++ b/packages/_examples/warehouse/.gitignore @@ -37,3 +37,5 @@ /db/*.sqlite3 /db/*.sqlite3-journal /db/*.sqlite3-[0-9]* + +.forestadmin-rpc-schema.json \ No newline at end of file diff --git a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/collection.rb b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/collection.rb index 3b63fbe8c..4e7be520c 100644 --- a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/collection.rb +++ b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/collection.rb @@ -65,7 +65,7 @@ def fetch_fields is_primary_key: column_name == @model.primary_key || @model.primary_key.include?(column_name), is_read_only: false, is_sortable: true, - default_value: column.default, + default_value: column.default.nil? ? nil : normalize_default_value(column), enum_values: get_enum_values(@model, column), validation: get_validations(column) ) diff --git a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/parser/column.rb b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/parser/column.rb index 59e578d2a..3f5b6cd0e 100644 --- a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/parser/column.rb +++ b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/parser/column.rb @@ -56,29 +56,29 @@ def sti_column?(model, column) model.inheritance_column && column.name == model.inheritance_column end + def normalize_default_value(column) + case column.type + when :boolean + case column.default.to_s + when '0', 'f', 'false' + false + when '1', 't', 'true' + true + else + column.default + end + when :integer + column.default.to_i + when :float, :decimal + column.default.to_f + else + column.default + end + end + def operators_for_column_type(type) result = [Operators::PRESENT, Operators::BLANK, Operators::MISSING] equality = [Operators::EQUAL, Operators::NOT_EQUAL, Operators::IN, Operators::NOT_IN] - orderables = [ - Operators::LESS_THAN, - Operators::GREATER_THAN, - Operators::LESS_THAN_OR_EQUAL, - Operators::GREATER_THAN_OR_EQUAL - ] - strings = [ - Operators::LIKE, - Operators::I_LIKE, - Operators::CONTAINS, - Operators::I_CONTAINS, - Operators::NOT_CONTAINS, - Operators::NOT_I_CONTAINS, - Operators::STARTS_WITH, - Operators::I_STARTS_WITH, - Operators::ENDS_WITH, - Operators::I_ENDS_WITH, - Operators::SHORTER_THAN, - Operators::LONGER_THAN - ] if type.is_a? String orderables = [ diff --git a/packages/forest_admin_datasource_active_record/spec/dummy/app/models/test_default.rb b/packages/forest_admin_datasource_active_record/spec/dummy/app/models/test_default.rb new file mode 100644 index 000000000..f41af5ed9 --- /dev/null +++ b/packages/forest_admin_datasource_active_record/spec/dummy/app/models/test_default.rb @@ -0,0 +1,4 @@ +class TestDefault < ApplicationRecord + enum :status, { inactive: 0, active: 1, archived: 2 } + enum :priority, { low: 0, medium: 1, high: 2 } +end diff --git a/packages/forest_admin_datasource_active_record/spec/dummy/db/migrate/20260113130000_create_test_defaults.rb b/packages/forest_admin_datasource_active_record/spec/dummy/db/migrate/20260113130000_create_test_defaults.rb new file mode 100644 index 000000000..a5d0e5a46 --- /dev/null +++ b/packages/forest_admin_datasource_active_record/spec/dummy/db/migrate/20260113130000_create_test_defaults.rb @@ -0,0 +1,13 @@ +class CreateTestDefaults < ActiveRecord::Migration[7.0] + def change + create_table :test_defaults do |t| + t.boolean :active, default: false + t.boolean :verified, default: true + t.integer :status, default: 0 + t.integer :priority, default: 1 + t.string :name + + t.timestamps + end + end +end diff --git a/packages/forest_admin_datasource_active_record/spec/lib/forest_admin_datasource_active_record/parser/column_default_values_spec.rb b/packages/forest_admin_datasource_active_record/spec/lib/forest_admin_datasource_active_record/parser/column_default_values_spec.rb new file mode 100644 index 000000000..1a3c4c7af --- /dev/null +++ b/packages/forest_admin_datasource_active_record/spec/lib/forest_admin_datasource_active_record/parser/column_default_values_spec.rb @@ -0,0 +1,87 @@ +require 'spec_helper' + +module ForestAdminDatasourceActiveRecord + module Parser + describe Column do + include described_class + + let(:datasource) { Datasource.new({ adapter: 'sqlite3', database: 'db/database.db' }) } + let(:collection) { ForestAdminDatasourceActiveRecord::Collection.new(datasource, TestDefault) } + + before do + # Run the migration to create the test_defaults table + unless ActiveRecord::Base.connection.table_exists?(:test_defaults) + ActiveRecord::Migration.suppress_messages do + CreateTestDefaults.migrate(:up) + end + end + end + + describe 'normalize_default_value' do + context 'with boolean fields' do + it 'normalizes false default value' do + column = TestDefault.columns.find { |c| c.name == 'active' } + result = normalize_default_value(column) + + expect(result).to be(false) + expect(result).to be_a(FalseClass) + end + + it 'normalizes true default value' do + column = TestDefault.columns.find { |c| c.name == 'verified' } + result = normalize_default_value(column) + + expect(result).to be(true) + expect(result).to be_a(TrueClass) + end + end + + context 'with integer/enum fields' do + it 'normalizes integer default value to 0' do + column = TestDefault.columns.find { |c| c.name == 'status' } + result = normalize_default_value(column) + + expect(result).to eq(0) + expect(result).to be_a(Integer) + end + + it 'normalizes integer default value to 1' do + column = TestDefault.columns.find { |c| c.name == 'priority' } + result = normalize_default_value(column) + + expect(result).to eq(1) + expect(result).to be_a(Integer) + end + end + + context 'with nil default' do + it 'returns the original value for string with no default' do + column = TestDefault.columns.find { |c| c.name == 'name' } + + # name has no default, so column.default is nil + # In the collection, we check nil before calling normalize + expect(column.default).to be_nil + end + end + end + + describe 'integration with real columns' do + it 'handles all default value types correctly' do + # Test that actual column defaults are normalized properly + columns = TestDefault.columns + + active = columns.find { |c| c.name == 'active' } + verified = columns.find { |c| c.name == 'verified' } + status = columns.find { |c| c.name == 'status' } + priority = columns.find { |c| c.name == 'priority' } + + # All these should be properly normalized + expect(normalize_default_value(active)).to be(false) + expect(normalize_default_value(verified)).to be(true) + expect(normalize_default_value(status)).to eq(0) + expect(normalize_default_value(priority)).to eq(1) + end + end + end + end +end