better_auth-mongo-adapter 0.6.2 → 0.7.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c3abe1d68b1d2865e2f74cefbcd53ee67fd1f2384b20a20db6532d7fe3fc4c96
4
- data.tar.gz: 6e8b7e4e3ad10076e7cc2ad9f8b9982be4d355e64dcac381c6a7c6624e2ed369
3
+ metadata.gz: 8422b43a0fc6310519286bca03295aecb91752ae5c4cd5997ff5be64486af7c9
4
+ data.tar.gz: ed65ca011f7f4a5913ae03830c049b8fcc023200c47ad9da125e55f6569272ba
5
5
  SHA512:
6
- metadata.gz: 369ce690cbe825954bd2e716d32ec6dac751d18e766a4656e2d904e834e85f4fbab26a0b77dc939d7c6e52648aba18866adcf01cde4e9935a60583ca7ebd6a52
7
- data.tar.gz: 32e903a2de49c6b6718c280e7d185dbc4d6d43a22349cd960b3e219a4b6154efd402e6e3a1cc5493460d12dd9062083de005bf9a5ee4a00aabd67ef7535513fe
6
+ metadata.gz: 285006c97e41014ba65eb450e520bd9ac82848a0d796894ba526f1427a605808d51e25d174cd35d4f11884e235b4cc9465017a91c874b6aed5b6e45b41c8550a
7
+ data.tar.gz: 16029924074e406bb142a50936b4b77a1a90367c5aaf8a390ecfb8723f051a1e2c98c770bea4bab1daa7784c7a9763946c46b5a55c9fc3d5a5620dfc5e830d16
data/CHANGELOG.md CHANGED
@@ -2,6 +2,15 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.7.0 - 2026-05-05
6
+
7
+ - Added explicit `ensure_indexes!` setup helper for Mongo indexes derived from Better Auth schema metadata.
8
+ - Updated MongoDB setup docs to use the lambda adapter form, clearer standalone/replica-set transaction guidance, and production index guidance.
9
+ - Consolidated Mongo fake test support and strengthened transaction rollback coverage for staged mutations.
10
+ - Apply `advanced[:database][:default_find_many_limit]` to uncapped `find_many` calls and one-to-many Mongo `$lookup` joins, defaulting to 100 when omitted.
11
+ - Match upstream Mongo where-clause semantics for mixed connectors by bucketing multi-clause filters into `$and` and `$or` arrays instead of left-fold nesting.
12
+ - Allow scalar values for `in` and `not_in` filters as an intentional Ruby adapter-family adaptation.
13
+
5
14
  ## 0.1.1 - 2026-04-30
6
15
 
7
16
  - Fixed inferred limited joins so explicit relation and limit configuration is preserved.
data/README.md CHANGED
@@ -18,16 +18,85 @@ mongo_client = Mongo::Client.new(ENV.fetch("BETTER_AUTH_MONGODB_URL"))
18
18
 
19
19
  auth = BetterAuth.auth(
20
20
  secret: ENV.fetch("BETTER_AUTH_SECRET"),
21
- database: BetterAuth::Adapters::MongoDB.new(
22
- database: mongo_client.database,
23
- client: mongo_client,
24
- transaction: false
25
- )
21
+ database: ->(options) {
22
+ BetterAuth::Adapters::MongoDB.new(
23
+ options,
24
+ database: mongo_client.database,
25
+ client: mongo_client,
26
+ transaction: false
27
+ )
28
+ }
26
29
  )
