Merge branch 'main' into creation-bugs

* main:
  update lexical
  Update AR::Tenanted to have tenanted blob keys
  Delete old multitenancy migration script
  Drop the unnecessary initializer creating storage paths
  Card shadow tweaks
This commit is contained in:
Jorge Manrubia
2025-05-31 18:21:47 +02:00
9 changed files with 77 additions and 302 deletions
+1
View File
@@ -30,6 +30,7 @@ gem "jbuilder"
gem "actiontext-lexical", bc: "actiontext-lexical"
gem "image_processing", "~> 1.14"
gem "platform_agent"
gem "aws-sdk-s3", require: false
# Telemetry and logging
gem "sentry-ruby"
+21 -1
View File
@@ -7,7 +7,7 @@ GIT
GIT
remote: https://github.com/basecamp/active_record-tenanted
revision: 523947a0f0f49f66a8a62eb93e9ba1a345b3076b
revision: f444578d6e59a1bdd65eda0ff765c9b5dac73398
specs:
active_record-tenanted (0.1.0)
activerecord (>= 8.1.alpha)
@@ -144,6 +144,24 @@ GEM
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
ast (2.4.3)
aws-eventstream (1.3.2)
aws-partitions (1.1108.0)
aws-sdk-core (3.224.1)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
base64
jmespath (~> 1, >= 1.6.1)
logger
aws-sdk-kms (1.101.0)
aws-sdk-core (~> 3, >= 3.216.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.188.0)
aws-sdk-core (~> 3, >= 3.224.1)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.11.0)
aws-eventstream (~> 1, >= 1.0.2)
base64 (0.2.0)
bcrypt (3.1.20)
bcrypt_pbkdf (1.1.1)
@@ -225,6 +243,7 @@ GEM
jbuilder (2.13.0)
actionview (>= 5.0.0)
activesupport (>= 5.0.0)
jmespath (1.6.2)
json (2.11.3)
kamal (2.6.1)
activesupport (>= 7.0)
@@ -470,6 +489,7 @@ PLATFORMS
DEPENDENCIES
actiontext-lexical!
active_record-tenanted!
aws-sdk-s3
bcrypt (~> 3.1.7)
bootsnap
brakeman
+5 -7
View File
@@ -23,12 +23,10 @@
--border: 1px solid var(--color-ink-lighter);
/* Shadows */
--shadow: 0 0 0 1px oklch(var(--lch-black) / 0.02),
0 .2em 1.6em -0.8em oklch(var(--lch-black) / 0.2),
0 .4em 2.4em -1em oklch(var(--lch-black) / 0.3),
0 .4em .8em -1.2em oklch(var(--lch-black) / 0.4),
0 .8em 1.2em -1.6em oklch(var(--lch-black) / 0.5),
0 1.2em 1.6em -2em oklch(var(--lch-black) / 0.6);
--shadow: 0 0 0 1px oklch(var(--lch-black) / 5%),
0 0.2em 0.2em oklch(var(--lch-black) / 5%),
0 0.4em 0.4em oklch(var(--lch-black) / 5%),
0 0.8em 0.8em oklch(var(--lch-black) / 5%);
/* Components */
--btn-size: 2.65em;
@@ -186,7 +184,7 @@
--color-code-token__punctuation: oklch(var(--lch-ink-dark));
--color-code-token__selector: oklch(var(--lch-green-dark));
--color-code-token__variable: oklch(var(--lch-red-dark));
@media (prefers-color-scheme: dark) {
--lch-canvas: 20% 0.0195 232.58;
+16 -1
View File
@@ -101,7 +101,6 @@
--text-small: 1.1em;
background-color: var(--color-canvas);
box-shadow: 0 0 0 1px var(--color-ink-lighter);
line-height: 1.2;
.avatar {
@@ -122,6 +121,22 @@
display: none;
}
.card__collection {
background-color: var(--color-canvas);
color: color-mix(in srgb, var(--card-color) 40%, var(--color-ink));
padding: 0 0 0 var(--inline-space);
}
.card__collection-name {
border-inline-start: 1px solid color-mix(in srgb, var(--color-ink) 33%, var(--color-canvas));
color: color-mix(in srgb, var(--card-color) 40%, var(--color-ink));
}
.card__header {
color: color-mix(in srgb, var(--card-color) 40%, var(--color-ink));
margin: 0 0 calc(-0.5 * var(--card-padding-inline)) 0;
}
.card__title {
margin-block-start: 0.1em;
min-block-size: 0;
+5 -16
View File
@@ -25,6 +25,10 @@
.avatar {
--avatar-size: 2.75em;
}
@media (prefers-color-scheme: dark) {
box-shadow: 0 0 0 1px var(--color-ink-lighter);
}
}
/* Children
@@ -69,11 +73,6 @@
margin-block-start: calc(-1 * var(--card-padding-block));
margin-inline-start: calc(-1 * var(--card-padding-inline));
min-inline-size: 0;
.cards--considering & {
color: color-mix(in srgb, var(--card-color) 40%, var(--color-ink));
margin: 0 0 calc(-0.5 * var(--card-padding-inline)) 0;
}
}
.card__collection {
@@ -83,12 +82,6 @@
font-weight: 800;
padding: var(--block-space-half) var(--inline-space) var(--block-space-half) var(--inline-space-double);
text-transform: uppercase;
.cards--considering & {
background-color: var(--color-canvas);
color: color-mix(in srgb, var(--card-color) 40%, var(--color-ink));
padding: 0 0 0 var(--inline-space);
}
}
.card__id {
@@ -107,11 +100,6 @@
display: inline-flex;
margin-inline-start: var(--inline-space-half);
padding-inline-start: calc(var(--inline-space) * 0.75);
.cards--considering & {
border-inline-start: 1px solid color-mix(in srgb, var(--color-ink) 33%, var(--color-canvas));
color: color-mix(in srgb, var(--card-color) 40%, var(--color-ink));
}
}
.card__tags {
@@ -211,6 +199,7 @@
box-shadow: 0 0 0 1px color-mix(in srgb, var(--color-link) 20%, var(--color-canvas));
color: var(--btn-color);
font-weight: 700;
transition: none;
}
.card__move-button {
+1 -1
View File
@@ -6,7 +6,7 @@
<span class="card__collection">
<span class="card__id"><%= card.id %></span>
<%= link_to card.collection.name, cards_path(collection_ids: [ card.collection ]),
class: "card__collection-name txt-uppercase overflow-ellipsis" %>
class: "card__collection-name overflow-ellipsis" %>
</span>
<%= render "cards/display/preview/tags", card: card %>
</header>
-5
View File
@@ -1,5 +0,0 @@
Rails.application.config.after_initialize do
%w[ db files ].each do |dir|
Rails.root.join("storage", dir).mkpath
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
-271
View File
@@ -1,271 +0,0 @@
#!/usr/bin/env ruby
require_relative "../config/environment"
class EnactTenanting
CURRENT_DOMAIN = "https://fizzy.37signals.com"
TENANTED_DOMAIN = Rails.env.local? ? "http://%{tenant}.fizzy.localhost:3006" : "https://%{tenant}.fizzy.37signals.com"
PROJECT_MAPPING = {
693169841 => "37s", # Writebook improvements
693169850 => "37s", # Fizzy: Issues
693169853 => "37s", # Fizzy: 💡 IDEAS
693169858 => "37s", # Know It All: 💡 IDEAS
693169859 => "37s", # Cycle 1: BC4 Client access for templates
693169860 => "37s", # Know It All: Issues
693169862 => "37s", # Active Record Tenanting
693169863 => "37s", # Cycle 1: BC4 Hilltop View
693169865 => "37s", # [BC iOS] Search refactor
693169867 => "37s", # File icons refactor
693169870 => "37s", # HEY Preload Replies
693169872 => "37s", # [BC4 Android] Search Refactor
693169873 => "37s", # BC4
693169874 => "37s", # [HEY] ContentFilter pipeline rewrite
693169875 => "37s", # [Hey Calendar Android] Search
693169876 => "37s", # [HEY Calendar iOS] Search
693169877 => "37s", # [BC4] Turbo for comment and answer forms
693169842 => "dev", # Foobar
693169843 => "dev", # Test
693169851 => "dev", # Kevin
693169861 => "dev", # Testing space
693169869 => "dev", # Flower ideas
693169871 => "dev", # Mike's TODOs
693169856 => "qa" # QA Exploration
}
TENANTS = PROJECT_MAPPING.values.uniq
attr_reader :creation_counter, :update_counter
def initialize
@creation_counter = {}
@update_counter = {}
end
def up
setup
destroy_tenants
safety_check
copy_accounts_et_al
copy_buckets_et_al
copy_bubbles_et_al
copy_filters
copy_active_storage
cross_check
update_action_text_urls
pp [ "updated:", update_counter ]
hardlink_active_storage
end
def setup
ApplicationRecord.connects_to shards: { untenanted: { reading: :primary_original } }
end
def with_original_db(&block)
ApplicationRecord.connected_to(role: :reading, shard: :untenanted, &block)
end
def safety_check
buckets = with_original_db { Bucket.all.to_a }
unless buckets.map(&:id).sort == PROJECT_MAPPING.keys.sort
unknown_buckets = buckets.map(&:id) - PROJECT_MAPPING.keys
missing_buckets = PROJECT_MAPPING.keys - buckets.map(&:id)
raise "Surprising buckets. unknown #{unknown_buckets.inspect}, missing #{missing_buckets.inspect}"
end
end
def copy_accounts_et_al
account = with_original_db { Account.first }
users = with_original_db { User.all.to_a }
workflows = with_original_db { account.workflows.to_a }
workflow_stages = with_original_db { workflows.flat_map(&:stages) }
tags = with_original_db { account.tags.to_a }
TENANTS.each do |tenant|
ApplicationRecord.with_tenant(tenant) do
Account.create! name: tenant, id: account.id
upsert_all users
upsert_all workflows
upsert_all workflow_stages
upsert_all tags
end
end
end
def copy_buckets_et_al
PROJECT_MAPPING.each do |bucket_id, tenant|
bucket = with_original_db { Bucket.where(id: bucket_id).first }
accesses = with_original_db { bucket.accesses.to_a }
subscriptions = with_original_db { bucket.subscriptions.to_a }
ApplicationRecord.with_tenant(tenant) do
upsert_all bucket
upsert_all accesses
upsert_all subscriptions
end
end
end
def copy_bubbles_et_al
PROJECT_MAPPING.each do |bucket_id, tenant|
bubbles = with_original_db { Bucket.find(bucket_id).bubbles.to_a }
assignments = with_original_db { bubbles.flat_map(&:assignments) }
pops = with_original_db { bubbles.filter_map(&:pop) }
notifications = with_original_db { bubbles.flat_map(&:notifications) }
events = with_original_db { bubbles.flat_map(&:events) }
taggings = with_original_db { bubbles.flat_map(&:taggings) }
watches = with_original_db { bubbles.flat_map(&:watches) }
messages = with_original_db { bubbles.flat_map(&:messages) }
messageables = with_original_db { messages.filter_map(&:messageable) }
comments = messageables.select { _1.is_a?(Comment) }
markdowns = with_original_db { comments.map(&:markdown_body) }
reactions = with_original_db { comments.flat_map(&:reactions) }
ApplicationRecord.with_tenant(tenant) do
upsert_all bubbles
upsert_all assignments
upsert_all pops
upsert_all taggings
upsert_all watches
messageables.group_by(&:class).each do |klass, klass_messageables|
upsert_all klass_messageables
end
upsert_all markdowns
upsert_all reactions
upsert_all messages
upsert_all events
upsert_all notifications
end
end
end
def copy_filters
creation_counter["Filter"] = 0
TENANTS.each do |tenant|
buckets = ApplicationRecord.with_tenant(tenant) { Bucket.all.to_a }
filters = with_original_db do
Filter.all.filter_map do |filter| # hah no pun intended
next unless filter.buckets.empty? || buckets.any? { |b| filter.buckets.include?(b) }
filter.attributes.merge(filter.as_params)
end
end
ApplicationRecord.with_tenant(tenant) do
filters.each do |attr|
Filter.create! attr
end
end
creation_counter["Filter"] += filters.length
end
end
def copy_active_storage
attachments = with_original_db { ActiveStorage::Attachment.all.to_a }
blobs = with_original_db { ActiveStorage::Blob.all.to_a }
TENANTS.each do |tenant|
ApplicationRecord.with_tenant(tenant) do
upsert_all blobs
upsert_all attachments
end
end
end
def destroy_tenants
TENANTS.each do |tenant|
ApplicationRecord.destroy_tenant(tenant)
end
FileUtils.rm_rf "storage/tenants"
end
def cross_check
pp [ "created:", creation_counter ]
with_original_db do
assert_count User, User.count * TENANTS.length
assert_count Workflow, Workflow.count * TENANTS.length
assert_count Workflow::Stage, Workflow::Stage.count * TENANTS.length
assert_count Tag, Tag.count * TENANTS.length
assert_count Bucket, Bucket.count
assert_count Access, Access.count
assert_count Subscription, Subscription.count
assert_count Bubble, Bubble.count
assert_count Assignment, Assignment.count
assert_count Pop, Pop.count
assert_count EventSummary, EventSummary.count
assert_count Comment, Comment.count
assert_count Reaction, Reaction.count
assert_count Message, Message.count
assert_count Tagging, Tagging.count
assert_count Watch, Watch.count
assert_count Event, Event.count
assert_count Notification, Notification.count
assert_count ActiveStorage::Attachment, ActiveStorage::Attachment.count * TENANTS.length
assert_count ActiveStorage::Blob, ActiveStorage::Blob.count * TENANTS.length
raise "Filter count is off" unless creation_counter["Filter"] >= Filter.count
end
end
def update_action_text_urls
raise "No more markdown"
end
def hardlink_active_storage
files = Dir.glob("storage/files/*/*/*").map { _1.split("/")[2..].join("/") }
TENANTS.each do |tenant|
ApplicationRecord.with_tenant(tenant) do
destdir = ActiveStorage::Blob.service.root
files.each do |file|
FileUtils.mkdir_p File.join(destdir, File.dirname(file))
FileUtils.ln File.join("storage/files", file), File.join(destdir, file)
end
end
end
end
def upsert_all(originals)
originals = Array(originals)
return if originals.empty?
klass = originals.first.class
result = klass.upsert_all(originals.collect(&:attributes))
raise "Error upserting" unless result.rows.length == originals.length
creation_counter[klass.name] ||= 0
creation_counter[klass.name] += originals.length
nil
end
def assert_count(klass, expected)
actual = creation_counter[klass.name]
unless actual == expected
raise "#{klass} count is off: expected #{expected}, got #{actual}"
end
end
end
EnactTenanting.new.up
exit 0