better_auth-mongodb 0.8.0 → 0.10.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: 4ed1669dc271afab7b9e15d5abd0fdd7aeb5228ad4b602eeda5438f8f07d5270
4
- data.tar.gz: a6efaca422c47a3e24ffc7c6b2bcf0d7633843b76b0db514da8588bee4233090
3
+ metadata.gz: 7f38f577d0bab0d49bd3fd44384f0869e6323abcbe1ecaf6096e1bfdab3ed45a
4
+ data.tar.gz: 7b4074915658cc13c7e0ae92f545a8a98fb50764647c153c168ed0bc9675218d
5
5
  SHA512:
6
- metadata.gz: bb69f0da9105d78d720bdfca09f5c4e2fe99c9ee60a7f0ac392c4eab1f7613da8dc845ab23926499ff00107d455d8aa3a20fe2984fa6737fe603688cfc7bdde1
7
- data.tar.gz: de14ab5fb5b7078d3803a5fed75c7415a195276b0dbde522845f5337e3194c0cac75534b49a66e794cf8ffec013d8d07926c3a0077594054848c56f2f79bf94c
6
+ metadata.gz: 1acce3252242ec0a1470496cb819e6a9e6027ddff9c03df2ecf38a106293c28322cbaeeeb05bcb80e81867e7e75966bb3fc275a821d2c20f9b8a7aab1380d94f
7
+ data.tar.gz: 1a85cb29c777b43dbf086339e087d2062153de6a945a1f5750abb95130bf826a9734d8206060f04e93cad6c51cce08430704226976e5e1438c3b00a5ae3aa5b0
data/CHANGELOG.md CHANGED
@@ -2,8 +2,15 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.10.0 - 2026-05-21
6
+
5
7
  - Rename the canonical Ruby gem to `better_auth-mongodb` while keeping
6
8
  `better_auth-mongo-adapter` as a deprecated compatibility package.
9
+ - Fixed `use_plural: true` so configured schema `model_name` values such as
10
+ `people` and `api_keys` are used directly instead of being pluralized again.
11
+ - Clarified MongoDB filter docs: `in` requires array values, while `not_in`
12
+ accepts scalar values as a Ruby adapter-family compatibility behavior.
13
+ - Improved MongoDB owner counting, nullable unique indexes, and adapter parity coverage.
7
14
 
8
15
  ## 0.7.0 - 2026-05-05
9
16
 
@@ -12,7 +19,7 @@
12
19
  - Consolidated Mongo fake test support and strengthened transaction rollback coverage for staged mutations.
13
20
  - Apply `advanced[:database][:default_find_many_limit]` to uncapped `find_many` calls and one-to-many Mongo `$lookup` joins, defaulting to 100 when omitted.
14
21
  - Match upstream Mongo where-clause semantics for mixed connectors by bucketing multi-clause filters into `$and` and `$or` arrays instead of left-fold nesting.
15
- - Allow scalar values for `in` and `not_in` filters as an intentional Ruby adapter-family adaptation.
22
+ - Allow scalar values for `not_in` filters as an intentional Ruby adapter-family adaptation while keeping `in` aligned with the shared adapter array contract.
16
23
 
17
24
  ## 0.1.1 - 2026-04-30
18
25
 
