better_auth 0.8.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 +92 -0
- data/LICENSE.md +1 -1
- data/README.md +142 -95
- 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 +303 -54
- data/lib/better_auth/adapters/mssql.rb +7 -3
- data/lib/better_auth/adapters/sql.rb +390 -63
- data/lib/better_auth/api.rb +3 -24
- data/lib/better_auth/configuration.rb +48 -47
- data/lib/better_auth/context.rb +43 -67
- data/lib/better_auth/cookies.rb +119 -15
- data/lib/better_auth/core.rb +0 -2
- data/lib/better_auth/crypto.rb +4 -3
- data/lib/better_auth/doctor.rb +108 -0
- data/lib/better_auth/endpoint.rb +88 -5
- data/lib/better_auth/error.rb +3 -1
- data/lib/better_auth/http_client.rb +46 -0
- 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 +15 -0
- data/lib/better_auth/oauth2.rb +1 -1
- 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 +85 -29
- data/lib/better_auth/plugins/anonymous.rb +2 -0
- 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 +77 -5
- data/lib/better_auth/plugins/dub.rb +10 -2
- data/lib/better_auth/plugins/email_otp.rb +30 -47
- data/lib/better_auth/plugins/generic_oauth.rb +162 -140
- data/lib/better_auth/plugins/have_i_been_pwned.rb +1 -1
- data/lib/better_auth/plugins/i18n.rb +215 -0
- data/lib/better_auth/plugins/jwt.rb +44 -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 +233 -46
- 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 +78 -65
- data/lib/better_auth/plugins/one_time_token.rb +1 -4
- data/lib/better_auth/plugins/open_api.rb +77 -9
- data/lib/better_auth/plugins/organization.rb +546 -102
- data/lib/better_auth/plugins/passkey.rb +4 -7
- data/lib/better_auth/plugins/phone_number.rb +25 -27
- 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 +205 -19
- data/lib/better_auth/plugins/username.rb +56 -13
- data/lib/better_auth/plugins.rb +57 -0
- data/lib/better_auth/rate_limiter.rb +300 -133
- data/lib/better_auth/request_ip.rb +123 -13
- data/lib/better_auth/router.rb +3 -3
- data/lib/better_auth/routes/account.rb +44 -8
- data/lib/better_auth/routes/email_verification.rb +93 -6
- data/lib/better_auth/routes/password.rb +6 -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 +197 -54
- data/lib/better_auth/routes/user.rb +17 -10
- data/lib/better_auth/schema/sql.rb +189 -34
- data/lib/better_auth/schema.rb +159 -5
- data/lib/better_auth/session.rb +29 -10
- 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 +45 -23
- 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/github.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 +566 -0
- data/lib/better_auth/version.rb +1 -1
- data/lib/better_auth.rb +14 -41
- metadata +14 -18
- 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 -215
- data/lib/better_auth/plugins/oidc_provider.rb +0 -710
|
@@ -2,21 +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
|
|
10
|
+
include JoinSupport
|
|
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
|
+
|
|
9
23
|
attr_reader :db
|
|
10
24
|
|
|
11
25
|
def initialize(options, db = nil)
|
|
12
26
|
super(options)
|
|
13
27
|
@db = db || build_db
|
|
28
|
+
@lock = self.class.lock_for(@db)
|
|
14
29
|
end
|
|
15
30
|
|
|
16
31
|
def create(model:, data:, force_allow_id: false)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
20
51
|
end
|
|
21
52
|
|
|
22
53
|
def find_one(model:, where: [], select: nil, join: nil)
|
|
@@ -24,28 +55,42 @@ module BetterAuth
|
|
|
24
55
|
end
|
|
25
56
|
|
|
26
57
|
def find_many(model:, where: [], sort_by: nil, limit: nil, offset: nil, select: nil, join: nil)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
35
68
|
end
|
|
36
69
|
|
|
37
70
|
def update(model:, where:, update:)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
|
42
82
|
end
|
|
43
83
|
|
|
44
84
|
def update_many(model:, where:, update:)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
49
94
|
end
|
|
50
95
|
|
|
51
96
|
def delete(model:, where:)
|
|
@@ -54,26 +99,103 @@ module BetterAuth
|
|
|
54
99
|
end
|
|
55
100
|
|
|
56
101
|
def delete_many(model:, where:)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
61
108
|
end
|
|
62
109
|
|
|
63
110
|
def count(model:, where: nil)
|
|
64
111
|
find_many(model: model, where: where || []).length
|
|
65
112
|
end
|
|
66
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
|
+
|
|
67
141
|
def transaction
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
73
153
|
end
|
|
74
154
|
|
|
75
155
|
private
|
|
76
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
|
+
|
|
77
199
|
def build_db
|
|
78
200
|
Schema.auth_tables(options).keys.to_h { |model| [model, []] }
|
|
79
201
|
end
|
|
@@ -109,16 +231,68 @@ module BetterAuth
|
|
|
109
231
|
raise APIError.new("BAD_REQUEST", message: "#{field} is required") unless field == "id"
|
|
110
232
|
end
|
|
111
233
|
|
|
112
|
-
output[field] = coerce_value(value, attributes) if value_provided
|
|
234
|
+
output[field] = coerce_value(value, field, attributes) if value_provided
|
|
113
235
|
end
|
|
114
236
|
|
|
115
|
-
output["id"] = generated_id if action == "create" && !output.key?("id")
|
|
237
|
+
output["id"] = generated_id(model) if action == "create" && fields.key?("id") && (!output.key?("id") || output["id"].nil?)
|
|
116
238
|
output
|
|
117
239
|
end
|
|
118
240
|
|
|
119
|
-
def
|
|
241
|
+
def ensure_update_data!(data)
|
|
242
|
+
raise APIError.new("BAD_REQUEST", message: "No fields to update") if data.empty?
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def ensure_update_input_has_fields!(model, update)
|
|
246
|
+
raise APIError.new("BAD_REQUEST", message: "No fields to update") unless update.is_a?(Hash)
|
|
247
|
+
|
|
248
|
+
fields = Schema.auth_tables(options).fetch(model).fetch(:fields)
|
|
249
|
+
input = stringify_keys(update)
|
|
250
|
+
has_updatable_field = input.any? do |field, _value|
|
|
251
|
+
next false if field == "id" || field == "_id"
|
|
252
|
+
|
|
253
|
+
fields.key?(field) || fields.any? { |logical_field, attributes| Schema.storage_key(attributes[:field_name] || logical_field) == field }
|
|
254
|
+
end
|
|
255
|
+
raise APIError.new("BAD_REQUEST", message: "No fields to update") unless has_updatable_field
|
|
256
|
+
end
|
|
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
|
+
|
|
292
|
+
def generated_id(model)
|
|
120
293
|
generator = options.advanced.dig(:database, :generate_id)
|
|
121
294
|
return generator.call.to_s if generator.respond_to?(:call)
|
|
295
|
+
return table_for(model).length + 1 if generator == "serial"
|
|
122
296
|
return SecureRandom.uuid if generator == "uuid"
|
|
123
297
|
|
|
124
298
|
SecureRandom.hex(16)
|
|
@@ -128,8 +302,9 @@ module BetterAuth
|
|
|
128
302
|
default.respond_to?(:call) ? default.call : default
|
|
129
303
|
end
|
|
130
304
|
|
|
131
|
-
def coerce_value(value, attributes)
|
|
305
|
+
def coerce_value(value, field, attributes)
|
|
132
306
|
return value if value.nil?
|
|
307
|
+
return coerce_number(value) if serial_id_field?(field, attributes)
|
|
133
308
|
return Time.parse(value) if attributes[:type] == "date" && value.is_a?(String)
|
|
134
309
|
|
|
135
310
|
value
|
|
@@ -139,24 +314,19 @@ module BetterAuth
|
|
|
139
314
|
clauses = Array(where)
|
|
140
315
|
return true if clauses.empty?
|
|
141
316
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
if fetch_key(clause, :connector).to_s.upcase == "OR"
|
|
146
|
-
result ||= clause_result
|
|
147
|
-
else
|
|
148
|
-
result &&= clause_result
|
|
149
|
-
end
|
|
150
|
-
end
|
|
151
|
-
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) })
|
|
152
320
|
end
|
|
153
321
|
|
|
154
322
|
def evaluate_clause(record, clause)
|
|
155
323
|
field = Schema.storage_key(fetch_key(clause, :field))
|
|
156
324
|
value = fetch_key(clause, :value)
|
|
157
325
|
operator = (fetch_key(clause, :operator) || "eq").to_s
|
|
326
|
+
mode = (fetch_key(clause, :mode) || "sensitive").to_s
|
|
158
327
|
current = record[field]
|
|
159
328
|
comparable = coerce_where_value(record, field, value, operator)
|
|
329
|
+
current, comparable = insensitive_values(current, comparable) if insensitive_comparison?(mode, current, comparable)
|
|
160
330
|
|
|
161
331
|
case operator
|
|
162
332
|
when "in"
|
|
@@ -187,9 +357,9 @@ module BetterAuth
|
|
|
187
357
|
def coerce_where_value(record, field, value, operator)
|
|
188
358
|
attributes = schema_for_record_field(record, field)
|
|
189
359
|
return value unless attributes
|
|
190
|
-
return Array(value).map { |entry| coerce_scalar_where_value(entry, attributes) } if %w[in not_in].include?(operator)
|
|
360
|
+
return Array(value).map { |entry| coerce_scalar_where_value(entry, field, attributes) } if %w[in not_in].include?(operator)
|
|
191
361
|
|
|
192
|
-
coerce_scalar_where_value(value, attributes)
|
|
362
|
+
coerce_scalar_where_value(value, field, attributes)
|
|
193
363
|
end
|
|
194
364
|
|
|
195
365
|
def schema_for_record_field(record, field)
|
|
@@ -201,8 +371,9 @@ module BetterAuth
|
|
|
201
371
|
nil
|
|
202
372
|
end
|
|
203
373
|
|
|
204
|
-
def coerce_scalar_where_value(value, attributes)
|
|
374
|
+
def coerce_scalar_where_value(value, field, attributes)
|
|
205
375
|
return value if value.nil?
|
|
376
|
+
return coerce_number(value) if serial_id_field?(field, attributes)
|
|
206
377
|
|
|
207
378
|
case attributes[:type]
|
|
208
379
|
when "boolean"
|
|
@@ -221,6 +392,32 @@ module BetterAuth
|
|
|
221
392
|
value
|
|
222
393
|
end
|
|
223
394
|
|
|
395
|
+
def serial_id_field?(field, attributes)
|
|
396
|
+
return false unless options.advanced.dig(:database, :generate_id) == "serial"
|
|
397
|
+
return true if field.to_s == "id"
|
|
398
|
+
|
|
399
|
+
reference = attributes[:references]
|
|
400
|
+
return false unless reference
|
|
401
|
+
|
|
402
|
+
(reference[:field] || reference["field"]).to_s == "id"
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
def insensitive_comparison?(mode, current, comparable)
|
|
406
|
+
return false unless mode == "insensitive"
|
|
407
|
+
|
|
408
|
+
current.is_a?(String) && (comparable.is_a?(String) || comparable.is_a?(Array))
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
def insensitive_values(current, comparable)
|
|
412
|
+
normalized_current = current.downcase
|
|
413
|
+
normalized_comparable = if comparable.is_a?(Array)
|
|
414
|
+
comparable.map { |entry| entry.is_a?(String) ? entry.downcase : entry }
|
|
415
|
+
else
|
|
416
|
+
comparable.downcase
|
|
417
|
+
end
|
|
418
|
+
[normalized_current, normalized_comparable]
|
|
419
|
+
end
|
|
420
|
+
|
|
224
421
|
def coerce_number(value)
|
|
225
422
|
return value unless value.is_a?(String)
|
|
226
423
|
return value.to_i if /\A-?\d+\z/.match?(value)
|
|
@@ -245,25 +442,77 @@ module BetterAuth
|
|
|
245
442
|
value.nil? ? "" : value
|
|
246
443
|
end
|
|
247
444
|
|
|
248
|
-
def select_fields(
|
|
445
|
+
def select_fields(model, record, select, join: nil)
|
|
249
446
|
fields = Array(select).map { |field| Schema.storage_key(field) }
|
|
250
|
-
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
|
|
251
452
|
end
|
|
252
453
|
|
|
253
454
|
def apply_join(model, record, join)
|
|
254
455
|
joined = record.dup
|
|
255
|
-
join.
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
456
|
+
normalized_join(model, join).each do |join_model, config|
|
|
457
|
+
matches = table_for(join_model).select do |join_record|
|
|
458
|
+
join_record[config.fetch(:to)] == record[config.fetch(:from)]
|
|
459
|
+
end.map(&:dup)
|
|
460
|
+
|
|
461
|
+
joined[join_model] = if one_to_one_join?(config)
|
|
462
|
+
matches.first
|
|
463
|
+
else
|
|
464
|
+
matches.first(join_limit(config))
|
|
262
465
|
end
|
|
263
466
|
end
|
|
264
467
|
joined
|
|
265
468
|
end
|
|
266
469
|
|
|
470
|
+
def one_to_one_join?(config)
|
|
471
|
+
config[:relation] == "one-to-one" || config[:unique] == true
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
def inferred_join_config(model, join_model)
|
|
475
|
+
foreign_keys = schema_for(join_model).fetch(:fields).select do |_field, attributes|
|
|
476
|
+
reference_model_matches?(attributes, model)
|
|
477
|
+
end
|
|
478
|
+
forward_join = true
|
|
479
|
+
|
|
480
|
+
if foreign_keys.empty?
|
|
481
|
+
foreign_keys = schema_for(model).fetch(:fields).select do |_field, attributes|
|
|
482
|
+
reference_model_matches?(attributes, join_model)
|
|
483
|
+
end
|
|
484
|
+
forward_join = false
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
raise Error, "No foreign key found for model #{join_model} and base model #{model} while performing join operation." if foreign_keys.empty?
|
|
488
|
+
raise Error, "Multiple foreign keys found for model #{join_model} and base model #{model} while performing join operation. Only one foreign key is supported." if foreign_keys.length > 1
|
|
489
|
+
|
|
490
|
+
foreign_key, attributes = foreign_keys.first
|
|
491
|
+
reference = attributes.fetch(:references)
|
|
492
|
+
if forward_join
|
|
493
|
+
unique = attributes[:unique] == true
|
|
494
|
+
{from: reference.fetch(:field).to_s, to: foreign_key, relation: unique ? "one-to-one" : "one-to-many", unique: unique}
|
|
495
|
+
else
|
|
496
|
+
{from: foreign_key, to: reference.fetch(:field).to_s, relation: "one-to-one", unique: true}
|
|
497
|
+
end
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
def schema_for(model)
|
|
501
|
+
Schema.auth_tables(options).fetch(model.to_s)
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
def reference_model_matches?(attributes, model)
|
|
505
|
+
reference = attributes[:references]
|
|
506
|
+
return false unless reference
|
|
507
|
+
|
|
508
|
+
reference_model = reference[:model] || reference["model"]
|
|
509
|
+
reference_model.to_s == model.to_s || reference_model.to_s == schema_for(model).fetch(:model_name)
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
def storage_key(field)
|
|
513
|
+
Schema.storage_key(field)
|
|
514
|
+
end
|
|
515
|
+
|
|
267
516
|
def stringify_keys(data)
|
|
268
517
|
data.each_with_object({}) do |(key, value), result|
|
|
269
518
|
result[Schema.storage_key(key)] = value
|
|
@@ -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
|