mongoid 9.1.0 → 9.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. checksums.yaml +4 -4
  2. data/lib/config/locales/en.yml +69 -0
  3. data/lib/mongoid/association/accessors.rb +27 -5
  4. data/lib/mongoid/association/depending.rb +14 -10
  5. data/lib/mongoid/association/eager.rb +9 -0
  6. data/lib/mongoid/association/eager_load/discriminated_inclusion.rb +88 -0
  7. data/lib/mongoid/association/eager_load/embedded_distributor.rb +151 -0
  8. data/lib/mongoid/association/eager_load/inclusion.rb +173 -0
  9. data/lib/mongoid/association/eager_load/inclusion_tree.rb +96 -0
  10. data/lib/mongoid/association/eager_load/lookup_pipeline.rb +121 -0
  11. data/lib/mongoid/association/eager_load/polymorphic_preloader.rb +61 -0
  12. data/lib/mongoid/association/eager_load/polymorphic_targets.rb +151 -0
  13. data/lib/mongoid/association/eager_loadable.rb +65 -101
  14. data/lib/mongoid/association/embedded/batchable.rb +17 -1
  15. data/lib/mongoid/association/embedded/embeds_many/proxy.rb +6 -1
  16. data/lib/mongoid/association/embedded/embeds_one.rb +1 -0
  17. data/lib/mongoid/association/macros.rb +13 -1
  18. data/lib/mongoid/association/nested/many.rb +34 -5
  19. data/lib/mongoid/association/nested/nested_buildable.rb +14 -0
  20. data/lib/mongoid/association/nested/one.rb +6 -1
  21. data/lib/mongoid/association/options.rb +17 -0
  22. data/lib/mongoid/association/referenced/belongs_to/eager.rb +6 -1
  23. data/lib/mongoid/association/referenced/belongs_to.rb +1 -0
  24. data/lib/mongoid/association/referenced/counter_cache.rb +3 -3
  25. data/lib/mongoid/association/referenced/has_and_belongs_to_many/buildable.rb +6 -0
  26. data/lib/mongoid/association/referenced/has_and_belongs_to_many.rb +5 -0
  27. data/lib/mongoid/association/referenced/has_many/enumerable.rb +3 -1
  28. data/lib/mongoid/association/referenced/has_many/proxy.rb +100 -1
  29. data/lib/mongoid/association/referenced/has_many_through/eager.rb +99 -0
  30. data/lib/mongoid/association/referenced/has_many_through/proxy.rb +68 -0
  31. data/lib/mongoid/association/referenced/has_many_through.rb +201 -0
  32. data/lib/mongoid/association/referenced/has_one.rb +1 -0
  33. data/lib/mongoid/association/referenced/has_one_through/eager.rb +65 -0
  34. data/lib/mongoid/association/referenced/has_one_through/proxy.rb +30 -0
  35. data/lib/mongoid/association/referenced/has_one_through.rb +167 -0
  36. data/lib/mongoid/association/referenced.rb +2 -0
  37. data/lib/mongoid/association/relatable.rb +58 -0
  38. data/lib/mongoid/association.rb +15 -0
  39. data/lib/mongoid/atomic/modifiers.rb +100 -16
  40. data/lib/mongoid/clients/factory.rb +9 -13
  41. data/lib/mongoid/collection_configurable.rb +8 -0
  42. data/lib/mongoid/config/defaults.rb +0 -1
  43. data/lib/mongoid/config/encryption.rb +170 -22
  44. data/lib/mongoid/config.rb +56 -5
  45. data/lib/mongoid/contextual/aggregable/memory.rb +6 -2
  46. data/lib/mongoid/contextual/memory.rb +18 -7
  47. data/lib/mongoid/criteria/includable.rb +35 -11
  48. data/lib/mongoid/criteria/queryable/mergeable.rb +4 -0
  49. data/lib/mongoid/criteria/queryable/selectable.rb +123 -7
  50. data/lib/mongoid/deprecable.rb +45 -2
  51. data/lib/mongoid/document.rb +3 -0
  52. data/lib/mongoid/encryptable.rb +73 -0
  53. data/lib/mongoid/errors/config_redactor.rb +43 -0
  54. data/lib/mongoid/errors/in_memory_regexp_timeout.rb +26 -0
  55. data/lib/mongoid/errors/invalid_encryption_schema.rb +27 -0
  56. data/lib/mongoid/errors/mixed_client_configuration.rb +1 -1
  57. data/lib/mongoid/errors/no_client_database.rb +1 -1
  58. data/lib/mongoid/errors/no_client_hosts.rb +1 -1
  59. data/lib/mongoid/errors/no_encryption_schema.rb +30 -0
  60. data/lib/mongoid/errors/readonly_association.rb +29 -0
  61. data/lib/mongoid/errors.rb +5 -0
  62. data/lib/mongoid/extensions/vector.rb +64 -0
  63. data/lib/mongoid/extensions.rb +1 -0
  64. data/lib/mongoid/field_readable.rb +69 -0
  65. data/lib/mongoid/fields.rb +7 -0
  66. data/lib/mongoid/indexable.rb +38 -26
  67. data/lib/mongoid/matchable.rb +6 -1
  68. data/lib/mongoid/matcher/eq_impl_with_regexp.rb +2 -4
  69. data/lib/mongoid/matcher/regex.rb +11 -12
  70. data/lib/mongoid/matcher/regexp_budget.rb +372 -0
  71. data/lib/mongoid/matcher.rb +1 -0
  72. data/lib/mongoid/persistable/creatable.rb +120 -14
  73. data/lib/mongoid/persistable/updatable.rb +9 -11
  74. data/lib/mongoid/persistable.rb +15 -15
  75. data/lib/mongoid/persistence_context.rb +77 -0
  76. data/lib/mongoid/search_indexable.rb +69 -24
  77. data/lib/mongoid/serializable.rb +12 -4
  78. data/lib/mongoid/tasks/database.rb +33 -16
  79. data/lib/mongoid/threaded.rb +36 -0
  80. data/lib/mongoid/timestamps/timeless.rb +84 -10
  81. data/lib/mongoid/touchable.rb +28 -1
  82. data/lib/mongoid/traversable.rb +2 -2
  83. data/lib/mongoid/validatable/associated.rb +18 -2
  84. data/lib/mongoid/version.rb +1 -1
  85. data/lib/mongoid/warnings.rb +4 -0
  86. data/spec/integration/app_spec.rb +7 -1
  87. data/spec/integration/associations/belongs_to_spec.rb +13 -0
  88. data/spec/integration/associations/embedded_delayed_atomic_sets_spec.rb +216 -0
  89. data/spec/integration/associations/has_and_belongs_to_many_spec.rb +31 -2
  90. data/spec/integration/atomic/modifiers_spec.rb +283 -0
  91. data/spec/integration/dots_and_dollars_spec.rb +12 -2
  92. data/spec/integration/encryption_spec.rb +300 -0
  93. data/spec/integration/matcher_operator_data/regex.yml +21 -0
  94. data/spec/integration/matcher_regexp_timeout_spec.rb +215 -0
  95. data/spec/integration/query_operator_guard_spec.rb +89 -0
  96. data/spec/mongoid/association/accessors_spec.rb +35 -0
  97. data/spec/mongoid/association/eager_loadable_spec.rb +47 -0
  98. data/spec/mongoid/association/eager_spec.rb +2 -2
  99. data/spec/mongoid/association/embedded/embeds_many/proxy_spec.rb +33 -0
  100. data/spec/mongoid/association/fallback_spec.rb +425 -0
  101. data/spec/mongoid/association/macros_spec.rb +26 -0
  102. data/spec/mongoid/association/referenced/belongs_to/eager_spec.rb +17 -0
  103. data/spec/mongoid/association/referenced/has_and_belongs_to_many/buildable_spec.rb +11 -0
  104. data/spec/mongoid/association/referenced/has_and_belongs_to_many/proxy_spec.rb +48 -0
  105. data/spec/mongoid/association/referenced/has_and_belongs_to_many_spec.rb +50 -0
  106. data/spec/mongoid/association/referenced/has_many/eager_spec.rb +33 -0
  107. data/spec/mongoid/association/referenced/has_many/proxy_spec.rb +145 -0
  108. data/spec/mongoid/association/referenced/has_many_through/eager_spec.rb +58 -0
  109. data/spec/mongoid/association/referenced/has_many_through/proxy_spec.rb +62 -0
  110. data/spec/mongoid/association/referenced/has_many_through_spec.rb +399 -0
  111. data/spec/mongoid/association/referenced/has_one_through/eager_spec.rb +58 -0
  112. data/spec/mongoid/association/referenced/has_one_through/proxy_spec.rb +49 -0
  113. data/spec/mongoid/association/referenced/has_one_through_spec.rb +211 -0
  114. data/spec/mongoid/atomic/modifiers_spec.rb +347 -0
  115. data/spec/mongoid/attributes/nested_spec.rb +278 -8
  116. data/spec/mongoid/clients/factory_spec.rb +36 -0
  117. data/spec/mongoid/config/defaults_spec.rb +33 -0
  118. data/spec/mongoid/config/encryption_spec.rb +451 -0
  119. data/spec/mongoid/contextual/aggregable/memory_spec.rb +91 -0
  120. data/spec/mongoid/contextual/memory_spec.rb +177 -0
  121. data/spec/mongoid/criteria/includable_spec.rb +1221 -0
  122. data/spec/mongoid/criteria/queryable/selectable_logical_spec.rb +15 -1
  123. data/spec/mongoid/criteria/queryable/selectable_where_spec.rb +201 -0
  124. data/spec/mongoid/criteria_spec.rb +3 -0
  125. data/spec/mongoid/encryptable_spec.rb +115 -0
  126. data/spec/mongoid/errors/config_redactor_spec.rb +112 -0
  127. data/spec/mongoid/errors/mixed_client_configuration_spec.rb +31 -0
  128. data/spec/mongoid/errors/no_client_database_spec.rb +29 -0
  129. data/spec/mongoid/errors/no_client_hosts_spec.rb +26 -0
  130. data/spec/mongoid/errors/readonly_association_spec.rb +34 -0
  131. data/spec/mongoid/extensions/vector_spec.rb +175 -0
  132. data/spec/mongoid/fields_spec.rb +8 -0
  133. data/spec/mongoid/matcher/regexp_budget_spec.rb +570 -0
  134. data/spec/mongoid/search_indexable_spec.rb +321 -9
  135. data/spec/mongoid/tasks/database_spec.rb +31 -0
  136. data/spec/mongoid/timestamps/timeless_spec.rb +197 -5
  137. data/spec/mongoid/touchable_spec.rb +242 -0
  138. data/spec/mongoid/touchable_spec_models.rb +47 -0
  139. data/spec/mongoid/validatable/associated_spec.rb +19 -0
  140. data/spec/spec_helper.rb +5 -1
  141. data/spec/support/crypt/models.rb +253 -0
  142. data/spec/support/models/modifier_models.rb +65 -0
  143. metadata +55 -2