data/README.md CHANGED
@@ -94,12 +94,26 @@ auth = BetterAuth.auth(
94
94
 
95
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
96
 
97
+ Explicit `limit:` values must be positive integers. Explicit `offset:` values
98
+ must be zero or positive integers. Invalid configured defaults, including
99
+ non-positive `default_find_many_limit` values, fall back to the built-in cap of
100
+ 100 records.
101
+
97
102
  One-to-one joins ignore one-to-many limits. They are returned as a single object or `nil`.
98
103
 
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.
104
+ Ruby's MongoDB adapter matches upstream's adapter factory by requiring array
105
+ values for the `in` filter operator. The Ruby adapter still accepts scalar
106
+ values for `not_in` and coerces them to a one-element list, matching the Ruby
107
+ adapter-family behavior.
108
+
109
+ Update calls intentionally strip logical `id` / Mongo `_id` from `$set` payloads
110
+ so callers cannot mutate immutable Mongo identifiers. If an update contains no
111
+ caller-supplied schema fields after id and unknown fields are ignored, the
112
+ adapter raises `BAD_REQUEST` before calling MongoDB.
113
+
114
+ Default storage field names use Ruby's snake_case convention. For example, an
115
+ additional or plugin field named `camelCaseField` is stored as
116
+ `camel_case_field` unless the schema config provides an explicit `fieldName`.
103
117
 
104
118
  ## Compatibility
105
119
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module BetterAuth
4
4
  module MongoDB
5
- VERSION = "0.8.0"
5
+ VERSION = "0.10.0"
6
6
  end
7
7
  end
@@ -46,12 +46,12 @@ module BetterAuth
46
46
  def find_many(model:, where: [], sort_by: nil, limit: nil, offset: nil, select: nil, join: nil)
47
47
  model = model.to_s
48
48
  pipeline = [{"$match" => mongo_filter(model, where || [])}]
49
+ pipeline << {"$sort" => {sort_field(model, sort_by) => sort_direction(sort_by)}} if sort_by
50
+ pipeline << {"$skip" => non_negative_integer!(offset, "offset")} unless offset.nil?
51
+ effective_limit = limit.nil? ? default_find_many_limit : positive_integer!(limit, "limit")
52
+ pipeline << {"$limit" => effective_limit}
49
53
  pipeline.concat(join_stages(model, join)) if join
50
54
  pipeline << {"$project" => projection_for(model, select, join)} if select && !select.empty?
51
- pipeline << {"$sort" => {sort_field(model, sort_by) => sort_direction(sort_by)}} if sort_by
52
- pipeline << {"$skip" => offset.to_i} if offset
53
- effective_limit = limit.nil? ? default_find_many_limit : limit.to_i
54
- pipeline << {"$limit" => effective_limit} if effective_limit.positive?
55
55
 
56
56
  collection_for(model)
57
57
  .aggregate(pipeline, session_options)
@@ -61,9 +61,11 @@ module BetterAuth
61
61
 
62
62
  def update(model:, where:, update:)
63
63
  model = model.to_s
64
+ ensure_update_input_has_fields!(model, update)
64
65
  data = transform_input(model, update, "update", true)
65
66
  document = to_document(model, data)
66
67
  document.delete("_id")
68
+ ensure_update_document!(document)
67
69
  result = collection_for(model).find_one_and_update(
68
70
  mongo_filter(model, where || []),
69
71
  {"$set" => document},
@@ -75,9 +77,11 @@ module BetterAuth
75
77
 
76
78
  def update_many(model:, where:, update:)
77
79
  model = model.to_s
80
+ ensure_update_input_has_fields!(model, update)
78
81
  data = transform_input(model, update, "update", true)
79
82
  document = to_document(model, data)
80
83
  document.delete("_id")
84
+ ensure_update_document!(document)
81
85
  result = collection_for(model).update_many(
82
86
  mongo_filter(model, where || []),
83
87
  {"$set" => document},
@@ -115,8 +119,8 @@ module BetterAuth
115
119
 
116
120
  collection = collection_for(model)
117
121
  key = storage_field(model, field)
118
- index_options = attributes[:unique] ? {unique: true} : {}
119
- collection.indexes.create_one({key => 1}, index_options)
122
+ index_options = index_options_for(attributes)
123
+ create_index!(collection, {key => 1}, index_options)
120
124
  {
121
125
  collection: collection_name(model),
122
126
  field: field,
@@ -147,6 +151,34 @@ module BetterAuth
147
151
 
148
152
  private
149
153
 
154
+ def index_options_for(attributes)
155
+ return {} unless attributes[:unique]
156
+
157
+ options = {unique: true}
158
+ options[:sparse] = true unless attributes[:required]
159
+ options
160
+ end
161
+
162
+ def create_index!(collection, keys, options)
163
+ collection.indexes.create_one(keys, options)
164
+ rescue Mongo::Error::OperationFailure => error
165
+ raise unless index_options_conflict?(error) && collection.indexes.respond_to?(:drop_one)
166
+
167
+ collection.indexes.drop_one(default_index_name(keys))
168
+ collection.indexes.create_one(keys, options)
169
+ end
170
+
171
+ def index_options_conflict?(error)
172
+ error.message.include?("IndexOptionsConflict") ||
173
+ error.message.include?("IndexKeySpecsConflict") ||
174
+ error.message.include?("already exists with different options") ||
175
+ error.message.include?("same name as the requested index")
176
+ end
177
+
178
+ def default_index_name(keys)
179
+ keys.map { |field, direction| "#{field}_#{direction}" }.join("_")
180
+ end
181
+
150
182
  def transform_input(model, data, action, force_allow_id)
151
183
  fields = fields_for(model)
152
184
  input = stringify_keys(data)
@@ -180,7 +212,7 @@ module BetterAuth
180
212
  end
181
213
 
182
214
  def mongo_filter(model, where)
183
- clauses = Array(where)
215
+ clauses = validate_where!(where)
184
216
  return {} if clauses.empty?
185
217
 
186
218
  conditions = clauses.map do |clause|
@@ -205,7 +237,8 @@ module BetterAuth
205
237
  value = options.advanced.dig(:database, :default_find_many_limit)
206
238
  return 100 if value.nil?
207
239
 
208
- Integer(value)
240
+ parsed = Integer(value)
241
+ parsed.positive? ? parsed : 100
209
242
  rescue ArgumentError, TypeError
210
243
  100
211
244
  end
@@ -218,7 +251,10 @@ module BetterAuth
218
251
  operator = (fetch_key(clause, :operator) || "eq").to_s.downcase
219
252
  value = fetch_key(clause, :value)
220
253
 
221
- field = resolve_field(model, fetch_key(clause, :field))
254
+ requested_field = fetch_key(clause, :field)
255
+ bad_request!("where field is required") if requested_field.nil? || requested_field.to_s.empty?
256
+
257
+ field = resolve_field(model, requested_field)
222
258
  attributes = fields_for(model).fetch(field)
223
259
  key = (field == "id") ? "_id" : storage_field(model, field)
224
260
  mode = (fetch_key(clause, :mode) || "sensitive").to_s
@@ -230,6 +266,8 @@ module BetterAuth
230
266
  when "eq"
231
267
  (insensitive && value.is_a?(String)) ? regex_condition(key, value, :eq, insensitive: true) : {key => store_value(field, value, attributes, strict_id: true)}
232
268
  when "in"
269
+ bad_request!("where value must be an array for in operator") unless value.is_a?(Array)
270
+
233
271
  (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) }}}
234
272
  when "not_in"
235
273
  (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) }}}
