Module: BetterAuth::Rails::Migration

Defined in:
lib/better_auth/rails/migration.rb

Constant Summary collapse

BOUNDED_STRING_LIMIT =
191

Class Method Summary collapse

Class Method Details

.active_record_connectionObject



61
62
63
64
65
# File 'lib/better_auth/rails/migration.rb', line 61

def active_record_connection
  ::ActiveRecord::Base.connection if defined?(::ActiveRecord::Base)
rescue
  nil
end

.active_record_dialect(connection) ⇒ Object



67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/better_auth/rails/migration.rb', line 67

def active_record_dialect(connection)
  adapter = connection.respond_to?(:adapter_name) ? connection.adapter_name.to_s.downcase : ""
  case adapter
  when /postgres/
    :postgres
  when /mysql/
    :mysql
  when /sqlite/
    :sqlite
  when /sqlserver|sql_server|mssql/
    :mssql
  else
    :postgres
  end
end

.add_column_lines(change, dialect: :rails) ⇒ Object



160
161
162
163
164
165
166
167
# File 'lib/better_auth/rails/migration.rb', line 160

def add_column_lines(change, dialect: :rails)
  change.fields.map do |logical_field, attributes|
    column = attributes[:field_name] || physical_name(logical_field)
    parts = ["    add_column :#{change.table_name}, :#{column}, :#{rails_type(logical_field, attributes, dialect)}"]
    parts.concat(column_options(logical_field, attributes))
    parts.join(", ")
  end
end

.bounded_string?(logical_field, attributes) ⇒ Boolean

Returns:

  • (Boolean)


202
203
204
205
206
207
208
209
210
# File 'lib/better_auth/rails/migration.rb', line 202

def bounded_string?(logical_field, attributes)
  logical_field.to_s == "id" ||
    logical_field.to_s.end_with?("Id") ||
    attributes[:unique] ||
    attributes[:index] ||
    attributes[:sortable] ||
    attributes[:references] ||
    attributes.key?(:default_value)
end

.column_line(logical_field, attributes, dialect: :rails) ⇒ Object



129
130
131
132
133
134
135
136
137
138
139
# File 'lib/better_auth/rails/migration.rb', line 129

def column_line(logical_field, attributes, dialect: :rails)
  column = attributes[:field_name] || physical_name(logical_field)
  type = rails_type(logical_field, attributes, dialect)
  parts = if type == "timestamptz"
    ["t.column :#{column}, :timestamptz"]
  else
    ["t.#{type} :#{column}"]
  end
  parts.concat(column_options(logical_field, attributes))
  "      #{parts.join(", ")}"
end

.column_options(logical_field, attributes) ⇒ Object



141
142
143
144
145
146
147
148
# File 'lib/better_auth/rails/migration.rb', line 141

def column_options(logical_field, attributes)
  parts = []
  parts << "limit: #{BOUNDED_STRING_LIMIT}" if limited_string?(logical_field, attributes)
  parts << "null: false" if attributes[:required]
  default = default_value(attributes)
  parts << "default: #{default}" unless default.nil?
  parts
end

.create_table_lines(table, dialect: :rails) ⇒ Object



107
108
109
110
111
112
113
114
115
116
# File 'lib/better_auth/rails/migration.rb', line 107

def create_table_lines(table, dialect: :rails)
  table_name = table.fetch(:model_name)
  lines = ["", "    create_table :#{table_name}, #{primary_key_options(table, dialect: dialect)} do |t|"]
  table.fetch(:fields).each do |logical_field, attributes|
    next if logical_field == "id"

    lines << column_line(logical_field, attributes, dialect: dialect)
  end
  lines << "    end"
end

.current_schema(connection) ⇒ Object



83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/better_auth/rails/migration.rb', line 83

def current_schema(connection)
  connection.tables.each_with_object({}) do |table_name, schema|
    columns = connection.columns(table_name).each_with_object({}) do |column, result|
      result[column.name.to_s] = column.respond_to?(:sql_type) ? column.sql_type.to_s : column.type.to_s
    end
    indexes = {names: Set.new, columns: Set.new, unique_columns: Set.new}
    connection.indexes(table_name).each do |index|
      indexes[:names] << index.name.to_s
      Array(index.columns).each do |column|
        column = column.to_s
        indexes[:columns] << column
        indexes[:unique_columns] << column if index.unique
      end
    end
    schema[table_name.to_s] = {name: table_name.to_s, columns: columns, indexes: indexes}
  end