@@ -1400,6 +1400,46 @@ describe Mongoid::Touchable do
1400
1400
  end
1401
1401
  end
1402
1402
 
1403
+ describe 'the parent chain touch paths' do
1404
+ context 'on a root document' do
1405
+ it 'contains the root updated_at path' do
1406
+ expect(TouchableParent.new._touchable_chain_paths).to eq(%w[updated_at])
1407
+ end
1408
+ end
1409
+
1410
+ context 'on an embedded document with a touchable parent' do
1411
+ let(:parent) do
1412
+ parent = TouchableParent.create!
1413
+ parent.child = TouchableChild.create!(parent: parent)
1414
+ parent
1415
+ end
1416
+
1417
+ it 'contains the paths of each node in the chain' do
1418
+ expect(parent.child._touchable_chain_paths)
1419
+ .to eq(%w[child.updated_at updated_at])
1420
+ end
1421
+ end
1422
+
1423
+ context 'with a custom touch field' do
1424
+ it 'contains the custom field path of the anchor document' do
1425
+ building = TouchableSpec::Embedded::Building.create!
1426
+
1427
+ expect(building._touchable_chain_paths(:last_used_at))
1428
+ .to eq(%w[updated_at last_used_at])
1429
+ end
1430
+ end
1431
+
1432
+ context 'when the models use Timestamps::Short' do
1433
+ it 'resolves the aliased timestamp fields' do
1434
+ building = TouchableSpec::Short::Building.create!
1435
+ building.floors.create!
1436
+
1437
+ expect(building.reload.floors.first._touchable_chain_paths)
1438
+ .to eq(%w[floors.0.u_at u_at])
1439
+ end
1440
+ end
1441
+ end
1442
+
1403
1443
  describe 'touch merged with embedded insert' do
