better_auth 0.2.0 → 0.4.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.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +32 -0
  3. data/README.md +5 -3
  4. data/lib/better_auth/adapters/internal_adapter.rb +173 -20
  5. data/lib/better_auth/adapters/memory.rb +61 -12
  6. data/lib/better_auth/adapters/mongodb.rb +5 -365
  7. data/lib/better_auth/adapters/sql.rb +44 -3
  8. data/lib/better_auth/api.rb +7 -2
  9. data/lib/better_auth/async.rb +70 -0
  10. data/lib/better_auth/context.rb +2 -1
  11. data/lib/better_auth/database_hooks.rb +3 -3
  12. data/lib/better_auth/deprecate.rb +28 -0
  13. data/lib/better_auth/endpoint.rb +5 -2
  14. data/lib/better_auth/host.rb +166 -0
  15. data/lib/better_auth/instrumentation.rb +74 -0
  16. data/lib/better_auth/logger.rb +31 -0
  17. data/lib/better_auth/middleware/origin_check.rb +2 -2
  18. data/lib/better_auth/oauth2.rb +94 -0
  19. data/lib/better_auth/plugin.rb +14 -1
  20. data/lib/better_auth/plugins/email_otp.rb +16 -5
  21. data/lib/better_auth/plugins/generic_oauth.rb +14 -28
  22. data/lib/better_auth/plugins/oauth_protocol.rb +553 -64
  23. data/lib/better_auth/plugins/organization/schema.rb +6 -0
  24. data/lib/better_auth/plugins/organization.rb +56 -20
  25. data/lib/better_auth/plugins/two_factor.rb +53 -18
  26. data/lib/better_auth/rate_limiter.rb +37 -2
  27. data/lib/better_auth/request_state.rb +44 -0
  28. data/lib/better_auth/router.rb +14 -1
  29. data/lib/better_auth/routes/account.rb +16 -4
  30. data/lib/better_auth/routes/email_verification.rb +5 -2
  31. data/lib/better_auth/routes/password.rb +21 -1
  32. data/lib/better_auth/routes/session.rb +27 -4
  33. data/lib/better_auth/routes/sign_in.rb +3 -1
  34. data/lib/better_auth/routes/sign_up.rb +60 -1
  35. data/lib/better_auth/routes/social.rb +231 -22
  36. data/lib/better_auth/routes/user.rb +23 -5
  37. data/lib/better_auth/schema/sql.rb +11 -0
  38. data/lib/better_auth/schema.rb +16 -0
  39. data/lib/better_auth/session.rb +12 -1
  40. data/lib/better_auth/social_providers/apple.rb +44 -8
  41. data/lib/better_auth/social_providers/atlassian.rb +32 -0
  42. data/lib/better_auth/social_providers/base.rb +262 -4
  43. data/lib/better_auth/social_providers/cognito.rb +32 -0
  44. data/lib/better_auth/social_providers/discord.rb +27 -5
  45. data/lib/better_auth/social_providers/dropbox.rb +33 -0
  46. data/lib/better_auth/social_providers/facebook.rb +35 -0
  47. data/lib/better_auth/social_providers/figma.rb +31 -0
  48. data/lib/better_auth/social_providers/github.rb +21 -6
  49. data/lib/better_auth/social_providers/gitlab.rb +16 -3
  50. data/lib/better_auth/social_providers/google.rb +38 -13
  51. data/lib/better_auth/social_providers/huggingface.rb +31 -0
  52. data/lib/better_auth/social_providers/kakao.rb +32 -0
  53. data/lib/better_auth/social_providers/kick.rb +32 -0
  54. data/lib/better_auth/social_providers/line.rb +33 -0
  55. data/lib/better_auth/social_providers/linear.rb +44 -0
  56. data/lib/better_auth/social_providers/linkedin.rb +30 -0
  57. data/lib/better_auth/social_providers/microsoft_entra_id.rb +79 -7
  58. data/lib/better_auth/social_providers/naver.rb +31 -0
  59. data/lib/better_auth/social_providers/notion.rb +33 -0
  60. data/lib/better_auth/social_providers/paybin.rb +31 -0
  61. data/lib/better_auth/social_providers/paypal.rb +36 -0
  62. data/lib/better_auth/social_providers/polar.rb +31 -0
  63. data/lib/better_auth/social_providers/railway.rb +49 -0
  64. data/lib/better_auth/social_providers/reddit.rb +32 -0
  65. data/lib/better_auth/social_providers/roblox.rb +31 -0
  66. data/lib/better_auth/social_providers/salesforce.rb +38 -0
  67. data/lib/better_auth/social_providers/slack.rb +30 -0
  68. data/lib/better_auth/social_providers/spotify.rb +31 -0
  69. data/lib/better_auth/social_providers/tiktok.rb +35 -0
  70. data/lib/better_auth/social_providers/twitch.rb +39 -0
  71. data/lib/better_auth/social_providers/twitter.rb +32 -0
  72. data/lib/better_auth/social_providers/vercel.rb +47 -0
  73. data/lib/better_auth/social_providers/vk.rb +34 -0
  74. data/lib/better_auth/social_providers/wechat.rb +104 -0
  75. data/lib/better_auth/social_providers/zoom.rb +31 -0
  76. data/lib/better_auth/social_providers.rb +29 -0
  77. data/lib/better_auth/url_helpers.rb +195 -0
  78. data/lib/better_auth/version.rb +1 -1
  79. data/lib/better_auth.rb +8 -1
  80. metadata +38 -15
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a5e95c51ce68259fcccbbfabe1bd9ffa5383ce4c098161f1c2d54da76137fa0a
4
- data.tar.gz: 7c81ade6292ced9b98c1c411c320e4ad6e05734ea891145f75a9205d6e4fdea0
3
+ metadata.gz: 38179f5800613263ca30525a3d878b91c2a5f9057f104b1f24cecbf72a0cc030
4
+ data.tar.gz: 9883d339ce2f1ab8f5a618da3708f4ab5bdde69a09fe98b48889b6069351c7bb
5
5
  SHA512:
