better_auth 0.10.0 → 0.11.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 +4 -4
- data/CHANGELOG.md +83 -0
- data/LICENSE.md +1 -1
- data/README.md +138 -91
- data/lib/better_auth/adapters/base.rb +64 -1
- data/lib/better_auth/adapters/internal_adapter.rb +197 -11
- data/lib/better_auth/adapters/join_support.rb +10 -0
- data/lib/better_auth/adapters/memory.rb +191 -56
- data/lib/better_auth/adapters/mssql.rb +7 -3
- data/lib/better_auth/adapters/sql.rb +294 -49
- data/lib/better_auth/api.rb +3 -24
- data/lib/better_auth/configuration.rb +41 -46
- data/lib/better_auth/context.rb +43 -67
- data/lib/better_auth/cookies.rb +110 -14
- data/lib/better_auth/core.rb +0 -2
- data/lib/better_auth/crypto.rb +4 -3
- data/lib/better_auth/doctor.rb +14 -3
- data/lib/better_auth/error.rb +3 -1
- data/lib/better_auth/logger.rb +1 -1
- data/lib/better_auth/middleware/origin_check.rb +9 -1
- data/lib/better_auth/migration_plan.rb +4 -4
- data/lib/better_auth/oauth_state.rb +147 -0
- data/lib/better_auth/plugin_loader.rb +220 -0
- data/lib/better_auth/plugins/access.rb +10 -13
- data/lib/better_auth/plugins/admin.rb +79 -28
- data/lib/better_auth/plugins/api_key.rb +4 -7
- data/lib/better_auth/plugins/bearer.rb +8 -22
- data/lib/better_auth/plugins/captcha.rb +24 -6
- data/lib/better_auth/plugins/device_authorization.rb +47 -9
- data/lib/better_auth/plugins/dub.rb +2 -2
- data/lib/better_auth/plugins/email_otp.rb +30 -47
- data/lib/better_auth/plugins/generic_oauth.rb +141 -146
- data/lib/better_auth/plugins/i18n.rb +215 -0
- data/lib/better_auth/plugins/jwt.rb +37 -19
- data/lib/better_auth/plugins/magic_link.rb +18 -21
- data/lib/better_auth/plugins/oauth_popup.rb +330 -0
- data/lib/better_auth/plugins/oauth_protocol.rb +142 -32
- data/lib/better_auth/plugins/oauth_provider.rb +4 -7
- data/lib/better_auth/plugins/oauth_proxy.rb +1 -1
- data/lib/better_auth/plugins/one_tap.rb +61 -60
- data/lib/better_auth/plugins/one_time_token.rb +1 -4
- data/lib/better_auth/plugins/open_api.rb +35 -7
- data/lib/better_auth/plugins/organization.rb +455 -122
- data/lib/better_auth/plugins/passkey.rb +4 -7
- data/lib/better_auth/plugins/phone_number.rb +24 -26
- data/lib/better_auth/plugins/scim.rb +4 -7
- data/lib/better_auth/plugins/siwe.rb +126 -16
- data/lib/better_auth/plugins/sso.rb +4 -7
- data/lib/better_auth/plugins/stripe.rb +4 -7
- data/lib/better_auth/plugins/two_factor.rb +186 -21
- data/lib/better_auth/plugins/username.rb +56 -13
- data/lib/better_auth/plugins.rb +57 -0
- data/lib/better_auth/rate_limiter.rb +297 -135
- data/lib/better_auth/request_ip.rb +123 -13
- data/lib/better_auth/router.rb +3 -3
- data/lib/better_auth/routes/account.rb +40 -8
- data/lib/better_auth/routes/email_verification.rb +88 -5
- data/lib/better_auth/routes/password.rb +5 -6
- data/lib/better_auth/routes/session.rb +7 -2
- data/lib/better_auth/routes/sign_in.rb +10 -1
- data/lib/better_auth/routes/sign_up.rb +45 -22
- data/lib/better_auth/routes/social.rb +169 -54
- data/lib/better_auth/routes/user.rb +11 -8
- data/lib/better_auth/schema/sql.rb +90 -24
- data/lib/better_auth/schema.rb +124 -3
- data/lib/better_auth/session.rb +27 -9
- data/lib/better_auth/session_store.rb +1 -1
- data/lib/better_auth/social_providers/apple.rb +6 -1
- data/lib/better_auth/social_providers/atlassian.rb +1 -0
- data/lib/better_auth/social_providers/base.rb +41 -14
- data/lib/better_auth/social_providers/cognito.rb +75 -5
- data/lib/better_auth/social_providers/facebook.rb +95 -1
- data/lib/better_auth/social_providers/figma.rb +2 -0
- data/lib/better_auth/social_providers/google.rb +12 -1
- data/lib/better_auth/social_providers/line.rb +35 -1
- data/lib/better_auth/social_providers/linear.rb +1 -0
- data/lib/better_auth/social_providers/microsoft_entra_id.rb +9 -0
- data/lib/better_auth/social_providers/naver.rb +24 -1
- data/lib/better_auth/social_providers/notion.rb +1 -0
- data/lib/better_auth/social_providers/paybin.rb +25 -1
- data/lib/better_auth/social_providers/paypal.rb +118 -2
- data/lib/better_auth/social_providers/reddit.rb +25 -4
- data/lib/better_auth/social_providers/salesforce.rb +1 -0
- data/lib/better_auth/social_providers/twitch.rb +24 -1
- data/lib/better_auth/social_providers/twitter.rb +36 -1
- data/lib/better_auth/social_providers/vk.rb +34 -1
- data/lib/better_auth/social_providers/wechat.rb +6 -4
- data/lib/better_auth/sql_migration.rb +5 -5
- data/lib/better_auth/version.rb +1 -1
- data/lib/better_auth.rb +11 -41
- metadata +5 -13
- data/lib/better_auth/plugins/mcp/authorization.rb +0 -111
- data/lib/better_auth/plugins/mcp/config.rb +0 -51
- data/lib/better_auth/plugins/mcp/consent.rb +0 -31
- data/lib/better_auth/plugins/mcp/legacy_aliases.rb +0 -43
- data/lib/better_auth/plugins/mcp/metadata.rb +0 -81
- data/lib/better_auth/plugins/mcp/registration.rb +0 -31
- data/lib/better_auth/plugins/mcp/resource_handler.rb +0 -37
- data/lib/better_auth/plugins/mcp/schema.rb +0 -91
- data/lib/better_auth/plugins/mcp/token.rb +0 -108
- data/lib/better_auth/plugins/mcp/userinfo.rb +0 -37
- data/lib/better_auth/plugins/mcp.rb +0 -256
- data/lib/better_auth/plugins/oidc_provider.rb +0 -769
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "json"
|
|
4
4
|
require "securerandom"
|
|
5
5
|
require "time"
|
|
6
|
+
require "monitor"
|
|
6
7
|
|
|
7
8
|
module BetterAuth
|
|
8
9
|
module Adapters
|
|
@@ -13,6 +14,8 @@ module BetterAuth
|
|
|
13
14
|
@adapter = adapter
|
|
14
15
|
@options = options
|
|
15
16
|
@hooks = DatabaseHooks.new(adapter, options)
|
|
17
|
+
@verification_locks_guard = Mutex.new
|
|
18
|
+
@verification_locks = {}
|
|
16
19
|
end
|
|
17
20
|
|
|
18
21
|
def create_oauth_user(user, account, context: nil)
|
|
@@ -63,7 +66,7 @@ module BetterAuth
|
|
|
63
66
|
return false if deleted == false
|
|
64
67
|
|
|
65
68
|
hooks.delete_many([{field: "userId", value: user_id}], "account")
|
|
66
|
-
|
|
69
|
+
delete_user_sessions(user_id)
|
|
67
70
|
deleted
|
|
68
71
|
end
|
|
69
72
|
|
|
@@ -143,20 +146,24 @@ module BetterAuth
|
|
|
143
146
|
hooks.delete([{field: "token", value: token}], "session")
|
|
144
147
|
end
|
|
145
148
|
|
|
146
|
-
def
|
|
149
|
+
def delete_user_sessions(user_id)
|
|
147
150
|
if secondary_storage
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
151
|
+
active_session_entries(user_id).each { |entry| secondary_storage.delete(entry["token"]) }
|
|
152
|
+
secondary_storage.delete(active_key(user_id))
|
|
153
|
+
return if !options.session[:store_session_in_database] || options.session[:preserve_session_in_database]
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
hooks.delete_many([{field: "userId", value: user_id}], "session")
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def delete_sessions(tokens)
|
|
160
|
+
session_tokens = Array(tokens)
|
|
161
|
+
if secondary_storage
|
|
162
|
+
session_tokens.each { |token| secondary_storage.delete(token) }
|
|
154
163
|
return if !options.session[:store_session_in_database] || options.session[:preserve_session_in_database]
|
|
155
164
|
end
|
|
156
165
|
|
|
157
|
-
|
|
158
|
-
operator = user_id_or_tokens.is_a?(Array) ? "in" : nil
|
|
159
|
-
hooks.delete_many([{field: field, value: user_id_or_tokens, operator: operator}], "session")
|
|
166
|
+
hooks.delete_many([{field: "token", value: session_tokens, operator: "in"}], "session")
|
|
160
167
|
end
|
|
161
168
|
|
|
162
169
|
def delete_accounts(user_id)
|
|
@@ -167,6 +174,26 @@ module BetterAuth
|
|
|
167
174
|
hooks.delete([{field: "id", value: account_id}], "account")
|
|
168
175
|
end
|
|
169
176
|
|
|
177
|
+
# Removes access established before control of an email address was proven.
|
|
178
|
+
# The user is intentionally left unverified until the caller completes the
|
|
179
|
+
# proof transition and may safely mint a replacement session.
|
|
180
|
+
def revoke_unproven_account_access(user_id)
|
|
181
|
+
adapter.transaction do
|
|
182
|
+
user = find_user_by_id(user_id)
|
|
183
|
+
next unless user && !user["emailVerified"]
|
|
184
|
+
|
|
185
|
+
find_accounts(user_id).each do |account|
|
|
186
|
+
next unless account["providerId"] == "credential"
|
|
187
|
+
|
|
188
|
+
deleted = delete_account(account.fetch("id"))
|
|
189
|
+
raise Error, "Credential account revocation was vetoed" if deleted == false
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
deleted_sessions = delete_user_sessions(user_id)
|
|
193
|
+
raise Error, "Session revocation was vetoed" if deleted_sessions == false
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
170
197
|
def find_oauth_user(email, account_id, provider_id)
|
|
171
198
|
account = find_account_with_user(account_id, provider_id)
|
|
172
199
|
if account
|
|
@@ -277,6 +304,56 @@ module BetterAuth
|
|
|
277
304
|
values.first
|
|
278
305
|
end
|
|
279
306
|
|
|
307
|
+
def consume_verification_value(identifier)
|
|
308
|
+
identifiers = verification_identifiers(identifier)
|
|
309
|
+
consumed = if secondary_storage && !verification_store_in_database?
|
|
310
|
+
consume_secondary_verification(identifiers)
|
|
311
|
+
else
|
|
312
|
+
consume_database_verification(identifiers)
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
invalidate_secondary_verification(identifiers, consumed) if consumed && secondary_storage && verification_store_in_database?
|
|
316
|
+
return nil unless consumed
|
|
317
|
+
|
|
318
|
+
expires_at = normalize_time(consumed["expiresAt"] || consumed[:expiresAt])
|
|
319
|
+
(expires_at > Time.now) ? normalize_verification_dates(consumed) : nil
|
|
320
|
+
rescue ArgumentError, TypeError
|
|
321
|
+
nil
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def reserve_verification_value(data)
|
|
325
|
+
payload = stringify_keys(data)
|
|
326
|
+
identifier = payload.fetch("identifier")
|
|
327
|
+
stored_identifier = processed_verification_identifier(identifier)
|
|
328
|
+
reservation_id = Crypto.sha256("reserve:#{identifier}", encoding: :base64url)
|
|
329
|
+
verification = timestamps.merge(
|
|
330
|
+
"id" => reservation_id,
|
|
331
|
+
"identifier" => stored_identifier,
|
|
332
|
+
"value" => payload.fetch("value"),
|
|
333
|
+
"expiresAt" => normalize_time(payload.fetch("expiresAt"))
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
if secondary_storage && !verification_store_in_database?
|
|
337
|
+
return reserve_secondary_verification(verification)
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
adapter.consume_one(
|
|
341
|
+
model: "verification",
|
|
342
|
+
where: [
|
|
343
|
+
{field: "id", value: reservation_id},
|
|
344
|
+
{field: "expiresAt", value: Time.now, operator: "lt"}
|
|
345
|
+
]
|
|
346
|
+
)
|
|
347
|
+
created = adapter.create_if_absent(
|
|
348
|
+
model: "verification",
|
|
349
|
+
data: verification,
|
|
350
|
+
conflict_field: "id",
|
|
351
|
+
force_allow_id: true
|
|
352
|
+
)
|
|
353
|
+
store_reserved_verification(verification) if created && secondary_storage
|
|
354
|
+
created
|
|
355
|
+
end
|
|
356
|
+
|
|
280
357
|
def delete_verification_value(id)
|
|
281
358
|
if secondary_storage
|
|
282
359
|
stored_identifier = secondary_storage.get(verification_id_key(id))
|
|
@@ -325,6 +402,106 @@ module BetterAuth
|
|
|
325
402
|
|
|
326
403
|
private
|
|
327
404
|
|
|
405
|
+
def verification_identifiers(identifier)
|
|
406
|
+
stored_identifier = processed_verification_identifier(identifier)
|
|
407
|
+
storage_option = verification_storage_option(identifier)
|
|
408
|
+
if storage_option && storage_option.to_s != "plain" && stored_identifier != identifier.to_s
|
|
409
|
+
[stored_identifier, identifier.to_s]
|
|
410
|
+
else
|
|
411
|
+
[stored_identifier]
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
def consume_secondary_verification(identifiers)
|
|
416
|
+
identifiers.each do |stored_identifier|
|
|
417
|
+
key = verification_key(stored_identifier)
|
|
418
|
+
raw = if secondary_storage.respond_to?(:get_and_delete)
|
|
419
|
+
secondary_storage.get_and_delete(key)
|
|
420
|
+
elsif secondary_storage.respond_to?(:getAndDelete)
|
|
421
|
+
secondary_storage.getAndDelete(key)
|
|
422
|
+
else
|
|
423
|
+
raise Error, "Secondary storage must implement atomic `get_and_delete`/`getAndDelete`, or verification values must be database-backed"
|
|
424
|
+
end
|
|
425
|
+
consumed = begin
|
|
426
|
+
parsed = parse_storage(raw)
|
|
427
|
+
parsed ? normalize_verification_dates(parsed) : nil
|
|
428
|
+
rescue ArgumentError, TypeError
|
|
429
|
+
nil
|
|
430
|
+
end
|
|
431
|
+
next unless consumed
|
|
432
|
+
|
|
433
|
+
invalidate_secondary_verification(identifiers, consumed)
|
|
434
|
+
return consumed
|
|
435
|
+
end
|
|
436
|
+
nil
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
def reserve_secondary_verification(verification)
|
|
440
|
+
key = verification_key(verification.fetch("identifier"))
|
|
441
|
+
ttl_seconds = ttl(millis(verification.fetch("expiresAt")))
|
|
442
|
+
return false unless ttl_seconds.positive?
|
|
443
|
+
|
|
444
|
+
value = JSON.generate(normalize_verification_dates(verification))
|
|
445
|
+
created = if secondary_storage.respond_to?(:set_if_absent)
|
|
446
|
+
secondary_storage.set_if_absent(key, value, ttl_seconds)
|
|
447
|
+
elsif secondary_storage.respond_to?(:setIfAbsent)
|
|
448
|
+
secondary_storage.setIfAbsent(key, value, ttl_seconds)
|
|
449
|
+
else
|
|
450
|
+
raise Error, "Secondary storage must implement atomic `set_if_absent`/`setIfAbsent`, or verification values must be database-backed"
|
|
451
|
+
end
|
|
452
|
+
return false unless created
|
|
453
|
+
true
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
def consume_database_verification(identifiers)
|
|
457
|
+
identifiers.each do |stored_identifier|
|
|
458
|
+
consumed = with_verification_lock(verification_key(stored_identifier)) do
|
|
459
|
+
adapter.transaction do |transaction_adapter|
|
|
460
|
+
where = [{field: "identifier", value: stored_identifier}]
|
|
461
|
+
latest = transaction_adapter.find_many(
|
|
462
|
+
model: "verification",
|
|
463
|
+
where: where,
|
|
464
|
+
sort_by: {field: "createdAt", direction: "desc"},
|
|
465
|
+
limit: 1
|
|
466
|
+
).first
|
|
467
|
+
next nil unless latest
|
|
468
|
+
next nil if verification_delete_vetoed?(latest)
|
|
469
|
+
|
|
470
|
+
row = transaction_adapter.consume_one(
|
|
471
|
+
model: "verification",
|
|
472
|
+
where: [{field: "id", value: latest.fetch("id")}]
|
|
473
|
+
)
|
|
474
|
+
next nil unless row
|
|
475
|
+
|
|
476
|
+
transaction_adapter.delete_many(model: "verification", where: where)
|
|
477
|
+
run_verification_delete_after(row)
|
|
478
|
+
row
|
|
479
|
+
end
|
|
480
|
+
end
|
|
481
|
+
return consumed if consumed
|
|
482
|
+
end
|
|
483
|
+
nil
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
def invalidate_secondary_verification(identifiers, consumed)
|
|
487
|
+
identifiers.each { |stored_identifier| secondary_storage.delete(verification_key(stored_identifier)) }
|
|
488
|
+
id = consumed && (consumed["id"] || consumed[:id])
|
|
489
|
+
secondary_storage.delete(verification_id_key(id)) if id
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
def verification_delete_vetoed?(entity)
|
|
493
|
+
hooks.send(:before_hooks, "verification", :delete).any? { |hook| hook.call(entity, nil) == false }
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
def run_verification_delete_after(entity)
|
|
497
|
+
hooks.send(:after_hooks, "verification", :delete).each { |hook| hook.call(entity, nil) }
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
def with_verification_lock(key)
|
|
501
|
+
lock = @verification_locks_guard.synchronize { @verification_locks[key] ||= Monitor.new }
|
|
502
|
+
lock.synchronize { yield }
|
|
503
|
+
end
|
|
504
|
+
|
|
328
505
|
def secondary_storage
|
|
329
506
|
options.secondary_storage
|
|
330
507
|
end
|
|
@@ -527,6 +704,15 @@ module BetterAuth
|
|
|
527
704
|
normalized
|
|
528
705
|
end
|
|
529
706
|
|
|
707
|
+
def store_reserved_verification(verification)
|
|
708
|
+
normalized = normalize_verification_dates(verification)
|
|
709
|
+
ttl_seconds = ttl(millis(normalized["expiresAt"]))
|
|
710
|
+
return normalized unless ttl_seconds.positive?
|
|
711
|
+
|
|
712
|
+
secondary_storage.set(verification_key(normalized["identifier"]), JSON.generate(normalized), ttl_seconds)
|
|
713
|
+
normalized
|
|
714
|
+
end
|
|
715
|
+
|
|
530
716
|
def read_verification(identifier)
|
|
531
717
|
normalize_verification_dates(parse_storage(secondary_storage.get(verification_key(identifier))))
|
|
532
718
|
end
|
|
@@ -58,6 +58,16 @@ module BetterAuth
|
|
|
58
58
|
end
|
|
59
59
|
end
|
|
60
60
|
end
|
|
61
|
+
|
|
62
|
+
def join_limit(config)
|
|
63
|
+
value = config[:limit]
|
|
64
|
+
return 100 if value.nil?
|
|
65
|
+
|
|
66
|
+
parsed = Integer(value)
|
|
67
|
+
parsed.positive? ? parsed : 100
|
|
68
|
+
rescue ArgumentError, TypeError
|
|
69
|
+
100
|
|
70
|
+
end
|
|
61
71
|
end
|
|
62
72
|
end
|
|
63
73
|
end
|
|
@@ -2,23 +2,52 @@
|
|
|
2
2
|
|
|
3
3
|
require "securerandom"
|
|
4
4
|
require "time"
|
|
5
|
+
require "monitor"
|
|
5
6
|
|
|
6
7
|
module BetterAuth
|
|
7
8
|
module Adapters
|
|
8
9
|
class Memory < Base
|
|
9
10
|
include JoinSupport
|
|
10
11
|
|
|
12
|
+
LOCK_REGISTRY_GUARD = Mutex.new
|
|
13
|
+
LOCKS_BY_DATABASE = {}
|
|
14
|
+
|
|
15
|
+
class << self
|
|
16
|
+
def lock_for(database)
|
|
17
|
+
LOCK_REGISTRY_GUARD.synchronize do
|
|
18
|
+
LOCKS_BY_DATABASE[database.object_id] ||= Monitor.new
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
11
23
|
attr_reader :db
|
|
12
24
|
|
|
13
25
|
def initialize(options, db = nil)
|
|
14
26
|
super(options)
|
|
15
27
|
@db = db || build_db
|
|
28
|
+
@lock = self.class.lock_for(@db)
|
|
16
29
|
end
|
|
17
30
|
|
|
18
31
|
def create(model:, data:, force_allow_id: false)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
32
|
+
@lock.synchronize do
|
|
33
|
+
model = model.to_s
|
|
34
|
+
table_for(model) << transform_input(model, data, "create", force_allow_id)
|
|
35
|
+
table_for(model).last
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def create_if_absent(model:, data:, conflict_field: "id", force_allow_id: true)
|
|
40
|
+
@lock.synchronize do
|
|
41
|
+
model = model.to_s
|
|
42
|
+
fields = Schema.auth_tables(options).fetch(model).fetch(:fields)
|
|
43
|
+
field = atomic_schema_field(fields, conflict_field)
|
|
44
|
+
input = transform_input(model, data, "create", force_allow_id)
|
|
45
|
+
raise APIError.new("BAD_REQUEST", message: "Missing conflict field #{conflict_field}") unless input.key?(field)
|
|
46
|
+
next false if table_for(model).any? { |record| record[field] == input[field] }
|
|
47
|
+
|
|
48
|
+
table_for(model) << input
|
|
49
|
+
true
|
|
50
|
+
end
|
|
22
51
|
end
|
|
23
52
|
|
|
24
53
|
def find_one(model:, where: [], select: nil, join: nil)
|
|
@@ -26,34 +55,42 @@ module BetterAuth
|
|
|
26
55
|
end
|
|
27
56
|
|
|
28
57
|
def find_many(model:, where: [], sort_by: nil, limit: nil, offset: nil, select: nil, join: nil)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
58
|
+
@lock.synchronize do
|
|
59
|
+
model = model.to_s
|
|
60
|
+
records = table_for(model).select { |record| matches_where?(record, where || []) }.map(&:dup)
|
|
61
|
+
records = records.map { |record| apply_join(model, record, join) } if join
|
|
62
|
+
records = sort_records(model, records, sort_by) if sort_by
|
|
63
|
+
records = records.drop(offset.to_i) if offset
|
|
64
|
+
records = records.first(limit.to_i) if limit
|
|
65
|
+
records = records.map { |record| select_fields(model, record, select, join: join) } if select && !select.empty?
|
|
66
|
+
records
|
|
67
|
+
end
|
|
37
68
|
end
|
|
38
69
|
|
|
39
70
|
def update(model:, where:, update:)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
71
|
+
@lock.synchronize do
|
|
72
|
+
model = model.to_s
|
|
73
|
+
next nil if Array(where).empty?
|
|
74
|
+
|
|
75
|
+
ensure_update_input_has_fields!(model, update)
|
|
76
|
+
records = table_for(model).select { |record| matches_where?(record, where) }
|
|
77
|
+
data = transform_input(model, update, "update", true)
|
|
78
|
+
ensure_update_data!(data)
|
|
79
|
+
records.each { |record| record.merge!(data) }
|
|
80
|
+
records.first
|
|
81
|
+
end
|
|
47
82
|
end
|
|
48
83
|
|
|
49
84
|
def update_many(model:, where:, update:)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
85
|
+
@lock.synchronize do
|
|
86
|
+
model = model.to_s
|
|
87
|
+
ensure_update_input_has_fields!(model, update)
|
|
88
|
+
records = table_for(model).select { |record| matches_where?(record, where || []) }
|
|
89
|
+
data = transform_input(model, update, "update", true)
|
|
90
|
+
ensure_update_data!(data)
|
|
91
|
+
records.each { |record| record.merge!(data) }
|
|
92
|
+
records.length
|
|
93
|
+
end
|
|
57
94
|
end
|
|
58
95
|
|
|
59
96
|
def delete(model:, where:)
|
|
@@ -62,26 +99,103 @@ module BetterAuth
|
|
|
62
99
|
end
|
|
63
100
|
|
|
64
101
|
def delete_many(model:, where:)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
102
|
+
@lock.synchronize do
|
|
103
|
+
table = table_for(model)
|
|
104
|
+
matches = table.select { |record| matches_where?(record, where || []) }
|
|
105
|
+
@db[model.to_s] = table.reject { |record| matches.include?(record) }
|
|
106
|
+
matches.length
|
|
107
|
+
end
|
|
69
108
|
end
|
|
70
109
|
|
|
71
110
|
def count(model:, where: nil)
|
|
72
111
|
find_many(model: model, where: where || []).length
|
|
73
112
|
end
|
|
74
113
|
|
|
114
|
+
def consume_one(model:, where:)
|
|
115
|
+
@lock.synchronize do
|
|
116
|
+
table = table_for(model)
|
|
117
|
+
index = table.index { |record| matches_where?(record, where || []) }
|
|
118
|
+
index ? table.delete_at(index) : nil
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def increment_one(model:, where:, increment:, set: nil, allow_server_managed: false)
|
|
123
|
+
@lock.synchronize do
|
|
124
|
+
model = model.to_s
|
|
125
|
+
increments = normalized_increments(model, increment, allow_server_managed)
|
|
126
|
+
assignments = normalized_increment_set(model, set)
|
|
127
|
+
raise APIError.new("BAD_REQUEST", message: "increment_one requires a non-empty increment or set") if increments.empty? && assignments.empty?
|
|
128
|
+
|
|
129
|
+
target = table_for(model).find { |record| matches_where?(record, where || []) }
|
|
130
|
+
next nil unless target
|
|
131
|
+
|
|
132
|
+
increments.each do |field, delta|
|
|
133
|
+
current = target[field]
|
|
134
|
+
target[field] = (current.is_a?(Numeric) ? current : 0) + delta
|
|
135
|
+
end
|
|
136
|
+
target.merge!(assignments)
|
|
137
|
+
target
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
75
141
|
def transaction
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
142
|
+
return yield active_transaction_adapter if active_transaction_adapter
|
|
143
|
+
|
|
144
|
+
@lock.synchronize do
|
|
145
|
+
snapshot = transaction_snapshot(db)
|
|
146
|
+
begin
|
|
147
|
+
with_transaction_context(self) { yield self }
|
|
148
|
+
rescue
|
|
149
|
+
db.replace(snapshot)
|
|
150
|
+
raise
|
|
151
|
+
end
|
|
152
|
+
end
|
|
81
153
|
end
|
|
82
154
|
|
|
83
155
|
private
|
|
84
156
|
|
|
157
|
+
def transaction_snapshot(value, copies = {})
|
|
158
|
+
object_id = value.object_id
|
|
159
|
+
return copies.fetch(object_id) if copies.key?(object_id)
|
|
160
|
+
|
|
161
|
+
copy = case value
|
|
162
|
+
when Hash
|
|
163
|
+
value.dup.tap do |hash|
|
|
164
|
+
hash.clear
|
|
165
|
+
copies[object_id] = hash
|
|
166
|
+
if value.default_proc
|
|
167
|
+
hash.default_proc = value.default_proc
|
|
168
|
+
else
|
|
169
|
+
hash.default = transaction_snapshot(value.default, copies)
|
|
170
|
+
end
|
|
171
|
+
value.each do |key, child|
|
|
172
|
+
hash[transaction_snapshot(key, copies)] = transaction_snapshot(child, copies)
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
when Array
|
|
176
|
+
value.dup.tap do |array|
|
|
177
|
+
array.clear
|
|
178
|
+
copies[object_id] = array
|
|
179
|
+
value.each { |child| array << transaction_snapshot(child, copies) }
|
|
180
|
+
end
|
|
181
|
+
when Struct
|
|
182
|
+
value.dup.tap do |struct|
|
|
183
|
+
copies[object_id] = struct
|
|
184
|
+
value.each_pair do |member, child|
|
|
185
|
+
struct[member] = transaction_snapshot(child, copies)
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
when String, Time
|
|
189
|
+
value.dup.tap { |object| copies[object_id] = object }
|
|
190
|
+
when Proc, Method, UnboundMethod, NilClass, TrueClass, FalseClass, Symbol, Numeric
|
|
191
|
+
return value
|
|
192
|
+
else
|
|
193
|
+
raise TypeError, "Unsupported Memory transaction snapshot value: #{value.class}"
|
|
194
|
+
end
|
|
195
|
+
copy.freeze if value.frozen?
|
|
196
|
+
copy
|
|
197
|
+
end
|
|
198
|
+
|
|
85
199
|
def build_db
|
|
86
200
|
Schema.auth_tables(options).keys.to_h { |model| [model, []] }
|
|
87
201
|
end
|
|
@@ -141,6 +255,40 @@ module BetterAuth
|
|
|
141
255
|
raise APIError.new("BAD_REQUEST", message: "No fields to update") unless has_updatable_field
|
|
142
256
|
end
|
|
143
257
|
|
|
258
|
+
def normalized_increments(model, increment, allow_server_managed)
|
|
259
|
+
raise APIError.new("BAD_REQUEST", message: "increment must be a Hash") unless increment.is_a?(Hash)
|
|
260
|
+
|
|
261
|
+
fields = Schema.auth_tables(options).fetch(model).fetch(:fields)
|
|
262
|
+
increment.each_with_object({}) do |(field, delta), result|
|
|
263
|
+
logical_field = atomic_schema_field(fields, field)
|
|
264
|
+
attributes = fields[logical_field]
|
|
265
|
+
valid_field = attributes && logical_field != "id" && attributes[:type] == "number"
|
|
266
|
+
valid_field = false if attributes && attributes[:input] == false && allow_server_managed != true
|
|
267
|
+
unless valid_field
|
|
268
|
+
raise APIError.new("BAD_REQUEST", message: "Invalid increment field #{field}; expected a mutable numeric field")
|
|
269
|
+
end
|
|
270
|
+
if !delta.is_a?(Numeric) || (delta.respond_to?(:finite?) && !delta.finite?)
|
|
271
|
+
raise APIError.new("BAD_REQUEST", message: "Increment delta for #{field} must be numeric")
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
result[logical_field] = delta
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def normalized_increment_set(model, set)
|
|
279
|
+
return {} if set.nil? || set.empty?
|
|
280
|
+
|
|
281
|
+
ensure_update_input_has_fields!(model, set)
|
|
282
|
+
transform_input(model, set, "update", true)
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def atomic_schema_field(fields, field)
|
|
286
|
+
candidate = Schema.storage_key(field)
|
|
287
|
+
return candidate if fields.key?(candidate)
|
|
288
|
+
|
|
289
|
+
fields.find { |logical, attributes| Schema.storage_key(attributes[:field_name] || logical) == candidate }&.first
|
|
290
|
+
end
|
|
291
|
+
|
|
144
292
|
def generated_id(model)
|
|
145
293
|
generator = options.advanced.dig(:database, :generate_id)
|
|
146
294
|
return generator.call.to_s if generator.respond_to?(:call)
|
|
@@ -166,16 +314,9 @@ module BetterAuth
|
|
|
166
314
|
clauses = Array(where)
|
|
167
315
|
return true if clauses.empty?
|
|
168
316
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
if fetch_key(clause, :connector).to_s.upcase == "OR"
|
|
173
|
-
result ||= clause_result
|
|
174
|
-
else
|
|
175
|
-
result &&= clause_result
|
|
176
|
-
end
|
|
177
|
-
end
|
|
178
|
-
result
|
|
317
|
+
and_clauses, or_clauses = grouped_where_clauses(clauses)
|
|
318
|
+
and_clauses.all? { |clause| evaluate_clause(record, clause) } &&
|
|
319
|
+
(or_clauses.empty? || or_clauses.any? { |clause| evaluate_clause(record, clause) })
|
|
179
320
|
end
|
|
180
321
|
|
|
181
322
|
def evaluate_clause(record, clause)
|
|
@@ -301,9 +442,13 @@ module BetterAuth
|
|
|
301
442
|
value.nil? ? "" : value
|
|
302
443
|
end
|
|
303
444
|
|
|
304
|
-
def select_fields(
|
|
445
|
+
def select_fields(model, record, select, join: nil)
|
|
305
446
|
fields = Array(select).map { |field| Schema.storage_key(field) }
|
|
306
|
-
record.slice(*fields)
|
|
447
|
+
selected = record.slice(*fields)
|
|
448
|
+
normalized_join(model, join).each_key do |join_model|
|
|
449
|
+
selected[join_model] = record[join_model] if record.key?(join_model)
|
|
450
|
+
end
|
|
451
|
+
selected
|
|
307
452
|
end
|
|
308
453
|
|
|
309
454
|
def apply_join(model, record, join)
|
|
@@ -326,16 +471,6 @@ module BetterAuth
|
|
|
326
471
|
config[:relation] == "one-to-one" || config[:unique] == true
|
|
327
472
|
end
|
|
328
473
|
|
|
329
|
-
def join_limit(config)
|
|
330
|
-
value = config[:limit]
|
|
331
|
-
return 100 if value.nil?
|
|
332
|
-
|
|
333
|
-
parsed = Integer(value)
|
|
334
|
-
parsed.positive? ? parsed : 100
|
|
335
|
-
rescue ArgumentError, TypeError
|
|
336
|
-
100
|
|
337
|
-
end
|
|
338
|
-
|
|
339
474
|
def inferred_join_config(model, join_model)
|
|
340
475
|
foreign_keys = schema_for(join_model).fetch(:fields).select do |_field, attributes|
|
|
341
476
|
reference_model_matches?(attributes, model)
|
|
@@ -17,16 +17,20 @@ module BetterAuth
|
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
def transaction
|
|
20
|
+
return yield active_transaction_adapter if active_transaction_adapter
|
|
20
21
|
return super unless connection.respond_to?(:transaction)
|
|
21
22
|
|
|
22
|
-
connection.transaction { yield self }
|
|
23
|
+
connection.transaction { with_transaction_context(self) { yield self } }
|
|
23
24
|
end
|
|
24
25
|
|
|
25
26
|
private
|
|
26
27
|
|
|
27
|
-
def execute(sql, params)
|
|
28
|
+
def execute(sql, params, affected_rows_result: false)
|
|
28
29
|
if connection.respond_to?(:fetch)
|
|
29
|
-
connection.fetch(sql, *params)
|
|
30
|
+
dataset = connection.fetch(sql, *params)
|
|
31
|
+
return dataset.delete if affected_rows_result
|
|
32
|
+
|
|
33
|
+
dataset.all.map { |row| stringify_row(row) }
|
|
30
34
|
else
|
|
31
35
|
super
|
|
32
36
|
end
|