1404
1444
  context 'when pushing an embedded document with touch: true' do
1405
1445
  let(:building) do
@@ -1424,6 +1464,21 @@ describe Mongoid::Touchable do
1424
1464
  end
1425
1465
  end
1426
1466
 
1467
+ it 'sends the merged touch updates rewritten to the positional operator' do
1468
+ subscriber = Mrss::EventSubscriber.new
1469
+ Mongoid.client(:default).subscribe(Mongo::Monitoring::COMMAND, subscriber)
1470
+ begin
1471
+ floor.sofas.push(TouchableSpec::Embedded::Sofa.new)
1472
+ ensure
1473
+ Mongoid.client(:default).unsubscribe(Mongo::Monitoring::COMMAND, subscriber)
1474
+ end
1475
+
1476
+ command = subscriber.single_command_started_event('update').command
1477
+ sets = command['updates'].first['u']['$set']
1478
+
1479
+ expect(sets.keys).to contain_exactly('floors.$.updated_at', 'updated_at')
1480
+ end
1481
+
1427
1482
  it 'updates updated_at on the parent after push' do
1428
1483
  original_floor_updated_at = floor.updated_at
1429
1484
  original_building_updated_at = building.updated_at
@@ -1511,5 +1566,192 @@ describe Mongoid::Touchable do
1511
1566
  expect(building.updated_at).to eq(original_building_updated_at)