end

.default_value(attributes) ⇒ Object



216
217
218
219
220
221
222
223
224
225
226
# File 'lib/better_auth/rails/migration.rb', line 216

def default_value(attributes)
  default = attributes[:default_value]
  return if default.respond_to?(:call)

  case default
  when true then "true"
  when false then "false"
  when Numeric then default.to_s
  when String then default.inspect
  end
end

.foreign_key_lines(table, options) ⇒ Object



174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
# File 'lib/better_auth/rails/migration.rb', line 174

def foreign_key_lines(table, options)
  table_name = table.fetch(:model_name)
  tables = table_map(options)
  table.fetch(:fields).filter_map do |logical_field, attributes|
    reference = attributes[:references]
    next unless reference

    column = attributes[:field_name] || physical_name(logical_field)
    target_table = foreign_key_target_table(reference, tables)
    target = target_table&.fetch(:model_name) || reference.fetch(:model)
    target_field = foreign_key_target_field(reference, target_table)
    primary_key = (target_field.to_s == "id") ? "" : ", primary_key: :#{target_field}"
    on_delete = reference[:on_delete] ? ", on_delete: :#{reference[:on_delete]}" : ""
    "    add_foreign_key :#{table_name}, :#{target}, column: :#{column}#{primary_key}#{on_delete}"
  end
end

.foreign_key_target_field(reference, target_table) ⇒ Object



245
246
247
248
249
250
251
252
253
254
255
256
257
258
# File 'lib/better_auth/rails/migration.rb', line 245

def foreign_key_target_field(reference, target_table)
  field = reference.fetch(:field).to_s
  return field unless target_table

  fields = target_table.fetch(:fields)
  attributes = fields.fetch(field, nil)
  return attributes[:field_name] || physical_name(field) if attributes

  if fields.each_value.any? { |data| data[:field_name].to_s == field }
    field
  else
    physical_name(field)
  end
end

.foreign_key_target_table(reference, tables) ⇒ Object



240
241
242
243
# File 'lib/better_auth/rails/migration.rb', line 240

def foreign_key_target_table(reference, tables)
  model = reference.fetch(:model).to_s
  tables.fetch(model, nil) || tables.each_value.find { |table| table.fetch(:model_name).to_s == model }
end

.index_line(table_name, column, unique: false) ⇒ Object



169
170
171
172
# File 'lib/better_auth/rails/migration.rb', line 169

def index_line(table_name, column, unique: false)
  unique_option = unique ? ", unique: true" : ""
  "    add_index :#{table_name}, :#{column}#{unique_option}"
end

.index_lines(table) ⇒ Object



150
151
152
153
154
155
156
157
158
# File 'lib/better_auth/rails/migration.rb', line 150

def index_lines(table)
  table_name = table.fetch(:model_name)
  table.fetch(:fields).filter_map do |logical_field, attributes|
    next unless attributes[:unique] || attributes[:index]

    column = attributes[:field_name] || physical_name(logical_field)
    index_line(table_name, column, unique: attributes[:unique])
  end
end

.limited_string?(logical_field, attributes) ⇒ Boolean

Returns:

  • (Boolean)


212
213
214
# File 'lib/better_auth/rails/migration.rb', line 212

def limited_string?(logical_field, attributes)
  attributes[:type] == "string" && bounded_string?(logical_field, attributes)
end

.migration_versionObject



101
102
103
104
105
# File 'lib/better_auth/rails/migration.rb', line 101

def migration_version
  return ::ActiveRecord::Migration.current_version if defined?(::ActiveRecord::Migration)

  "7.0"
end

.physical_name(value) ⇒ Object



228
229
230
# File 'lib/better_auth/rails/migration.rb', line 228

def physical_name(value)
  BetterAuth::Schema.send(:physical_name, value)
end

.plan_pending(options, connection: active_record_connection) ⇒ Object



