diff --git a/CLAUDE.md b/CLAUDE.md index 27fe7931..f6ea7157 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -44,7 +44,7 @@ CI (`.github/workflows/{backend,frontend,native}.yml`) uses path filters — bac The backend uses **both PostgreSQL and MongoDB simultaneously**, split by data type: - **PostgreSQL (ActiveRecord)** — relational/reference data: `User` (Devise auth), `Condition`, `Symptom`, `Treatment`, `Food`, `Tag`, `Profile`, `Weather`, and the `user_*` join tables. These models subclass `ActiveRecord::Base` and carry a `# == Schema Information` header. Schema lives in `db/schema.rb` + `db/structure.sql`; migrations in `db/migrate/`. -- **MongoDB (Mongoid 8)** — high-volume, user-generated, schemaless data: `Checkin` (the core daily symptom/treatment/tag log), `Comment`, `Reaction`, `Pattern`, `Notification`, `HarveyBradshawIndex`, `Feedback`, `PromotionRate`, `OracleRequest`. These `include Mongoid::Document`. Config in `config/mongoid.yml`. +- **MongoDB (Mongoid 9)** — high-volume, user-generated, schemaless data: `Checkin` (the core daily symptom/treatment/tag log), `Comment`, `Reaction`, `Pattern`, `Notification`, `HarveyBradshawIndex`, `Feedback`, `PromotionRate`, `OracleRequest`. These `include Mongoid::Document`. Config in `config/mongoid.yml`. The two stores are linked by an **encrypted foreign key**: Mongo documents store `encrypted_user_id` (symmetric-encryption gem, see `config/symmetric-encryption.yml`) rather than a plain `user_id`, and dereference it back to the Postgres `User`. When querying check-in data by user, filter on `encrypted_user_id`, not `user_id`. `Checkin` embeds condition/symptom/treatment sub-documents inline. diff --git a/backend/Gemfile b/backend/Gemfile index a90be923..faef920d 100644 --- a/backend/Gemfile +++ b/backend/Gemfile @@ -16,7 +16,7 @@ gem "sprockets-rails" gem "active_model_serializers", "0.9.8" # Use postgresql and mongo as the database for Active Record -gem "mongoid", "8.1.3" # https://www.mongodb.com/docs/mongoid/current/reference/compatibility/#rails-compatibility +gem "mongoid", "9.0.11" # https://www.mongodb.com/docs/mongoid/current/reference/compatibility/#rails-compatibility gem "pg" # Use Puma as the app server diff --git a/backend/Gemfile.lock b/backend/Gemfile.lock index 710394c6..098d1b82 100644 --- a/backend/Gemfile.lock +++ b/backend/Gemfile.lock @@ -106,11 +106,11 @@ GEM bigdecimal (4.1.2) brakeman (6.1.2) racc - bson (4.15.0) + bson (5.2.0) bugsnag (6.27.1) concurrent-ruby (~> 1.0) builder (3.3.0) - bullet (7.2.0) + bullet (8.2.0) activesupport (>= 3.0.0) uniform_notifier (~> 1.11) byebug (11.1.3) @@ -265,13 +265,14 @@ GEM minitest (6.0.6) drb (~> 2.0) prism (~> 1.5) - mongo (2.20.1) + mongo (2.25.0) + base64 bson (>= 4.14.1, < 6.0.0) - mongoid (8.1.3) - activemodel (>= 5.1, < 7.2, != 7.0.0) + mongoid (9.0.11) + activemodel (>= 5.1, < 8.2, != 7.0.0) concurrent-ruby (>= 1.0.5, < 2.0) mongo (>= 2.18.0, < 3.0.0) - ruby2_keywords (~> 0.0.5) + ostruct mongoid-compatibility (0.6.0) activesupport mongoid (>= 2.0) @@ -314,6 +315,7 @@ GEM oauth2 (~> 1.1) omniauth (~> 1.2) orm_adapter (0.5.0) + ostruct (0.6.3) parallel (1.24.0) parser (3.3.0.5) ast (~> 2.4.1) @@ -542,7 +544,7 @@ DEPENDENCIES kaminari-actionview kaminari-mongoid letter_opener - mongoid (= 8.1.3) + mongoid (= 9.0.11) mongoid-rspec nearest_time_zone omniauth (~> 1.8) diff --git a/backend/spec/models/checkin_spec.rb b/backend/spec/models/checkin_spec.rb index d91b72b6..8f5530d5 100644 --- a/backend/spec/models/checkin_spec.rb +++ b/backend/spec/models/checkin_spec.rb @@ -15,4 +15,24 @@ it { is_expected.to validate_presence_of(:date) } it { is_expected.to validate_presence_of(:encrypted_user_id) } end + + describe "encrypted_user_id" do + let(:user) { create(:user) } + subject(:checkin) { create(:checkin, user_id: user.id) } + + it "persists the user id encrypted rather than in the clear" do + expect(checkin.encrypted_user_id).to be_present + expect(checkin.encrypted_user_id).not_to eq user.id.to_s + end + + it "round-trips back to the Postgres user" do + expect(checkin.user_id).to eq user.id + expect(checkin.user).to eq user + end + + it "is queryable by the encrypted value" do + checkin + expect(Checkin.where(encrypted_user_id: user.encrypted_id).to_a).to eq [checkin] + end + end end