1512
1567
  end
1513
1568
  end
1569
+
1570
+ context 'when creating an embeds_one document with touch: true (regression for #6128)' do
1571
+ let(:building) do
1572
+ TouchableSpec::Embedded::Building.create!(title: 'Tower')
1573
+ end
1574
+
1575
+ it 'persists the embedded document fields after create_<assoc>' do
1576
+ building.create_lobby(name: 'Main Lobby')
1577
+
1578
+ reloaded = TouchableSpec::Embedded::Building.find(building.id)
1579
+ expect(reloaded.lobby).not_to be_nil
1580
+ expect(reloaded.lobby.name).to eq('Main Lobby')
1581
+ end
1582
+
1583
+ it 'updates the parent updated_at after create_<assoc>' do
1584
+ original_updated_at = building.updated_at
1585
+
1586
+ Timecop.travel(Time.now + 10) do
1587
+ building.create_lobby(name: 'Main Lobby')
1588
+ end
1589
+
1590
+ building.reload
1591
+ expect(building.updated_at).to be > original_updated_at
1592
+ end
1593
+ end
1594
+
1595
+ context 'when an existing child of the same array has a pending touch' do
1596
+ let(:building) do
1597
+ TouchableSpec::Embedded::Building.create!(title: 'Tower')
1598
+ end
1599
+
1600
+ before do
1601
+ floor = building.floors.create!(level: 1)
1602
+ # Leave a pending touch on the existing sibling so merging it into
1603
+ # the insert would target a path inside the pushed array.
1604
+ floor.updated_at = Time.now + 60
1605
+ end
1606
+
1607
+ it 'does not raise a conflict error when creating a sibling' do
1608
+ expect { building.floors.create!(level: 2) }.not_to raise_error
1609
+ end
1610
+
1611
+ it 'persists the new sibling' do
1612
+ building.floors.create!(level: 2)
1613
+ expect(building.reload.floors.length).to eq(2)
1614
+ end
1615
+
1616
+ it 'touches the parent' do
1617
+ building.floors.create!(level: 2)
1618
+ building.reload
1619
+ expect(building.updated_at).to be_within(5).of(Time.now)
1620
+ end
1621
+ end
1622
+
1623
+ context 'when a sibling in a parent array has a pending touch' do
1624
+ let(:building) do
1625
+ TouchableSpec::Embedded::Building.create!(title: 'Tower')
1626
+ end
1627
+
1628
+ let(:first_floor) do
1629
+ building.floors.create!(level: 1)
1630
+ end
1631
+
1632
+ let(:second_floor) do
1633
+ building.floors.create!(level: 2)
1634
+ end
1635
+
1636
+ let(:pending_touch_time) do
1637
+ Time.now + 60
1638
+ end
1639
+
1640
+ before do
1641
+ first_floor
1642
+ # Leave a pending touch on the other floor, at a different index of
1643
+ # the same array. Rewriting both that touch and the insert's own
1644
+ # touch to the positional operator collapses them onto one path and
1645
+ # touches the wrong floor.
1646
+ second_floor.updated_at = pending_touch_time
1647
+ end
1648
+
1649
+ it 'does not raise when creating a grandchild under the other floor' do
1650
+ expect { first_floor.sofas.create! }.not_to raise_error
1651
+ end
1652
+
1653
+ it 'touches the floor the grandchild was inserted under' do
1654
+ first_floor.sofas.create!
1655
+ building.reload
1656
+ expect(building.floors[0].updated_at).to be_within(5).of(Time.now)
1657
+ end
1658
+
1659
+ it 'preserves the pending touch on the sibling floor' do
1660
+ first_floor.sofas.create!
1661
+ building.reload
1662
+ expect(building.floors[1].updated_at.to_f)
1663
+ .to be_within(1).of(pending_touch_time.to_f)
1664
+ end
1665
+ end
1666
+
1667
+ context 'when the parent touch option names a custom field' do
1668
+ let(:building) do
1669
+ TouchableSpec::Embedded::Building.create!(title: 'Tower')
1670
+ end
1671
+
1672
+ it 'writes the custom touch field when creating an embedded child' do
1673
+ # Materialize the parent before the freeze so its updated_at differs
1674
+ # from the touch time, forcing the touch updates to merge into the
1675
+ # child's insert.
1676
+ building
1677
+ Timecop.freeze(Time.utc(2026, 1, 2, 3, 4, 5)) do
1678
+ building.waiting_rooms.create!
1679
+ building.reload
1680
+ expect(building.last_used_at).not_to be_nil
1681
+ expect(building.last_used_at.to_f)
1682
+ .to be_within(1).of(Time.utc(2026, 1, 2, 3, 4, 5).to_f)
1683
+ end
1684
+ end
1685
+ end
1686
+
1687
+ context 'when the models use Timestamps::Short' do
1688
+ let(:building) do
1689
+ TouchableSpec::Short::Building.create!
1690
+ end
1691
+
1692
+ let(:floor) do
1693
+ building.floors.create!
1694
+ end
1695
+
1696
+ before do
1697
+ building
1698
+ floor
1699
+ end
1700
+
1701
+ it 'merges the touch updates into the insert round-trip' do
1702
+ expect_query(1) do
1703
+ floor.gadgets.push(TouchableSpec::Short::Gadget.new)
1704
+ end
1705
+ end
1706
+
1707
+ it 'touches the parent chain through the aliased timestamp fields' do
1708
+ original_building_updated_at = building.u_at
1709
+ original_floor_updated_at = floor.u_at
1710
+
1711
+ Timecop.travel(Time.now + 10) do
1712
+ floor.gadgets.push(TouchableSpec::Short::Gadget.new)
1713
+ end
1714
+
1715
+ building.reload
1716
+ expect(building.u_at).to be > original_building_updated_at
1717
+ expect(building.floors.first.u_at).to be > original_floor_updated_at
1718
+ end
1719
+ end
1720
+
1721
+ context 'when a sibling has a pending touch and the touch callback does not run' do
1722
+ let(:building) do
1723
+ TouchableSpec::Embedded::Building.create!(title: 'Tower')
1724
+ end
1725
+
1726
+ let(:first_floor) do
1727
+ building.floors.create!(level: 1)
1728
+ end
1729
+
1730
+ let(:second_floor) do
1731
+ building.floors.create!(level: 2)
1732
+ end
1733
+
1734
+ let(:pending_touch_time) do
1735
+ Time.now + 60
1736
+ end
1737
+
1738
+ before do
1739
+ first_floor
1740
+ # Leave a pending touch on the other floor: rewriting it and this
1741
+ # chain's own touch would collapse both onto the same positional
1742
+ # path, so the touch updates cannot be merged into the insert.
1743
+ second_floor.updated_at = pending_touch_time
1744
+ end
1745
+
1746
+ it 'persists the deferred touches at insert time' do
1747
+ Mongoid::Touchable.suppress_touch_callbacks(TouchableSpec::Embedded::Sofa.name) do
1748
+ first_floor.sofas.push(TouchableSpec::Embedded::Sofa.new)
1749
+ end
1750
+
1751
+ building.reload
1752
+ expect(building.floors[1].updated_at.to_f)
1753
+ .to be_within(1).of(pending_touch_time.to_f)
1754
+ end
1755
+ end
1514
1756
  end