52
53
54
55
56
57
58
59
# File 'lib/better_auth/rails/migration.rb', line 52

def plan_pending(options, connection: active_record_connection)
  dialect = active_record_dialect(connection)
  BetterAuth::SQLMigration.plan_from_existing(
    options,
    existing: current_schema(connection),
    dialect: dialect
  )
end

.primary_key_options(table, dialect: :rails) ⇒ Object



118
119
120
121
122
123
124
125
126
127
# File 'lib/better_auth/rails/migration.rb', line 118

def primary_key_options(table, dialect: :rails)
  attributes = table.fetch(:fields)["id"]
  return "id: false" unless attributes

  column = attributes[:field_name] || physical_name("id")
  parts = ["id: :#{rails_type("id", attributes, dialect)}"]
  parts << "limit: #{BOUNDED_STRING_LIMIT}" if limited_string?("id", attributes)
  parts << "primary_key: :#{column}" unless column == "id"
  parts.join(", ")
end

.rails_type(logical_field, attributes, dialect = :rails) ⇒ Object



191
192
193
194
195
196
197
198
199
200
# File 'lib/better_auth/rails/migration.rb', line 191

def rails_type(logical_field, attributes, dialect = :rails)
  case attributes[:type]
  when "boolean" then "boolean"
  when "date" then (dialect == :postgres) ? "timestamptz" : "datetime"
  when "number" then attributes[:bigint] ? "bigint" : "integer"
  when "json", "string[]", "number[]" then (dialect == :postgres) ? "jsonb" : "json"
  when "string" then bounded_string?(logical_field, attributes) ? "string" : "text"
  else "text"
  end
end

.render(options, migration_version: nil, dialect: nil) ⇒ Object



12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# File 'lib/better_auth/rails/migration.rb', line 12

def render(options, migration_version: nil, dialect: nil)
  migration_version ||= self.migration_version
  dialect ||= active_record_connection ? active_record_dialect(active_record_connection) : :rails
  tables = BetterAuth::Schema.auth_tables(options)
  lines = [
    "# frozen_string_literal: true",
    "",
    "class CreateBetterAuthTables < ActiveRecord::Migration[#{migration_version}]",
    "  def change"
  ]
  tables.each_value { |table| lines.concat(create_table_lines(table, dialect: dialect)) }
  tables.each_value { |table| lines.concat(index_lines(table)) }
  tables.each_value { |table| lines.concat(foreign_key_lines(table, options)) }
  lines.concat(["  end", "end", ""])
  lines.join("\n")
end

.render_pending(plan, class_name: "UpdateBetterAuthTables", migration_version: nil) ⇒ Object



29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# File 'lib/better_auth/rails/migration.rb', line 29

def render_pending(plan, class_name: "UpdateBetterAuthTables", migration_version: nil)
  migration_version ||= self.migration_version
  created_tables = plan.to_create.map(&:table_name).to_set
  lines = [
    "# frozen_string_literal: true",
    "",
    "class #{class_name} < ActiveRecord::Migration[#{migration_version}]",
    "  def change"
  ]
  plan.to_create.each { |change| lines.concat(create_table_lines(change.table, dialect: plan.dialect)) }
  plan.to_create.each { |change| lines.concat(index_lines(change.table)) }
  plan.to_create.each { |change| lines.concat(foreign_key_lines(change.table, plan.tables)) }
  plan.to_add.each { |change| lines.concat(add_column_lines(change, dialect: plan.dialect)) }
  plan.to_index.reject { |change| created_tables.include?(change.table_name) }.each do |change|
    lines << index_line(change.table_name, change.field_name, unique: change.unique)
  end
  plan.to_add.each do |change|
    lines.concat(foreign_key_lines({model_name: change.table_name, fields: change.fields}, plan.tables))
  end
  lines.concat(["  end", "end", ""])
  lines.join("\n")
end

.table_map(options) ⇒ Object



232
233
234
235
236
237
238
# File 'lib/better_auth/rails/migration.rb', line 232

def table_map(options)
  if options.respond_to?(:values) && options.values.all? { |value| value.respond_to?(:fetch) && value.key?(:fields) }
    options
  else
    BetterAuth::Schema.auth_tables(options)
  end
end