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:
@@ -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
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
Rails.application.config.after_initialize do
|
||||
%w[ db files ].each do |dir|
|
||||
Rails.root.join("storage", dir).mkpath
|
||||
end
|
||||
end
|
||||
Executable
+28
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user