1515
1757
  end
@@ -7,9 +7,21 @@ module TouchableSpec
7
7
  include Mongoid::Timestamps
8
8
 
9
9
  field :title, type: String
10
+ field :last_used_at, type: Time
10
11
 
11
12
  embeds_many :entrances, class_name: 'TouchableSpec::Embedded::Entrance'
12
13
  embeds_many :floors, class_name: 'TouchableSpec::Embedded::Floor'
14
+ embeds_one :lobby, class_name: 'TouchableSpec::Embedded::Lobby'
15
+ embeds_many :waiting_rooms, class_name: 'TouchableSpec::Embedded::WaitingRoom'
16
+ end
17
+
18
+ class Lobby
19
+ include Mongoid::Document
20
+ include Mongoid::Timestamps
21
+
22
+ field :name, type: String
23
+
24
+ embedded_in :building, touch: true, class_name: 'TouchableSpec::Embedded::Building'
13
25
  end
14
26
 
15
27
  class Entrance
@@ -65,6 +77,41 @@ module TouchableSpec
65
77
 
66
78
  embedded_in :floor, touch: true, class_name: 'TouchableSpec::Embedded::Floor'
67
79
  end
80
+
81
+ class WaitingRoom
82
+ include Mongoid::Document
83
+ include Mongoid::Timestamps
84
+
85
+ field :name, type: String
86
+
87
+ embedded_in :building, touch: :last_used_at,
88
+ class_name: 'TouchableSpec::Embedded::Building'
89
+ end
90
+ end
91
+
92
+ module Short
93
+ class Building
94
+ include Mongoid::Document
95
+ include Mongoid::Timestamps::Short
96
+
97
+ embeds_many :floors, class_name: 'TouchableSpec::Short::Floor'
98
+ end
99
+
100
+ class Floor
101
+ include Mongoid::Document
102
+ include Mongoid::Timestamps::Short
103
+
104
+ embedded_in :building, touch: true, class_name: 'TouchableSpec::Short::Building'
105
+
106
+ embeds_many :gadgets, class_name: 'TouchableSpec::Short::Gadget'
107
+ end
108
+
109
+ class Gadget
110
+ include Mongoid::Document
111
+ include Mongoid::Timestamps::Short
112
+
113
+ embedded_in :floor, touch: true, class_name: 'TouchableSpec::Short::Floor'
114
+ end
68
115
  end