6
- metadata.gz: 4b7f5c635ce16aa7be7f3ea42c5fa03304b7c682821c8a82e55c81754ba330a88aba350f0a6ed59b171333766ba6a973c4a2d15cf3d46debeae42ddd59b602c5
7
- data.tar.gz: 524ce3bfb3f023bcf057154fde0e88e0555753016d1297d67d40f2db6de6cf5516b0fcb8b513fd260cd284b38722eeffe9a02784d3d08a58cd5811cce23a1d39
6
+ metadata.gz: b0bdd97ab9d677df601b0eed5d387a09543743aee41d0243f7ed3981935c3391f3ae10dfc110fc41312a5a4b188f29581563f2c99154a8808ed3158cb47663ad
7
+ data.tar.gz: 6b9b76c6969e601e01506a2cd91a28abf13eeccf0446023fad7c58e8c95476f6110b7ac6f3979fc18705687589cff74748f839c685a8ccb791392d0d2e729c15
data/CHANGELOG.md CHANGED
@@ -7,6 +7,38 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.4.0] - 2026-04-30
11
+
12
+ ### Added
13
+
14
+ - Added upstream-parity helpers for async execution, host resolution, instrumentation, request state, URL handling, OAuth2, deprecation warnings, and expanded route behavior.
15
+ - Added two-factor, OAuth protocol, social route, organization, admin, adapter, schema, and session parity coverage.
16
+
17
+ ### Changed
18
+
19
+ - Aligned core auth, email OTP, generic OAuth, organization, two-factor, OAuth protocol, adapter, router, rate-limiter, logger, and middleware behavior more closely with upstream Better Auth.
20
+
21
+ ### Fixed
22
+
23
+ - Fixed upstream parity gaps in organization handling, generic OAuth user info, email OTP sign-up, database schema behavior, and route/session edge cases.
24
+
25
+ ## [0.3.0] - 2026-04-29
26
+
27
+ ### Added
28
+
29
+ - Added upstream-parity social provider support, including provider-specific authorization, token, profile, refresh, and revocation behavior for the expanded provider set.
30
+ - Added OAuth/OIDC protocol hardening for authorization, callback, discovery, metadata, token, and userinfo flows.
31
+ - Added upstream v1.6.9 parity coverage for schema generation, adapter behavior, plugin hooks, session handling, and account/user route edge cases.
32
+
33
+ ### Changed
34
+
35
+ - Extracted MongoDB adapter support behind the external `better_auth-mongo-adapter` shim while preserving compatibility for existing adapter configuration.
36
+ - Updated auth routes, router behavior, rate limiting, password and email-verification flows, and schema metadata to match upstream semantics more closely.
37
+
38
+ ### Fixed
39
+
40
+ - Fixed social provider edge cases, magic-link expiration behavior, adapter value coercion, and callback/session handling across Rack integrations.
41
+
10
42
  ## [0.1.1] - 2026-03-22
