Clean up the /scripts directory

Not removing anything, just organizing scripts into subdirectories to
make it a bit more readable.
This commit is contained in:
Mike Dalessio
2025-07-21 09:07:15 -04:00
parent f4ad46fe29
commit db6b700da8
18 changed files with 0 additions and 0 deletions
+117
View File
@@ -0,0 +1,117 @@
#!/usr/bin/env ruby
require_relative "../config/environment"
ActiveRecord::Base.logger = Logger.new(File::NULL)
class BootstrapSignalId
def initialize(dry_run: false)
@dry_run = dry_run
end
def run
SignalId::Database.on_master do
ApplicationRecord.with_each_tenant do |tenant|
puts "\n# tenant: #{tenant}"
next unless check_account_preconditions
create_signal_id_account if Account.sole.queenbee_id.nil?
create_signal_id_users
end
end
end
def check_account_preconditions
unless Account.count == 1
puts "There are #{Account.count} accounts, but exactly one is expected."
false
else
true
end
end
def create_signal_id_account
owner = SignalId::Identity.find_by_email_address!("kevin@37signals.com")
print_identity("New owner is:", owner)
unless @dry_run
qbattr = queenbee_account_attributes(owner)
queenbee_account = Queenbee::Remote::Account.create!(qbattr)
signal_id_account = SignalId::Account.find_by!(queenbee_id: queenbee_account.id)
signal_id_account.update_column :subdomain, ApplicationRecord.current_tenant
account = Account.sole
account.queenbee_id = queenbee_account.id
account.name = ApplicationRecord.current_tenant
account.save!
end
end
def create_signal_id_users
signal_account = Account.sole.signal_account
User.find_each do |user|
if !user.system? && user.signal_user_id.nil?
signal_identities = SignalId::Identity.where(email_address: user.email_address)
if signal_identities.length > 1
puts "Multiple identities found for #{user.email_address}:"
signal_identities.each { |identity| print_identity(" - ", identity) }
signal_identity = signal_identities.first
elsif signal_identities.length == 1
signal_identity = signal_identities.first
print_identity("Identity for #{user.email_address}:", signal_identity)
else
puts "No identity found for #{user.name} (#{user.email_address})"
signal_identity = nil
end
if signal_identity
unless @dry_run
signal_user = SignalId::User.find_or_create_by!(identity: signal_identity, account: signal_account)
user.signal_user_id = signal_user.id
user.save!
end
end
end
end
end
def print_identity(message = "Identity:", identity)
pad = " " * message.length
puts "#{message} #{identity.name} (#{identity.email_address})"
puts "#{pad } ID: #{identity.id}"
puts "#{pad } Username: #{identity.username}"
end
def queenbee_account_attributes(signal_identity)
{
skip_remote: true, # Fizzy creates its own local account
product_name: "fizzy",
name: account_name,
owner_identity_id: signal_identity.id,
trial: false,
subscription: subscription_attributes,
remote_request: request_attributes
}
end
def subscription_attributes
subscription = FreeV1Subscription
{ name: subscription.to_param, price: subscription.price }
end
def request_attributes
{ user_agent: "script/bootstrap-signal-id.rb" }
end
def account_name
name = ApplicationRecord.current_tenant
name += " (Beta)" if Rails.env.beta?
name
end
end
BootstrapSignalId.new(dry_run: false).run
+115
View File
@@ -0,0 +1,115 @@
#!/usr/bin/env ruby
require_relative "../config/environment"
require "redcarpet"
require "nokogiri"
class ActionText::Markdown < ApplicationRecord
belongs_to :record, polymorphic: true
end
class MarkdownToActionTextConverter
ATTACHMENT_URL_REGEX = %r{/u/(?<slug>[^\/\s\)]+)}
def initialize(html)
@doc = Nokogiri::HTML::DocumentFragment.parse(html)
@attachments = []
end
def convert
process_images
process_links
[ @doc.to_html, @attachments ]
end
private
def process_images
@doc.css("img").each do |img|
src = img["src"].presence
if src && match = src.match(ATTACHMENT_URL_REGEX)
if (attachment = find_attachment(match[:slug]))
img.replace(build_attachment_node(attachment))
@attachments << attachment
end
end
end
end
def process_links
@doc.css("a").each do |link|
href = link["href"].presence
if href && match = href.match(ATTACHMENT_URL_REGEX)
if (attachment = find_attachment(match[:slug]))
link.replace(build_attachment_node(attachment))
@attachments << attachment
end
end
end
end
def build_attachment_node(attachment)
html = ActionText::Attachment.from_attachable(attachment).to_html
fragment = Nokogiri::HTML::DocumentFragment.parse(html)
node = fragment.at_css("action-text-attachment")
node["url"] = Rails.application.routes.url_helpers.rails_blob_path(attachment.blob, only_path: true)
fragment
end
def find_attachment(slug)
ActiveStorage::Attachment.find_by(slug: slug)
end
end
class RedcarpetRenderer
def self.render(markdown)
renderer = Redcarpet::Render::HTML.new
markdowner = Redcarpet::Markdown.new(renderer,
autolink: true,
tables: true,
fenced_code_blocks: true,
strikethrough: true,
superscript: true,
)
markdowner.render(markdown.to_s)
end
end
def process_all(klass, field)
klass.find_each do |record|
markdown = ActionText::Markdown.find_by(record: record, name: field)
next unless markdown
puts "markdown.id=#{markdown.id}"
next unless markdown.record
html = RedcarpetRenderer.render(markdown.content.to_s)
converter = MarkdownToActionTextConverter.new(html)
rich_text_html, attachments = converter.convert
rich_text = ActionText::RichText.create!(
name: markdown.name,
record: markdown.record,
body: rich_text_html
)
attachments.each do |attachment|
attachment.update!(record: rich_text)
end
puts "✓ Created rich text for #{markdown.record_type}##{markdown.record_id} (#{markdown.name})"
rescue => e
warn "✗ Failed to process markdown ##{markdown.id}: #{e.class} - #{e.message}"
end
end
ApplicationRecord.with_each_tenant do |tenant|
puts "Processing tenant: #{tenant}"
ActionText::RichText.delete_all
process_all(Card, :description)
process_all(Comment, :body)
end
+47
View File
@@ -0,0 +1,47 @@
#! /usr/bin/env ruby
require_relative "../config/environment"
def migrate(source_service_name, target_service_name)
ApplicationRecord.with_each_tenant do |tenant|
puts "\n## #{tenant}"
report = { updated: 0, skipped: 0, errors: 0 }
if ActiveStorage::Blob.count == 0
puts "No blobs found, skipping."
next
end
ActiveStorage::Blob.service = source_service = ActiveStorage::Blob.services.fetch(source_service_name)
target_service = ActiveStorage::Blob.services.fetch(target_service_name)
ActiveStorage::Blob.find_each do |blob|
if target_service.name.to_sym == blob.service_name.to_sym
report[:skipped] += 1
putc "-"
elsif target_service.exist?(blob.key)
report[:skipped] += 1
putc "S"
else
begin
blob.open do |stream|
target_service.upload(blob.key, stream, checksum: blob.checksum)
end
report[:updated] += 1
putc "."
rescue ActiveStorage::FileNotFoundError
report[:errors] += 1
putc "E"
end
end
# Update the service name of the blob.
blob.update_column :service_name, target_service_name
end
puts
pp report
end
end
migrate :local, :purestorage
+21
View File
@@ -0,0 +1,21 @@
#!/usr/bin/env ruby
require_relative "../config/environment"
ApplicationRecord.with_each_tenant do |tenant|
account = Account.sole
signal_account = account.signal_account
signal_users = SignalId::User.where(account_id: signal_account.id)
signal_users.each do |signal_user|
unless User.find_by(signal_user_id: signal_user.id)
User.create!(
name: signal_user.identity.name,
email_address: signal_user.identity.email_address,
signal_user_id: signal_user.id,
password: SecureRandom.hex(36) # TODO: remove password column?
)
end
end
end
+9
View File
@@ -0,0 +1,9 @@
#!/usr/bin/env ruby
require_relative "../config/environment"
ApplicationRecord.with_each_tenant do |tenant|
Account.find_each do |account|
account.send(:create_default_closure_reasons)
end
end
+11
View File
@@ -0,0 +1,11 @@
#!/usr/bin/env ruby
require_relative "../config/environment"
ApplicationRecord.with_each_tenant do |tenant|
Card.find_each do |card|
card.events.find_each do |event|
Card::Eventable::SystemCommenter.new(card.reload, event).comment
end
end
end
+70
View File
@@ -0,0 +1,70 @@
#!/usr/bin/env ruby
require_relative "../config/environment"
domains = {
"production" => "fizzy.37signals.com",
"beta" => "fizzy-beta.37signals.com",
"staging" => "fizzy.37signals-staging.com"
}
def fix_attachments(rich_text)
if rich_text.body
rich_text.body.send(:attachment_nodes).each do |node|
sgid = SignedGlobalID.parse(node["sgid"], for: ActionText::Attachable::LOCATOR_NAME)
if sgid
puts "Fixing attachment node: #{node.to_html}"
model = sgid.model_class.find(sgid.model_id)
node["sgid"] = model.attachable_sgid
else
puts "Skipping attachment node without valid sgid: #{node.to_html}"
end
end
rich_text.save!
end
end
ApplicationRecord.with_each_tenant do |tenant|
account_id = Account.sole.queenbee_id
unless account_id
puts "Skipping URL fixup for tenant: #{tenant}"
next
end
puts "\n## Processing tenant: #{tenant}\n"
domain = domains[Rails.env] || domains["production"]
regex = %r{://\w+\.#{domain}/}
pp [ Account.sole.name, account_id, domain, regex ]
puts
Card.find_each do |card|
puts "### Processing card #{card.id} in #{Rails.application.routes.url_helpers.collection_card_path(card.collection, card)}"
fix_attachments(card.description)
card.reload
old_body = card.description.body.to_s
if match = regex.match(old_body)
puts "URL found in card #{card.id} in #{Rails.application.routes.url_helpers.collection_card_path(card.collection, card)}"
new_body = old_body.gsub(regex, "://#{domain}/#{account_id}/")
card.description.update(body: new_body) || raise("Failed to update card description for card #{card.id}")
end
end
Comment.find_each do |comment|
puts "### Processing comment #{comment.id} in #{Rails.application.routes.url_helpers.collection_card_path(comment.card.collection, comment.card)}"
fix_attachments(comment.body)
comment.reload
old_body = comment.body.body.to_s
if match = regex.match(old_body)
puts "URL found in comment #{comment.id} in #{Rails.application.routes.url_helpers.collection_card_path(comment.card.collection, comment.card)}"
new_body = old_body.gsub(regex, "://#{domain}/#{account_id}/")
comment.body.update(body: new_body) || raise("Failed to update comment body for comment #{comment.id}")
end
end
end
+28
View File
@@ -0,0 +1,28 @@
#! /usr/bin/env ruby
require_relative "../config/environment"
ApplicationRecord.with_each_tenant do |tenant|
puts "\n## #{tenant}"
report = { updated: 0, skipped: 0 }
ActiveStorage::Blob.find_each do |blob|
if blob.key.start_with?("#{tenant}/")
report[:skipped] += 1
else
blob.update_column :key, "#{tenant}/#{blob.key}"
report[:updated] += 1
end
end
pp report
disk_service = ActiveStorage::Blob.services.fetch(:local)
new_root = File.join(disk_service.root, tenant)
old_root = File.join("storage", "tenants", Rails.env, tenant, "files")
FileUtils.mkdir_p(new_root, verbose: true) unless File.directory?(new_root)
Dir.glob(File.join(old_root, "??")).each_slice(20) do |blob_dirs|
FileUtils.mv(blob_dirs, new_root, verbose: true)
end
end
+17
View File
@@ -0,0 +1,17 @@
#!/usr/bin/env ruby
require_relative "../config/environment"
def replace_url(string)
string.gsub(%r{/buckets/(\d+)/bubbles/(\d+)}) do
"/collections/#{$1}/cards/#{$2}"
end
end
ApplicationRecord.with_each_tenant do |tenant|
Account.find_each do |account|
Comment.find_each do |comment|
comment.update!(body: replace_url(comment.body.content.to_s))
end
end
end
+33
View File
@@ -0,0 +1,33 @@
#!/usr/bin/env ruby
require_relative "../config/environment"
tenant_names = []
ApplicationRecord.with_each_tenant do |tenant|
next if tenant == "#{Rails.env}-tenant"
account = Account.sole
queenbee_id = account.queenbee_id
tenant_names << { from: tenant, to: queenbee_id }
ApplicationRecord.remove_connection
end
pp [ "Tenant name changes:", tenant_names ]
root_config = ApplicationRecord.tenanted_root_config
tenant_names.each do |name|
from_db_path = root_config.database_path_for(name[:from])
to_db_path = root_config.database_path_for(name[:to])
from_path = from_db_path.split("/").take(4).join("/")
to_path = to_db_path.split("/").take(4).join("/")
unless from_path == to_path
FileUtils.move from_path, to_path, verbose: true
end
end
puts
pp [ "Tenants after renaming:", ApplicationRecord.tenants ]
+64
View File
@@ -0,0 +1,64 @@
#!/usr/bin/env ruby
require "find"
require "active_support/core_ext/string" # for camelize
RENAME_RULES = {
"bubble" => "card",
"closed" => "closed",
"poppable" => "closeable",
"pop" => "closure",
"bucket" => "collection"
}
EXTENSIONS = %w[.rb .yml .html .js .css .erb]
EXCLUDED_DIRS = %w[db .git script/renaming vendor/javascript]
# Helper to build replacement regex patterns respecting case and separators
def build_patterns(from, to)
boundary = "(?<=\\A|[^a-zA-Z0-9])#{from}(?=[^a-zA-Z0-9]|\\z)"
camel = from.camelize
camel_plural = camel.pluralize
underscore_plural = from.pluralize.underscore
dasherized_plural = underscore_plural.dasherize
[
# Match lowercase boundary-delimited
[ /#{boundary}/, to ],
# Match capitalized version (e.g., Bubble => Card)
[ /(?<![a-zA-Z0-9])#{from.capitalize}(?![a-z])/, to.capitalize ],
# Match all-uppercase
[ /(?<![a-zA-Z0-9])#{from.upcase}(?![A-Z])/, to.upcase ],
# Match CamelCase and plural CamelCase
[ /(?<![a-zA-Z0-9])#{camel}(?![a-z])/, to.camelize ],
[ /(?<![a-zA-Z0-9])#{camel_plural}(?![a-z])/, to.camelize.pluralize ],
# Match lowerCamelCase
[ /(?<![a-zA-Z0-9])#{from.camelize(:lower)}(?![a-z])/, to.camelize(:lower) ],
# Match underscore and dashed plural forms (e.g. bubbles(:logo) => cards(:logo))
[ /(?<![a-zA-Z0-9])#{underscore_plural}(?![a-z])/, to.pluralize.underscore ],
[ /(?<![a-zA-Z0-9])#{dasherized_plural}(?![a-z])/, to.pluralize.underscore.dasherize ]
]
end
patterns = []
RENAME_RULES.each do |from, to|
patterns.concat(build_patterns(from, to))
end
Find.find(".") do |path|
next if File.directory?(path)
next unless EXTENSIONS.include?(File.extname(path))
next if EXCLUDED_DIRS.any? { |dir| path.start_with?("./#{dir}/") }
content = File.read(path)
original_content = content.dup
patterns.each do |regex, replacement|
content.gsub!(regex, replacement)
end
if content != original_content
puts "Renaming in: #{path}"
File.write(path, content)
end
end
+75
View File
@@ -0,0 +1,75 @@
#!/usr/bin/env ruby
require "fileutils"
require "active_support/core_ext/string" # for camelize
# Configuration
EXCLUDED_DIRS = [ "db", ".git", "script/renaming" ].freeze
RENAMES = {
"bubble" => "card",
"poppable" => "closeable",
"closed" => "closed",
"pop" => "closure",
"bucket" => "collection"
}.freeze
FILE_EXTENSIONS = %w[rb yml html css js jpg jpeg png gif svg erb].freeze
def excluded_path?(path)
EXCLUDED_DIRS.any? { |excluded| path.split(File::SEPARATOR).include?(excluded) }
end
def rename_path(path)
new_path = path.dup
RENAMES.each do |from, to|
# Replace snake_case, kebab-case, plain, and CamelCase versions
patterns = [
[ /(?<=\A|[^a-zA-Z0-9])#{from}(?=[^a-zA-Z0-9]|\z)/i, to ],
[ from.camelize, to.camelize ],
[ from.camelize(:lower), to.camelize(:lower) ],
[ from.underscore.dasherize, to.underscore.dasherize ],
[ from.underscore, to.underscore ]
]
patterns.each do |pattern, replacement|
new_path.gsub!(pattern, replacement)
end
end
new_path
end
# Rename Directories First
dirs = Dir.glob("**/*/").reject { |path| excluded_path?(path) }.sort_by { |dir| -dir.count("/") }
puts "Renaming directories..."
dirs.each do |dir|
clean_dir = dir.chomp("/")
new_dir = rename_path(clean_dir)
next if clean_dir == new_dir
next if File.exist?(new_dir)
puts "Renaming dir: #{clean_dir} => #{new_dir}"
FileUtils.mkdir_p(File.dirname(new_dir))
FileUtils.mv(clean_dir, new_dir)
end
# Rename Files
files = Dir.glob("**/*.{#{FILE_EXTENSIONS.join(",")}}").reject { |path| excluded_path?(path) }
puts "Renaming files..."
files.each do |file|
new_file = rename_path(file)
next if file == new_file
next if File.exist?(new_file)
puts "Renaming file: #{file} => #{new_file}"
FileUtils.mkdir_p(File.dirname(new_file))
FileUtils.mv(file, new_file)
end
puts "Renaming complete!"
+165
View File
@@ -0,0 +1,165 @@
#!/usr/bin/env ruby
require_relative "../config/environment"
ApplicationRecord.with_each_tenant do |tenant|
id_mapping = {}
puts "Processing tenant: #{tenant}"
# Disable foreign key constraints
ApplicationRecord.connection.execute("PRAGMA foreign_keys = OFF;")
begin
# Get all cards ordered by ID
cards = Card.order(:id).to_a
# Create mapping of old IDs to new IDs
cards.each_with_index do |card, index|
id_mapping[card.id] = index + 1
end
# Update foreign keys in related tables
puts "Updating foreign keys in related tables..."
# Update assignments table
Assignment.where.not(card_id: nil).find_each do |assignment|
if id_mapping[assignment.card_id]
assignment.update_column(:card_id, id_mapping[assignment.card_id])
end
end
# Update card_engagements table
Card::Engagement.where.not(card_id: nil).find_each do |engagement|
if id_mapping[engagement.card_id]
engagement.update_column(:card_id, id_mapping[engagement.card_id])
end
end
# Update card_goldnesses table
Card::Goldness.where.not(card_id: nil).find_each do |goldness|
if id_mapping[goldness.card_id]
goldness.update_column(:card_id, id_mapping[goldness.card_id])
end
end
# Update closures table
Closure.where.not(card_id: nil).find_each do |closure|
if id_mapping[closure.card_id]
closure.update_column(:card_id, id_mapping[closure.card_id])
end
end
# Update comments table
Comment.where.not(card_id: nil).find_each do |comment|
if id_mapping[comment.card_id]
comment.update_column(:card_id, id_mapping[comment.card_id])
end
end
# Update pins table
Pin.where.not(card_id: nil).find_each do |pin|
if id_mapping[pin.card_id]
pin.update_column(:card_id, id_mapping[pin.card_id])
end
end
# Update taggings table
Tagging.where.not(card_id: nil).find_each do |tagging|
if id_mapping[tagging.card_id]
tagging.update_column(:card_id, id_mapping[tagging.card_id])
end
end
# Update watches table
Watch.where.not(card_id: nil).find_each do |watch|
if id_mapping[watch.card_id]
watch.update_column(:card_id, id_mapping[watch.card_id])
end
end
# Update events table (polymorphic relationship)
Event.where(eventable_type: "Card").find_each do |event|
if id_mapping[event.eventable_id]
event.update_column(:eventable_id, id_mapping[event.eventable_id])
end
end
# Update mentions table (polymorphic relationship)
Mention.where(source_type: "Card").find_each do |mention|
if id_mapping[mention.source_id]
mention.update_column(:source_id, id_mapping[mention.source_id])
end
end
# Update notifications table (polymorphic relationship)
Notification.where(source_type: "Card").find_each do |notification|
if id_mapping[notification.source_id]
notification.update_column(:source_id, id_mapping[notification.source_id])
end
end
# Update action_text_markdowns table (polymorphic relationship)
ActionText::RichText.where(record_type: "Card").find_each do |rich_text|
if id_mapping[rich_text.record_id]
rich_text.update_column(:record_id, id_mapping[rich_text.record_id])
end
end
# Reset the cards table IDs
puts "Resetting card IDs..."
cards.each do |card|
new_id = id_mapping[card.id]
# Use direct SQL to update the ID to avoid ActiveRecord validations
ApplicationRecord.connection.execute("UPDATE cards SET id = #{new_id} WHERE id = #{card.id}")
end
# Reset the SQLite sequence for the cards table
ApplicationRecord.connection.execute("DELETE FROM sqlite_sequence WHERE name = 'cards'")
max_id = Card.maximum(:id) || 0
ApplicationRecord.connection.execute("INSERT INTO sqlite_sequence (name, seq) VALUES ('cards', #{max_id})")
puts "Card IDs have been reset successfully!"
rescue => e
puts "Error: #{e.message}"
puts e.backtrace
ensure
# Re-enable foreign key constraints
ApplicationRecord.connection.execute("PRAGMA foreign_keys = ON;")
end
Card.find_each do |card|
description = card.description.content.dup
description.gsub!(/cards\/(\d+)\)/) do |match|
old_id = $1.to_i
new_id = id_mapping[old_id]
new_id ? "cards/#{new_id})" : match
end
if description != card.description.content
puts "Updating links in card #{card.id}"
card.update!(description: description)
end
end
Comment.find_each do |comment|
body = comment.body.content.dup
body.gsub!(/cards\/(\d+)\)/) do |match|
old_id = $1.to_i
new_id = id_mapping[old_id]
new_id ? "cards/#{new_id})" : match
end
if body != comment.body.content
puts "Updating links in comment #{comment.id}"
comment.update!(body: body)
end
end
# Output the mapping of old IDs to new IDs
puts "\nMapping of old IDs to new IDs:"
puts id_mapping.inspect
end
+146
View File
@@ -0,0 +1,146 @@
#!/usr/bin/env ruby
require_relative "../config/environment"
ApplicationRecord.with_each_tenant do |tenant|
id_mapping = {}
puts "Processing tenant: #{tenant}"
# Disable foreign key constraints
ApplicationRecord.connection.execute("PRAGMA foreign_keys = OFF;")
begin
# Get all collections ordered by ID
collections = Collection.order(:id).to_a
# Create mapping of old IDs to new IDs
collections.each_with_index do |collection, index|
id_mapping[collection.id] = index + 1
end
# Update foreign keys in related tables
puts "Updating foreign keys in related tables..."
# Update accesses table
Access.where.not(collection_id: nil).find_each do |access|
if id_mapping[access.collection_id]
access.update_column(:collection_id, id_mapping[access.collection_id])
end
end
# Update cards table
Card.where.not(collection_id: nil).find_each do |card|
if id_mapping[card.collection_id]
card.update_column(:collection_id, id_mapping[card.collection_id])
end
end
# Update collections_filters table (join table)
ApplicationRecord.connection.execute("SELECT collection_id FROM collections_filters").each do |row|
old_id = row[0]
if id_mapping[old_id]
ApplicationRecord.connection.execute("UPDATE collections_filters SET collection_id = #{id_mapping[old_id]} WHERE collection_id = #{old_id}")
end
end
# Update events table
Event.where.not(collection_id: nil).find_each do |event|
if id_mapping[event.collection_id]
event.update_column(:collection_id, id_mapping[event.collection_id])
end
end
# Update events table (polymorphic relationship)
Event.where(eventable_type: "Collection").find_each do |event|
if id_mapping[event.eventable_id]
event.update_column(:eventable_id, id_mapping[event.eventable_id])
end
end
# Update mentions table (polymorphic relationship)
Mention.where(source_type: "Collection").find_each do |mention|
if id_mapping[mention.source_id]
mention.update_column(:source_id, id_mapping[mention.source_id])
end
end
# Update notifications table (polymorphic relationship)
Notification.where(source_type: "Collection").find_each do |notification|
if id_mapping[notification.source_id]
notification.update_column(:source_id, id_mapping[notification.source_id])
end
end
# Update action_text_markdowns table (polymorphic relationship)
ActionText::RichText.where(record_type: "Collection").find_each do |rich_text|
if id_mapping[rich_text.record_id]
rich_text.update_column(:record_id, id_mapping[rich_text.record_id])
end
end
# Update active_storage_attachments table (polymorphic relationship)
ActiveStorage::Attachment.where(record_type: "Collection").find_each do |attachment|
if id_mapping[attachment.record_id]
attachment.update_column(:record_id, id_mapping[attachment.record_id])
end
end
# Reset the collections table IDs
puts "Resetting collection IDs..."
collections.each do |collection|
new_id = id_mapping[collection.id]
# Use direct SQL to update the ID to avoid ActiveRecord validations
ApplicationRecord.connection.execute("UPDATE collections SET id = #{new_id} WHERE id = #{collection.id}")
end
# Reset the SQLite sequence for the collections table
ApplicationRecord.connection.execute("DELETE FROM sqlite_sequence WHERE name = 'collections'")
max_id = Collection.maximum(:id) || 0
ApplicationRecord.connection.execute("INSERT INTO sqlite_sequence (name, seq) VALUES ('collections', #{max_id})")
puts "Collection IDs have been reset successfully!"
rescue => e
puts "Error: #{e.message}"
puts e.backtrace
ensure
# Re-enable foreign key constraints
ApplicationRecord.connection.execute("PRAGMA foreign_keys = ON;")
end
# Update links in card descriptions and comment bodies
Card.find_each do |card|
description = card.description.content.dup
description.gsub!(/collections\/(\d+)\//) do |match|
old_id = $1.to_i
new_id = id_mapping[old_id]
new_id ? "collections/#{new_id}/" : match
end
if description != card.description.content
puts "Updating links in card #{card.id}"
card.update!(description: description)
end
end
Comment.find_each do |comment|
body = comment.body.content.dup
body.gsub!(/collections\/(\d+)\//) do |match|
old_id = $1.to_i
new_id = id_mapping[old_id]
new_id ? "collections/#{new_id}/" : match
end
if body != comment.body.content
puts "Updating links in comment #{comment.id}"
comment.update!(body: body)
end
end
# Output the mapping of old IDs to new IDs
puts "\nMapping of old IDs to new IDs:"
puts id_mapping.inspect
end
+27
View File
@@ -0,0 +1,27 @@
#!/usr/bin/env ruby
require_relative "../config/environment"
CHANGES = [
{ from: "kevin@37signals.com", to: "kevin@basecamp.com" },
{ from: "david@37signals.com", to: "david@hey.com" },
{ from: "jay@37signals.com", to: "jay@basecamp.com" },
{ from: "jeremy@37signals.com", to: "jeremy@basecamp.com" },
{ from: "jillian@37signals.com", to: "jillian@basecamp.com" },
{ from: "jorge@37signals.com", to: "jorge@basecamp.com" },
{ from: "merissa@37signals.com", to: "merissa@basecamp.com" },
{ from: "michelle@37signals.com", to: "michelle@basecamp.com" },
{ from: "scott@37signals.com", to: "scott@basecamp.com" },
{ from: "silvia@37signals.com", to: "silvia@basecamp.com" }
]
ApplicationRecord.with_each_tenant do |tenant|
CHANGES.each do |change|
user = User.find_by(email_address: change[:from])
if user
puts "Updating user #{user.id} in tenant #{tenant}: #{change[:from]} -> #{change[:to]}"
user.email_address = change[:to]
user.save!
end
end
end