69
116
 
70
117
  module Referenced
@@ -82,6 +82,25 @@ describe Mongoid::Validatable::AssociatedValidator do
82
82
  expect(user).to be_valid
83
83
  end
84
84
  end
85
+
86
+ context 'when a child document has been destroyed' do
87
+ let(:user) do
88
+ User.new(name: 'test')
89
+ end
90
+
91
+ let(:description) do
92
+ Description.new
93
+ end
94
+
95
+ before do
96
+ user.descriptions << description
97
+ description.destroyed = true
98
+ end
99
+
100
+ it 'does not run validation on the destroyed child' do
101
+ expect(user).to be_valid
102
+ end
103
+ end
85
104
  end
86
105
  end
87
106
 
data/spec/spec_helper.rb CHANGED
@@ -101,7 +101,11 @@ CONFIG = {
101
101
  heartbeat_frequency: 180,
102
102
  user: SpecConfig.instance.uri.client_options[:user] || MONGOID_ROOT_USER.name,
103
103
  password: SpecConfig.instance.uri.client_options[:password] || MONGOID_ROOT_USER.password,
104
- auth_source: Mongo::Database::ADMIN
104
+ auth_source: Mongo::Database::ADMIN,
105
+
106
+ # so that we can run an Atlas cluster locally, and run tests with Atlas
107
+ # support enabled.
108
+ direct_connection: SpecConfig.instance.uri.uri_options[:direct_connection]
105
109
  )
106
110
  }
107
111
  },
@@ -46,4 +46,257 @@ module Crypt
46
46
  field :vin, type: String, encrypt: true
47
47
  field :make, type: String
48
48
  end