11
43
 
12
44
  ### Fixed
data/README.md CHANGED
@@ -90,7 +90,7 @@ Custom Better Auth-style password callbacks are still supported through `email_a
90
90
 
91
91
  ### Database Adapters
92
92
 
93
- The core gem ships framework-agnostic adapters for memory, PostgreSQL, MySQL, SQLite, MongoDB, and MSSQL. Driver gems are loaded only when their adapter is instantiated.
93
+ The core gem ships framework-agnostic adapters for memory, PostgreSQL, MySQL, SQLite, and MSSQL. Driver gems are loaded only when their adapter is instantiated. MongoDB support lives in the external `better_auth-mongo-adapter` package so apps that do not use MongoDB do not install the Mongo driver.
94
94
 
95
95
  ```ruby
96
96
  auth = BetterAuth.auth(
@@ -100,6 +100,8 @@ auth = BetterAuth.auth(
100
100
  ```
101
101
 
102
102
  ```ruby
103
+ require "better_auth/mongo_adapter"
104
+
103
105
  auth = BetterAuth.auth(
104
106
  secret: ENV.fetch("BETTER_AUTH_SECRET"),
105
107
  database: BetterAuth::Adapters::MongoDB.new(
@@ -155,7 +157,7 @@ export const authClient = createAuthClient({
155
157
  Add to your Gemfile:
156
158
 
157
159
  ```ruby
158
- gem 'better_auth', require: 'better_auth/rails'
160
+ gem "better_auth-rails"
159
161
  ```
160
162
 
161
163
  Then in your ApplicationController:
@@ -170,7 +172,7 @@ Now you have access to `current_user` and authentication methods:
170
172
 
171
173
  ```ruby
172
174
  class PostsController < ApplicationController
173
- before_action :authenticate_user!
175
+ before_action :require_authentication
174
176
 
175
177
  def index
176
178
  @posts = current_user.posts
@@ -59,9 +59,12 @@ module BetterAuth
59
59
  end
60
60
 
61
61
  def delete_user(user_id)
62
- delete_sessions(user_id) if !secondary_storage || options.session[:store_session_in_database]
62
+ deleted = hooks.delete([{field: "id", value: user_id}], "user")
63
+ return false if deleted == false
64
+
63
65
  hooks.delete_many([{field: "userId", value: user_id}], "account")
64
- hooks.delete([{field: "id", value: user_id}], "user")
66
+ delete_sessions(user_id) if !secondary_storage || options.session[:store_session_in_database]
67
+ deleted
65
68
  end
66
69
 
67
70
  def create_session(user_id, dont_remember_me = false, override = nil, override_all = false, context = nil)
@@ -74,28 +77,31 @@ module BetterAuth
74
77
  "userId" => user_id,
75
78
  "token" => token
76
79
  }.merge(timestamps)
80
+ base["id"] = generated_id if secondary_storage
77
81
  data = override_all ? base.merge(override) : override.merge(base)
78
82
 
79
83
  custom = secondary_storage && lambda do |session_data|
80
84
  actual_session = apply_schema_create("session", session_data)
81
85
  store_session(actual_session)
86
+ adapter.create(model: "session", data: actual_session, force_allow_id: true) if options.session[:store_session_in_database]
82
87
  actual_session
83
88
  end
84
- execute_main = !secondary_storage || options.session[:store_session_in_database]
85
- created = hooks.create(data, "session", custom: custom, context: context)
86
- adapter.create(model: "session", data: data, force_allow_id: true) if secondary_storage && execute_main
87
- created
89
+ hooks.create(data, "session", custom: custom, context: context)
88
90
  end
89
91
 
90
92
  def find_session(token)
91
93
  if secondary_storage
92
94
  data = parse_storage(secondary_storage.get(token))
93
- return nil unless data
95
+ unless data
96
+ return nil unless options.session[:store_session_in_database] && !options.session[:preserve_session_in_database]
97
+ end
94
98
 
95
- return {
96
- session: normalize_session_dates(data["session"]),
97
- user: normalize_user_dates(data["user"])
98
- }
99
+ if data
100
+ return {
101
+ session: normalize_session_dates(data["session"]),
102
+ user: normalize_user_dates(data["user"])
103
+ }
104
+ end
99
105
  end
100
106
 
101
107
  found = find_session_with_user(token)
@@ -113,7 +119,9 @@ module BetterAuth
113
119
  data = stringify_keys(session)
114
120
  if secondary_storage
115
121
  return hooks.update(data, [{field: "token", value: token}], "session", custom: lambda { |actual_data|
116
- update_stored_session(token, actual_data)
122
+ stored = update_stored_session(token, actual_data)
123
+ db = adapter.update(model: "session", where: [{field: "token", value: token}], update: actual_data) if options.session[:store_session_in_database]
124
+ db || stored
117
125
  })
118
126
  end
119
127
 
@@ -226,30 +234,93 @@ module BetterAuth
226
234
  end
227
235
 
228
236
  def create_verification_value(data)
229
- hooks.create(timestamps.merge(stringify_keys(data)), "verification")
237
+ payload = timestamps.merge(stringify_keys(data))
238
+ stored_identifier = processed_verification_identifier(payload.fetch("identifier"))
239
+ payload["identifier"] = stored_identifier
240
+
241
+ custom = secondary_storage && lambda do |verification_data|
242
+ actual = apply_schema_create("verification", verification_data)
243
+ actual["id"] ||= generated_id
244
+ store_verification(actual)
245
+ adapter.create(model: "verification", data: actual, force_allow_id: true) if verification_store_in_database?
246
+ actual
247
+ end
248
+
249
+ hooks.create(payload, "verification", custom: custom)
230
250
  end
231
251
 
232
252
  def find_verification_value(identifier)
253
+ stored_identifier = processed_verification_identifier(identifier)
254
+ storage_option = verification_storage_option(identifier)
255
+ if secondary_storage
256
+ cached = read_verification(stored_identifier)
257
+ cached ||= read_verification(identifier) if storage_option && storage_option.to_s != "plain"
258
+ return cached if cached
259
+ return nil unless verification_store_in_database?
260
+ end
261
+
233
262
  values = adapter.find_many(
234
263
  model: "verification",
235
- where: [{field: "identifier", value: identifier}],
264
+ where: [{field: "identifier", value: stored_identifier}],
236
265
  sort_by: {field: "createdAt", direction: "desc"},
237
266
  limit: 1
238
267
  )
268
+ if values.empty? && storage_option && storage_option.to_s != "plain"
269
+ values = adapter.find_many(
270
+ model: "verification",
271
+ where: [{field: "identifier", value: identifier}],
272
+ sort_by: {field: "createdAt", direction: "desc"},
273
+ limit: 1
274
+ )
275
+ end
239
276
  hooks.delete_many([{field: "expiresAt", value: Time.now, operator: "lt"}], "verification") unless options.verification[:disable_cleanup]
240
277
  values.first
241
278
  end
242
279
 
243
280
  def delete_verification_value(id)
281
+ if secondary_storage
282
+ stored_identifier = secondary_storage.get(verification_id_key(id))
283
+ if stored_identifier
284
+ secondary_storage.delete(verification_key(stored_identifier))
285
+ secondary_storage.delete(verification_id_key(id))
286
+ return nil unless verification_store_in_database?
287
+ elsif !verification_store_in_database?
288
+ return nil
289
+ end
290
+ end
291
+
244
292
  hooks.delete([{field: "id", value: id}], "verification")
245
293
  end
246
294
 
247
295
  def delete_verification_by_identifier(identifier)
248
- hooks.delete([{field: "identifier", value: identifier}], "verification")
296
+ stored_identifier = processed_verification_identifier(identifier)
297
+ if secondary_storage
298
+ cached = read_verification(stored_identifier)
299
+ secondary_storage.delete(verification_key(stored_identifier))
300
+ secondary_storage.delete(verification_id_key(cached["id"])) if cached && cached["id"]
301
+ return nil unless verification_store_in_database?
302
+ end
303
+
304
+ hooks.delete([{field: "identifier", value: stored_identifier}], "verification")
249
305
  end
250
306
 
251
307
  def update_verification_value(id, data)
252
- hooks.update(stringify_keys(data), [{field: "id", value: id}], "verification")
308
+ update = stringify_keys(data)
309
+ if secondary_storage
310
+ stored_identifier = secondary_storage.get(verification_id_key(id))
311
+ if stored_identifier
312
+ cached = read_verification(stored_identifier)
313
+ if cached
314
+ updated = cached.merge(update)
315
+ store_verification(updated)
316
+ return updated unless verification_store_in_database?
317
+ end
318
+ elsif !verification_store_in_database?
319
+ return nil
320
+ end
321
+ end
322
+
323
+ hooks.update(update, [{field: "id", value: id}], "verification")
253
324
  end
254
325
 
255
326
  private
@@ -285,6 +356,14 @@ module BetterAuth
285
356
  {"createdAt" => now, "updatedAt" => now}
286
357
  end
287
358
 
359
+ def generated_id
360
+ generator = options.advanced.dig(:database, :generate_id)
361
+ return generator.call.to_s if generator.respond_to?(:call)
362
+ return SecureRandom.uuid if generator == "uuid"
363
+
364
+ SecureRandom.hex(16)
365
+ end
366
+
288
367
  def stringify_keys(data)
289
368
  data.each_with_object({}) do |(key, value), result|
290
369
  result[Schema.storage_key(key)] = value
@@ -295,6 +374,8 @@ module BetterAuth
295
374
  fields = Schema.auth_tables(options)[model]&.fetch(:fields)
296
375
  fields ||= session_additional_fields if model == "session"
297
376
  output = stringify_keys(data)
377
+ return output unless fields
378
+
298
379
  fields.each do |field, attributes|
299
380
  unless output.key?(field)
300
381
  if attributes.key?(:default_value)
@@ -334,7 +415,8 @@ module BetterAuth
334
415
  .push({"token" => session["token"], "expiresAt" => expires_ms})
335
416
  .sort_by { |entry| entry["expiresAt"] }
336
417
  write_active_sessions(session["userId"], entries)
337
- secondary_storage.set(session["token"], JSON.generate({session: session, user: user}), ttl(expires_ms))
418
+ ttl_seconds = ttl(expires_ms)
419
+ secondary_storage.set(session["token"], JSON.generate({session: session, user: user}), ttl_seconds) if ttl_seconds.positive?
338
420
  end
339
421
 
340
422
  def update_stored_session(token, data)
@@ -345,7 +427,12 @@ module BetterAuth
345
427
  merged["expiresAt"] = normalize_time(merged["expiresAt"])
346
428
  merged["createdAt"] = normalize_time(merged["createdAt"])
347
429
  merged["updatedAt"] = normalize_time(merged["updatedAt"])
348
- secondary_storage.set(token, JSON.generate({session: merged, user: parsed["user"]}), ttl(millis(merged["expiresAt"])))
430
+ ttl_seconds = ttl(millis(merged["expiresAt"]))
431
+ if ttl_seconds.positive?
432
+ secondary_storage.set(token, JSON.generate({session: merged, user: parsed["user"]}), ttl_seconds)
433
+ else
434
+ secondary_storage.delete(token)
435
+ end
349
436
  entries = active_session_entries(merged["userId"])
350
437
  .reject { |entry| entry["token"] == token || entry["expiresAt"].to_i <= current_millis }
351
438
  .push({"token" => token, "expiresAt" => millis(merged["expiresAt"])})
@@ -369,7 +456,7 @@ module BetterAuth
369
456
  raw = secondary_storage.get(active_key(user_id))
370
457
  Array(parse_storage(raw)).map do |entry|
371
458
  entry.transform_keys(&:to_s)
372
- end
459
+ end.uniq { |entry| entry["token"] }
373
460
  end
374
461
 
375
462
  def write_active_sessions(user_id, entries)
@@ -377,7 +464,12 @@ module BetterAuth
377
464
  if future.empty?
378
465
  secondary_storage.delete(active_key(user_id))
379
466
  else
380
- secondary_storage.set(active_key(user_id), JSON.generate(future), ttl(future.last["expiresAt"]))
467
+ ttl_seconds = ttl(future.last["expiresAt"])
468
+ if ttl_seconds.positive?
469
+ secondary_storage.set(active_key(user_id), JSON.generate(future), ttl_seconds)
470
+ else
471
+ secondary_storage.delete(active_key(user_id))
472
+ end
381
473
  end
382
474
  end
383
475
 
@@ -415,6 +507,67 @@ module BetterAuth
415
507
  )
416
508
  end
417
509
 
510
+ def normalize_verification_dates(verification)
511
+ return nil unless verification
512
+
513
+ verification.transform_keys(&:to_s).merge(
514
+ "expiresAt" => normalize_time(verification["expiresAt"] || verification[:expiresAt]),
515
+ "createdAt" => normalize_time(verification["createdAt"] || verification[:createdAt]),
516
+ "updatedAt" => normalize_time(verification["updatedAt"] || verification[:updatedAt])
517
+ )
518
+ end
519
+
520
+ def store_verification(verification)
521
+ normalized = normalize_verification_dates(verification)
522
+ ttl_seconds = ttl(millis(normalized["expiresAt"]))
523
+ return normalized unless ttl_seconds.positive?
524
+
525
+ secondary_storage.set(verification_key(normalized["identifier"]), JSON.generate(normalized), ttl_seconds)
526
+ secondary_storage.set(verification_id_key(normalized["id"]), normalized["identifier"], ttl_seconds) if normalized["id"]
527
+ normalized
528
+ end
529
+
530
+ def read_verification(identifier)
531
+ normalize_verification_dates(parse_storage(secondary_storage.get(verification_key(identifier))))
532
+ end
533
+
534
+ def verification_key(identifier)
535
+ "verification:#{identifier}"
536
+ end
537
+
538
+ def verification_id_key(id)
539
+ "verification-id:#{id}"
540
+ end
541
+
542
+ def verification_store_in_database?
543
+ !!options.verification[:store_in_database]
544
+ end
545
+
546
+ def processed_verification_identifier(identifier)
547
+ option = verification_storage_option(identifier)
548
+ return identifier.to_s if option.nil? || option.to_s == "plain"
549
+ return Crypto.sha256(identifier.to_s, encoding: :base64url) if option.to_s == "hashed"
550
+ return option[:hash].call(identifier.to_s).to_s if option.is_a?(Hash) && option[:hash].respond_to?(:call)
551
+ return option["hash"].call(identifier.to_s).to_s if option.is_a?(Hash) && option["hash"].respond_to?(:call)
552
+
553
+ identifier.to_s
554
+ end
555
+
556
+ def verification_storage_option(identifier)
557
+ config = options.verification[:store_identifier]
558
+ return nil unless config
559
+
560
+ if config.is_a?(Hash) && (config.key?(:default) || config.key?("default"))
561
+ overrides = config[:overrides] || config["overrides"] || {}
562
+ overrides.each do |prefix, option|
563
+ return option if identifier.to_s.start_with?(prefix.to_s)
564
+ end
565
+ return config[:default] || config["default"]
566
+ end
567
+
568
+ config
569
+ end
570
+
418
571
  def normalize_time(value)
419
572
  return value if value.is_a?(Time)
420
573
  return Time.at(value / 1000.0) if value.is_a?(Integer) && value > 10_000_000_000
@@ -156,33 +156,79 @@ module BetterAuth
156
156
  value = fetch_key(clause, :value)
157
157
  operator = (fetch_key(clause, :operator) || "eq").to_s
158
158
  current = record[field]
159
+ comparable = coerce_where_value(record, field, value, operator)
159
160
 
160
161
  case operator
161
162
  when "in"
162
- Array(value).include?(current)
163
+ Array(comparable).include?(current)
163
164
  when "not_in"
164
- !Array(value).include?(current)
165
+ !Array(comparable).include?(current)
165
166
  when "contains"
166
- current.to_s.include?(value.to_s)
167
+ current.to_s.include?(comparable.to_s)
167
168
  when "starts_with"
168
- current.to_s.start_with?(value.to_s)
169
+ current.to_s.start_with?(comparable.to_s)
169
170
  when "ends_with"
170
- current.to_s.end_with?(value.to_s)
171
+ current.to_s.end_with?(comparable.to_s)
171
172
  when "ne"
172
- current != value
173
+ current != comparable
173
174
  when "gt"
174
- !value.nil? && current > value
175
+ !comparable.nil? && current > comparable
175
176
  when "gte"
176
- !value.nil? && current >= value
177
+ !comparable.nil? && current >= comparable
177
178
  when "lt"
178
- !value.nil? && current < value
179
+ !comparable.nil? && current < comparable
179
180
  when "lte"
180
- !value.nil? && current <= value
181
+ !comparable.nil? && current <= comparable
181
182
  else
182
- current == value
183
+ current == comparable
183
184
  end
184
185
  end
185
186
 
187
+ def coerce_where_value(record, field, value, operator)
188
+ attributes = schema_for_record_field(record, field)
189
+ return value unless attributes
190
+ return Array(value).map { |entry| coerce_scalar_where_value(entry, attributes) } if %w[in not_in].include?(operator)
191
+
192
+ coerce_scalar_where_value(value, attributes)
193
+ end
194
+
195
+ def schema_for_record_field(record, field)
196
+ db.each_key do |model|
197
+ fields = Schema.auth_tables(options)[model]&.fetch(:fields, nil)
198
+ next unless fields&.key?(field)
199
+ return fields[field] if table_for(model).include?(record)
200
+ end
201
+ nil
202
+ end
203
+
204
+ def coerce_scalar_where_value(value, attributes)
205
+ return value if value.nil?
206
+
207
+ case attributes[:type]
208
+ when "boolean"
209
+ return false if value == false || value == 0 || value.to_s.downcase == "false" || value.to_s == "0"
210
+ return true if value == true || value == 1 || value.to_s.downcase == "true" || value.to_s == "1"
211
+ when "number"
212
+ return coerce_number(value)
213
+ when "date"
214
+ return Time.parse(value) if value.is_a?(String)
215
+ when "number[]"
216
+ return Array(value).map { |entry| coerce_number(entry) }
217
+ when "string[]"
218
+ return Array(value).map(&:to_s)
219
+ end
220
+
221
+ value
222
+ end
223
+
224
+ def coerce_number(value)
225
+ return value unless value.is_a?(String)
226
+ return value.to_i if /\A-?\d+\z/.match?(value)
227
+ return value.to_f if /\A-?\d+\.\d+\z/.match?(value)
228
+
229
+ value
230
+ end
231
+
186
232
  def sort_records(model, records, sort_by)
187
233
  field = Schema.storage_key(fetch_key(sort_by, :field))
188
234
  direction = fetch_key(sort_by, :direction).to_s
@@ -225,7 +271,10 @@ module BetterAuth
225
271
  end
226
272
 
227
273
  def fetch_key(hash, key)
228
- hash[key] || hash[key.to_s] || hash[Schema.storage_key(key)] || hash[Schema.storage_key(key).to_sym]
274
+ [key, key.to_s, Schema.storage_key(key), Schema.storage_key(key).to_sym].each do |candidate|
275
+ return hash[candidate] if hash.key?(candidate)
276
+ end
277
+ nil
229
278
  end
230
279
  end
231
280
  end