27
30
  ```
28
31
 
32
+ The lambda form lets Better Auth pass the final configuration into the adapter,
33
+ including plugins, custom schemas, and advanced database options.
34
+
29
35
  ## Notes
30
36
 
31
37
  This package depends on the official `mongo` gem. Keeping MongoDB support outside `better_auth` avoids installing MongoDB client dependencies for applications that only use SQL, Rails, Hanami, or in-memory storage.
32
38
 
33
39
  The adapter stores Better Auth models in singular MongoDB collections by default, maps logical `id` values to Mongo `_id`, converts ObjectId-compatible ids through the Mongo driver, and supports the shared Better Auth database adapter contract.
40
+
41
+ Transactions are deployment-dependent. MongoDB multi-document transactions may
42
+ be unavailable on standalone servers and usually require a replica set plus
43
+ compatible driver/session settings. The setup example uses `transaction: false`;
44
+ enable transactions only when the MongoDB deployment supports them.
45
+
46
+ When using a replica set, remove `transaction: false` or pass
47
+ `transaction: true`. When using standalone local MongoDB, keep
48
+ `transaction: false`.
49
+
50
+ ## Indexes
51
+
52
+ MongoDB does not run SQL-style migrations. The adapter can create recommended
53
+ indexes from Better Auth schema metadata, but this is an explicit setup step:
54
+
55
+ ```ruby
56
+ adapter = BetterAuth::Adapters::MongoDB.new(
57
+ options,
58
+ database: mongo_client.database,
59
+ client: mongo_client,
60
+ transaction: false
61
+ )
62
+
63
+ adapter.ensure_indexes!
64
+ ```
65
+
66
+ `ensure_indexes!` creates indexes for schema fields marked `unique: true` or
67
+ `index: true`, including plugin schemas and custom model or field names. It
68
+ skips Mongo `_id` because MongoDB creates that index automatically. The method
69
+ returns a summary of requested indexes so deployment scripts can log what was
70
+ applied.
71
+
72
+ ## Limits
73
+
74
+ By default, `find_many` calls without an explicit `limit:` are capped at 100 records. Configure the default with Better Auth's advanced database option:
75
+
76
+ ```ruby
77
+ auth = BetterAuth.auth(
78
+ secret: ENV.fetch("BETTER_AUTH_SECRET"),
79
+ advanced: {
80
+ database: {
81
+ default_find_many_limit: 250
82
+ }
83
+ },
84
+ database: ->(options) {
85
+ BetterAuth::Adapters::MongoDB.new(
86
+ options,
87
+ database: mongo_client.database,
88
+ client: mongo_client,
89
+ transaction: false
90
+ )
91
+ }
92
+ )
93
+ ```
94
+
95
+ The same default applies to one-to-many join lookups when the join config does not set `limit:`. Passing an explicit `limit:` to `find_many` or to the join config overrides the default.
96
+
97
+ One-to-one joins ignore one-to-many limits. They are returned as a single object or `nil`.
98
+
99
+ Ruby's adapters accept scalar values for `in` and `not_in` filters and coerce
100
+ them to a one-element list. This is an intentional Ruby adapter-family behavior;
101
+ upstream's TypeScript adapter factory is stricter before the Mongo adapter sees
102
+ the query.
@@ -2,6 +2,6 @@
2
2
 
3
3
  module BetterAuth
4
4
  module MongoAdapter
5
- VERSION = "0.6.2"
5
+ VERSION = "0.7.0"
6
6
  end
7
7
  end
@@ -49,7 +49,8 @@ module BetterAuth
49
49
  pipeline << {"$project" => projection_for(model, select, join)} if select && !select.empty?
50
50
  pipeline << {"$sort" => {sort_field(model, sort_by) => sort_direction(sort_by)}} if sort_by
51
51
  pipeline << {"$skip" => offset.to_i} if offset
52
- pipeline << {"$limit" => limit.to_i} if limit
52
+ effective_limit = limit.nil? ? default_find_many_limit : limit.to_i
53
+ pipeline << {"$limit" => effective_limit} if effective_limit.positive?
53
54
 
54
55
  collection_for(model)
55
56
  .aggregate(pipeline, session_options)
@@ -105,6 +106,26 @@ module BetterAuth
105
106
  (row["total"] || row[:total] || 0).to_i
106
107
  end
107
108
 
109
+ def ensure_indexes!
110
+ Schema.auth_tables(options).flat_map do |model, table|
111
+ table.fetch(:fields).filter_map do |field, attributes|
112
+ next if field == "id"
113
+ next unless attributes[:unique] || attributes[:index]
114
+
115
+ collection = collection_for(model)
116
+ key = storage_field(model, field)
117
+ index_options = attributes[:unique] ? {unique: true} : {}
118
+ collection.indexes.create_one({key => 1}, index_options)
119
+ {
120
+ collection: collection_name(model),
121
+ field: field,
122
+ keys: {key => 1},
123
+ unique: attributes[:unique] == true
124
+ }
125
+ end
126
+ end
127
+ end
128
+
108
129
  def transaction
109
130
  return yield self unless client && @transaction_enabled && client.respond_to?(:start_session)
110
131
 
@@ -161,24 +182,40 @@ module BetterAuth
161
182
  clauses = Array(where)
162
183
  return {} if clauses.empty?
163
184
 
164
- clauses.each_with_index.reduce(nil) do |filter, (clause, index)|
165
- condition = condition_for(model, clause)
166
- next condition if index.zero?
167
-
168
- if fetch_key(clause, :connector).to_s.upcase == "OR"
169
- {"$or" => [filter, condition]}
185
+ conditions = clauses.map do |clause|
186
+ connector = if fetch_key(clause, :connector).to_s.upcase == "OR"
187
+ "OR"
170
188
  else
171
- {"$and" => [filter, condition]}
189
+ "AND"
172
190
  end
191
+ {condition: condition_for(model, clause), connector: connector}
173
192
  end
193
+ return conditions.first.fetch(:condition) if conditions.one?
194
+
195
+ result = {}
196
+ and_conditions = conditions.select { |entry| entry.fetch(:connector) == "AND" }.map { |entry| entry.fetch(:condition) }
197
+ or_conditions = conditions.select { |entry| entry.fetch(:connector) == "OR" }.map { |entry| entry.fetch(:condition) }
198
+ result["$and"] = and_conditions if and_conditions.any?
199
+ result["$or"] = or_conditions if or_conditions.any?
200
+ result
201
+ end
202
+
203
+ def default_find_many_limit
204
+ value = options.advanced.dig(:database, :default_find_many_limit)
205
+ return 100 if value.nil?
206
+
207
+ Integer(value)
208
+ rescue ArgumentError, TypeError
209
+ 100
210
+ end
211
+
212
+ def array_operator_values(value)
213
+ value.is_a?(Array) ? value : [value]
174
214
  end
175
215
 
176
216
  def condition_for(model, clause)
177
217
  operator = (fetch_key(clause, :operator) || "eq").to_s.downcase
178
218
  value = fetch_key(clause, :value)
179
- if operator == "in" && !value.is_a?(Array)
180
- raise MongoAdapterError.new("UNSUPPORTED_OPERATOR", "Value must be an array")
181
- end
182
219
 
183
220
  field = resolve_field(model, fetch_key(clause, :field))
184
221
  attributes = fields_for(model).fetch(field)
@@ -192,9 +229,9 @@ module BetterAuth
192
229
  when "eq"
193
230
  (insensitive && value.is_a?(String)) ? regex_condition(key, value, :eq, insensitive: true) : {key => store_value(field, value, attributes, strict_id: true)}
194
231
  when "in"
195
- (insensitive && value.is_a?(Array)) ? insensitive_in_condition(key, value) : {key => {"$in" => Array(value).map { |entry| store_value(field, entry, attributes, strict_id: true) }}}
232
+ (insensitive && value.is_a?(Array)) ? insensitive_in_condition(key, value) : {key => {"$in" => array_operator_values(value).map { |entry| store_value(field, entry, attributes, strict_id: true) }}}
196
233
  when "not_in"
197
- (insensitive && value.is_a?(Array)) ? insensitive_not_in_condition(key, value) : {key => {"$nin" => Array(value).map { |entry| store_value(field, entry, attributes, strict_id: true) }}}
234
+ (insensitive && value.is_a?(Array)) ? insensitive_not_in_condition(key, value) : {key => {"$nin" => array_operator_values(value).map { |entry| store_value(field, entry, attributes, strict_id: true) }}}
198
235
  when "ne"
199
236
  (insensitive && value.is_a?(String)) ? {key => {"$not" => regex_for(value, :eq, insensitive: true)}} : {key => {"$ne" => store_value(field, value, attributes, strict_id: true)}}
200
237
  when "gt", "gte", "lt", "lte"
@@ -242,9 +279,10 @@ module BetterAuth
242
279
  local_field = storage_field_for_join(model, config.fetch(:from))
243
280
  foreign_field = storage_field_for_join(join_model, config.fetch(:to))
244
281
  relation = config[:relation]
245
- limit = config[:limit]
282
+ limit = config.key?(:limit) ? config[:limit] : nil
283
+ effective_limit = limit.nil? ? default_find_many_limit : limit.to_i
246
284
  unique = relation == "one-to-one" || config[:unique]
247
- should_limit = !unique && limit && limit.to_i.positive?
285
+ should_limit = !unique && effective_limit.positive?
248
286
 
249
287
  lookup = if should_limit
250
288
  {
@@ -253,7 +291,7 @@ module BetterAuth
253
291
  "let" => {"localFieldValue" => "$#{local_field}"},
254
292
  "pipeline" => [
255
293
  {"$match" => {"$expr" => {"$eq" => ["$#{foreign_field}", "$$localFieldValue"]}}},
256
- {"$limit" => limit.to_i}
294
+ {"$limit" => effective_limit}
257
295
  ],
258
296
  "as" => join_model
259
297
  }
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: better_auth-mongo-adapter
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.2
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sebastian Sala