49
+
50
+ # An encrypted model whose database name is a callable resolving to a
51
+ # constant. The callable form of :database is supported by store_in, but the
52
+ # encryption schema map is keyed by namespace and built once, so this model's
53
+ # namespace is not known when the map is generated.
54
+ class DynamicCar
55
+ include Mongoid::Document
56
+
57
+ store_in database: -> { 'vehicles_dynamic' }
58
+
59
+ encrypt_with key_id: 'grolrnFVSSW9Gq04Q87R9Q==', deterministic: true
60
+
61
+ field :vin, type: String, encrypt: true
62
+ end
63
+
64
+ # An encrypted model using the documented multi-tenant idiom, where the
65
+ # database name is only known once a tenant is selected. There is no correct
66
+ # value to resolve at client construction time.
67
+ class TenantCar
68
+ include Mongoid::Document
69
+
70
+ # The fallback keeps this model usable by tasks that iterate every model,
71
+ # such as Mongoid::Tasks::Database.create_collections. Mongoid raises
72
+ # NoMethodError when a callable :database resolves to nil.
73
+ store_in database: -> { Thread.current[:tenant_database] || 'vehicles_no_tenant' }
74
+
75
+ encrypt_with key_id: 'grolrnFVSSW9Gq04Q87R9Q==', deterministic: true
76
+
77
+ field :vin, type: String, encrypt: true
78
+ end
79
+
80
+ # An encrypted model that is embedded by more than one parent, and more than
81
+ # once by the same parent.
82
+ class Token
83
+ include Mongoid::Document
84
+
85
+ field :value, type: String, encrypt: { deterministic: true }
86
+
87
+ embedded_in :tokenized, polymorphic: true
88
+ end
89
+
90
+ # A parent whose encrypted data lives entirely in an embedded model: it has
91
+ # no encrypted field of its own and no encrypt_with.
92
+ class Wallet
93
+ include Mongoid::Document
94
+
95
+ embeds_one :token, class_name: 'Crypt::Token', as: :tokenized
96
+ end
97
+
98
+ # Two parents embedding the same encrypted model.
99
+ class Vault
100
+ include Mongoid::Document
101
+
102
+ encrypt_with key_id: 'grolrnFVSSW9Gq04Q87R9Q=='
103
+
104
+ embeds_one :token, class_name: 'Crypt::Token', as: :tokenized
105
+ end
106
+
107
+ class Chest
108
+ include Mongoid::Document
109
+
110
+ encrypt_with key_id: 'grolrnFVSSW9Gq04Q87R9Q=='
111
+
112
+ embeds_one :token, class_name: 'Crypt::Token', as: :tokenized
113
+ end
114
+
115
+ # A parent embedding the same encrypted model through two relations.
116
+ class Ledger
117
+ include Mongoid::Document
118
+
119
+ encrypt_with key_id: 'grolrnFVSSW9Gq04Q87R9Q=='
120
+
121
+ embeds_one :primary_token, class_name: 'Crypt::Token', as: :tokenized
122
+ embeds_one :backup_token, class_name: 'Crypt::Token', as: :tokenized
123
+ end
124
+
125
+ # Three levels of nesting, where the middle level has no encrypted field of
126
+ # its own.
127
+ class Owner
128
+ include Mongoid::Document
129
+
130
+ encrypt_with key_id: 'grolrnFVSSW9Gq04Q87R9Q=='
131
+
132
+ embeds_one :account, class_name: 'Crypt::Account'
133
+ end
134
+
135
+ class Account
136
+ include Mongoid::Document
137
+
138
+ embedded_in :owner, class_name: 'Crypt::Owner'
139
+ embeds_one :credential, class_name: 'Crypt::Credential'
140
+ end
141
+
142
+ class Credential
143
+ include Mongoid::Document
144
+
145
+ field :secret, type: String, encrypt: { deterministic: true }
146
+
147
+ embedded_in :account, class_name: 'Crypt::Account'
148
+ end
149
+
150
+ # A model that embeds itself. The walk over embedded relations has to stop
151
+ # here, or generating the map never terminates.
152
+ class Comment
153
+ include Mongoid::Document
154
+
155
+ field :body, type: String, encrypt: { deterministic: true }
156
+
157
+ embedded_in :commentable, polymorphic: true
158
+ embeds_one :reply, class_name: 'Crypt::Comment', as: :commentable
159
+ end
160
+
161
+ class Article
162
+ include Mongoid::Document
163
+
164
+ encrypt_with key_id: 'grolrnFVSSW9Gq04Q87R9Q=='
165
+
166
+ embeds_one :comment, class_name: 'Crypt::Comment', as: :commentable
167
+ end
168
+
169
+ # A parent with no encrypted field of its own: the encrypted field lives on
170
+ # the embedded model. Used by the integration specs, so the embedded model
171
+ # carries a key id.
172
+ class Folder
173
+ include Mongoid::Document
174
+
175
+ embeds_one :note, class_name: 'Crypt::Note'
176
+ end
177
+
178
+ class Note
179
+ include Mongoid::Document
180
+
181
+ encrypt_with key_id: 'grolrnFVSSW9Gq04Q87R9Q==', deterministic: true
182
+
183
+ field :text, type: String, encrypt: true
184
+
185
+ embedded_in :folder, class_name: 'Crypt::Folder'
186
+ end
187
+
188
+ # A model naming an embedded class that is never defined. The association
189
+ # cannot be used, so nothing is ever embedded through it, but every model in
190
+ # the application is walked when the schema map is generated.
191
+ class Drawer
192
+ include Mongoid::Document
193
+
194
+ embeds_one :missing_note, class_name: 'Crypt::MissingNote'
195
+ end
196
+
197
+ # The same, on a model that is encrypted itself, so the walk descends into
198
+ # its relations.
199
+ class Cabinet
200
+ include Mongoid::Document
201
+
202
+ field :label, type: String, encrypt: { deterministic: true }
203
+
204
+ embeds_one :missing_note, class_name: 'Crypt::MissingNote'
205
+ end
206
+
207
+ # Two unrelated models stored in one collection, each with its own encrypted
208
+ # field.
209
+ class Badge
210
+ include Mongoid::Document
211
+
212
+ store_in collection: 'crypt_badges'
213
+
214
+ encrypt_with key_id: 'grolrnFVSSW9Gq04Q87R9Q==', deterministic: true
215
+
216
+ field :serial, type: String, encrypt: true
217
+ end
218
+
219
+ class BadgeScan
220
+ include Mongoid::Document
221
+
222
+ store_in collection: 'crypt_badges'
223
+
224
+ encrypt_with key_id: 'grolrnFVSSW9Gq04Q87R9Q==', deterministic: true
225
+
226
+ field :holder, type: String, encrypt: true
227
+ end
228
+
229
+ # Single collection inheritance. Only Member is registered in
230
+ # Mongoid.models; the subclasses are not.
231
+ class Member
232
+ include Mongoid::Document
233
+
234
+ encrypt_with key_id: 'grolrnFVSSW9Gq04Q87R9Q==', deterministic: true
235
+
236
+ field :ssn, type: String, encrypt: true
237
+ end
238
+
239
+ class Employee < Member
240
+ field :salary, type: String, encrypt: true
241
+ end
242
+
243
+ class Contractor < Member
244
+ field :rate, type: String, encrypt: true
245
+ end
246
+
247
+ # Single collection inheritance where only the subclass is encrypted.
248
+ class Listing
249
+ include Mongoid::Document
250
+
251
+ field :title, type: String
252
+ end
253
+
254
+ class PrivateListing < Listing
255
+ encrypt_with key_id: 'grolrnFVSSW9Gq04Q87R9Q==', deterministic: true
256
+
257
+ field :owner_ssn, type: String, encrypt: true
258
+ end
259
+
260
+ # An embeds_one target whose subclass adds an encrypted field.
261
+ class Box
262
+ include Mongoid::Document
263
+
264
+ encrypt_with key_id: 'grolrnFVSSW9Gq04Q87R9Q==', deterministic: true
265
+
266
+ embeds_one :item, class_name: 'Crypt::Item'
267
+ end
268
+
269
+ class Item
270
+ include Mongoid::Document
271
+
272
+ field :label, type: String, encrypt: true
273
+
274
+ embedded_in :box, class_name: 'Crypt::Box'
275
+ end
276
+
277
+ class SealedItem < Item
278
+ field :seal, type: String, encrypt: true
279
+ end
280
+
281
+ # An embeds_one target with no encrypted field, whose subclass has one.
282
+ # Nothing on Locker or Content declares encryption.
283
+ class Locker
284
+ include Mongoid::Document
285
+
286
+ embeds_one :content, class_name: 'Crypt::Content'
287
+ end
288
+
289
+ class Content
290
+ include Mongoid::Document
291
+
292
+ field :label, type: String
293
+
294
+ embedded_in :locker, class_name: 'Crypt::Locker'
295
+ end
296
+
297
+ class SecretContent < Content
298
+ encrypt_with key_id: 'grolrnFVSSW9Gq04Q87R9Q==', deterministic: true
299
+
300
+ field :code, type: String, encrypt: true
301
+ end
49
302
  end