@@ -281,9 +319,9 @@ module BetterAuth
281
319
  foreign_field = storage_field_for_join(join_model, config.fetch(:to))
282
320
  relation = config[:relation]
283
321
  limit = config.key?(:limit) ? config[:limit] : nil
284
- effective_limit = limit.nil? ? default_find_many_limit : limit.to_i
322
+ effective_limit = limit.nil? ? default_find_many_limit : positive_integer!(limit, "join limit")
285
323
  unique = relation == "one-to-one" || config[:unique]
286
- should_limit = !unique && effective_limit.positive?
324
+ should_limit = !unique
287
325
 
288
326
  lookup = if should_limit
289
327
  {
@@ -313,19 +351,30 @@ module BetterAuth
313
351
  end
314
352
 
315
353
  def normalized_join(model, join)
354
+ bad_request!("join must be a hash") unless join.is_a?(Hash)
355
+
316
356
  join.each_with_object({}) do |(join_model, config), result|
317
357
  join_model = join_model.to_s
358
+ bad_request!("join model is required") if join_model.empty?
359
+
318
360
  result[join_model] = normalize_join_config(model, join_model, config)
319
361
  end
320
362
  end
321
363
 
322
364
  def normalize_join_config(model, join_model, config)
365
+ bad_request!("join config must be true or a hash") unless config == true || config.is_a?(Hash)
366
+
323
367
  if config.is_a?(Hash) && (config.key?(:on) || config.key?("on"))
324
368
  on = config[:on] || config["on"]
369
+ bad_request!("join on must be a hash") unless on.is_a?(Hash)
370
+
325
371
  relation = config[:relation] || config["relation"]
326
372
  limit = config[:limit] || config["limit"]
327
373
  from = fetch_key(on, :from)
328
374
  to = fetch_key(on, :to)
375
+ bad_request!("join on.from is required") if from.nil? || from.to_s.empty?
376
+ bad_request!("join on.to is required") if to.nil? || to.to_s.empty?
377
+
329
378
  return {from: Schema.storage_key(from), to: Schema.storage_key(to), relation: relation, limit: limit, unique: unique_join_field?(join_model, to)}
330
379
  end
331
380
 
@@ -414,9 +463,8 @@ module BetterAuth
414
463
  def collection_name(model)
415
464
  model = default_model_name(model)
416
465
  configured = configured_model_name(model)
417
- return "#{configured}s" if configured && use_plural
418
- return configured if configured
419
466
  return schema_for(model).fetch(:model_name) if use_plural
467
+ return configured if configured
420
468
 
421
469
  model.to_s
422
470
  end
@@ -548,7 +596,7 @@ module BetterAuth
548
596
 
549
597
  def coerce_value(value, attributes)
550
598
  return value if value.nil?
551
- return Time.parse(value) if attributes[:type] == "date" && value.is_a?(String)
599
+ return parse_date_value(value) if attributes[:type] == "date" && value.is_a?(String)
552
600
 
553
601
  value
554
602
  end
@@ -611,6 +659,61 @@ module BetterAuth
611
659
  nil
612
660
  end
613
661
 
662
+ def validate_where!(where)
663
+ bad_request!("where must be an array") unless where.is_a?(Array)
664
+
665
+ where.each do |clause|
666
+ bad_request!("where entries must be hashes") unless clause.is_a?(Hash)
667
+ end
668
+
669
+ where
670
+ end
671
+
672
+ def positive_integer!(value, name)
673
+ parsed = Integer(value)
674
+ bad_request!("#{name} must be a positive integer") unless parsed.positive?
675
+
676
+ parsed
677
+ rescue ArgumentError, TypeError
678
+ bad_request!("#{name} must be a positive integer")
679
+ end
680
+
681
+ def non_negative_integer!(value, name)
682
+ parsed = Integer(value)
683
+ bad_request!("#{name} must be zero or a positive integer") if parsed.negative?
684
+
685
+ parsed
686
+ rescue ArgumentError, TypeError
687
+ bad_request!("#{name} must be zero or a positive integer")
688
+ end
689
+
690
+ def ensure_update_document!(document)
691
+ bad_request!("No fields to update") if document.empty?
692
+ end
693
+
694
+ def ensure_update_input_has_fields!(model, update)
695
+ bad_request!("update must be a hash") unless update.is_a?(Hash)
696
+
697
+ fields = fields_for(model)
698
+ input = stringify_keys(update)
699
+ has_updatable_field = input.any? do |field_key, _value|
700
+ next false if field_key == "id" || field_key == "_id"
701
+
702
+ fields.key?(field_key) || fields.any? { |_field, attributes| attributes[:field_name].to_s == field_key }
703
+ end
704
+ bad_request!("No fields to update") unless has_updatable_field
705
+ end
706
+
707
+ def parse_date_value(value)
708
+ Time.parse(value)
709
+ rescue ArgumentError
710
+ bad_request!("Invalid date value")
711
+ end
712
+
713
+ def bad_request!(message)
714
+ raise APIError.new("BAD_REQUEST", message: message)
715
+ end
716
+
614
717
  def schema_for(model)
615
718
  Schema.auth_tables(options).fetch(default_model_name(model))
616
719
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: better_auth-mongodb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sebastian Sala
@@ -161,14 +161,14 @@ files:
161
161
  - lib/better_auth/mongo_adapter.rb
162
162
  - lib/better_auth/mongodb.rb
163
163
  - lib/better_auth/mongodb/version.rb
164
- homepage: https://github.com/sebasxsala/better-auth
164
+ homepage: https://github.com/sebasxsala/better-auth-rb
165
165
  licenses:
166
166
  - MIT
167
167
  metadata:
168
- homepage_uri: https://github.com/sebasxsala/better-auth
169
- source_code_uri: https://github.com/sebasxsala/better-auth
170
- changelog_uri: https://github.com/sebasxsala/better-auth/blob/main/packages/better_auth-mongodb/CHANGELOG.md
171
- bug_tracker_uri: https://github.com/sebasxsala/better-auth/issues
168
+ homepage_uri: https://github.com/sebasxsala/better-auth-rb
169
+ source_code_uri: https://github.com/sebasxsala/better-auth-rb
170
+ changelog_uri: https://github.com/sebasxsala/better-auth-rb/blob/main/packages/better_auth-mongodb/CHANGELOG.md
171
+ bug_tracker_uri: https://github.com/sebasxsala/better-auth-rb/issues
172
172
  rdoc_options: []
173
173
  require_paths:
174
174
  - lib