@@ -22,4 +22,69 @@ module ModifierModels
22
22
  embedded_in :book, class_name: 'ModifierModels::Book'
23
23
  field :text, type: String
24
24
  end
25
+
26
+ class Roster
27
+ include Mongoid::Document
28
+
29
+ embeds_many :players, class_name: 'ModifierModels::Player'
30
+ accepts_nested_attributes_for :players, allow_destroy: true
31
+ end
32
+
33
+ class Player
34
+ include Mongoid::Document
35
+
36
+ embedded_in :roster, class_name: 'ModifierModels::Roster'
37
+ embeds_one :medal, class_name: 'ModifierModels::Medal'
38
+ has_and_belongs_to_many :badges, inverse_of: nil, class_name: 'ModifierModels::Badge'
39
+ embeds_many :awards, class_name: 'ModifierModels::Award'
40
+ accepts_nested_attributes_for :awards, allow_destroy: true
41
+ field :nickname, type: String
42
+ end
43
+
44
+ class Award
45
+ include Mongoid::Document
46
+
47
+ embedded_in :player, class_name: 'ModifierModels::Player'
48
+ has_and_belongs_to_many :badges, inverse_of: nil, class_name: 'ModifierModels::Badge'
49
+ field :name, type: String
50
+ end
51
+
52
+ class Medal
53
+ include Mongoid::Document
54
+
55
+ embedded_in :player, class_name: 'ModifierModels::Player'
56
+ end
57
+
58
+ class Badge
59
+ include Mongoid::Document
60
+ end
61
+
62
+ class Cabinet
63
+ include Mongoid::Document
64
+
65
+ embeds_one :drawer, class_name: 'ModifierModels::Drawer', autobuild: true
66
+ end
67
+
68
+ class Drawer
69
+ include Mongoid::Document
70
+
71
+ field :matcher, type: String
72
+ embedded_in :cabinet, class_name: 'ModifierModels::Cabinet'
73
+ embeds_many :items, class_name: 'ModifierModels::Item'
74
+ end
75
+
76
+ class Item
77
+ include Mongoid::Document
78
+
79
+ field :name, type: String
80
+ embedded_in :drawer, class_name: 'ModifierModels::Drawer'
81
+ embeds_many :widgets, class_name: 'ModifierModels::Widget'
82
+ end
83
+
84
+ class Widget
85
+ include Mongoid::Document
86
+
87
+ field :label, type: String
88
+ embedded_in :item, class_name: 'ModifierModels::Item'
89
+ end
25
90
  end