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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +32 -0
- data/README.md +5 -3
- data/lib/better_auth/adapters/internal_adapter.rb +173 -20
- data/lib/better_auth/adapters/memory.rb +61 -12
- data/lib/better_auth/adapters/mongodb.rb +5 -365
- data/lib/better_auth/adapters/sql.rb +44 -3
- data/lib/better_auth/api.rb +7 -2
- data/lib/better_auth/async.rb +70 -0
- data/lib/better_auth/context.rb +2 -1
- data/lib/better_auth/database_hooks.rb +3 -3
- data/lib/better_auth/deprecate.rb +28 -0
- data/lib/better_auth/endpoint.rb +5 -2
- data/lib/better_auth/host.rb +166 -0
- data/lib/better_auth/instrumentation.rb +74 -0
- data/lib/better_auth/logger.rb +31 -0
- data/lib/better_auth/middleware/origin_check.rb +2 -2
- data/lib/better_auth/oauth2.rb +94 -0
- data/lib/better_auth/plugin.rb +14 -1
- data/lib/better_auth/plugins/email_otp.rb +16 -5
- data/lib/better_auth/plugins/generic_oauth.rb +14 -28
- data/lib/better_auth/plugins/oauth_protocol.rb +553 -64
- data/lib/better_auth/plugins/organization/schema.rb +6 -0
- data/lib/better_auth/plugins/organization.rb +56 -20
- data/lib/better_auth/plugins/two_factor.rb +53 -18
- data/lib/better_auth/rate_limiter.rb +37 -2
- data/lib/better_auth/request_state.rb +44 -0
- data/lib/better_auth/router.rb +14 -1
- data/lib/better_auth/routes/account.rb +16 -4
- data/lib/better_auth/routes/email_verification.rb +5 -2
- data/lib/better_auth/routes/password.rb +21 -1
- data/lib/better_auth/routes/session.rb +27 -4
- data/lib/better_auth/routes/sign_in.rb +3 -1
- data/lib/better_auth/routes/sign_up.rb +60 -1
- data/lib/better_auth/routes/social.rb +231 -22
- data/lib/better_auth/routes/user.rb +23 -5
- data/lib/better_auth/schema/sql.rb +11 -0
- data/lib/better_auth/schema.rb +16 -0
- data/lib/better_auth/session.rb +12 -1
- data/lib/better_auth/social_providers/apple.rb +44 -8
- data/lib/better_auth/social_providers/atlassian.rb +32 -0
- data/lib/better_auth/social_providers/base.rb +262 -4
- data/lib/better_auth/social_providers/cognito.rb +32 -0
- data/lib/better_auth/social_providers/discord.rb +27 -5
- data/lib/better_auth/social_providers/dropbox.rb +33 -0
- data/lib/better_auth/social_providers/facebook.rb +35 -0
- data/lib/better_auth/social_providers/figma.rb +31 -0
- data/lib/better_auth/social_providers/github.rb +21 -6
- data/lib/better_auth/social_providers/gitlab.rb +16 -3
- data/lib/better_auth/social_providers/google.rb +38 -13
- data/lib/better_auth/social_providers/huggingface.rb +31 -0
- data/lib/better_auth/social_providers/kakao.rb +32 -0
- data/lib/better_auth/social_providers/kick.rb +32 -0
- data/lib/better_auth/social_providers/line.rb +33 -0
- data/lib/better_auth/social_providers/linear.rb +44 -0
- data/lib/better_auth/social_providers/linkedin.rb +30 -0
- data/lib/better_auth/social_providers/microsoft_entra_id.rb +79 -7
- data/lib/better_auth/social_providers/naver.rb +31 -0
- data/lib/better_auth/social_providers/notion.rb +33 -0
- data/lib/better_auth/social_providers/paybin.rb +31 -0
- data/lib/better_auth/social_providers/paypal.rb +36 -0
- data/lib/better_auth/social_providers/polar.rb +31 -0
- data/lib/better_auth/social_providers/railway.rb +49 -0
- data/lib/better_auth/social_providers/reddit.rb +32 -0
- data/lib/better_auth/social_providers/roblox.rb +31 -0
- data/lib/better_auth/social_providers/salesforce.rb +38 -0
- data/lib/better_auth/social_providers/slack.rb +30 -0
- data/lib/better_auth/social_providers/spotify.rb +31 -0
- data/lib/better_auth/social_providers/tiktok.rb +35 -0
- data/lib/better_auth/social_providers/twitch.rb +39 -0
- data/lib/better_auth/social_providers/twitter.rb +32 -0
- data/lib/better_auth/social_providers/vercel.rb +47 -0
- data/lib/better_auth/social_providers/vk.rb +34 -0
- data/lib/better_auth/social_providers/wechat.rb +104 -0
- data/lib/better_auth/social_providers/zoom.rb +31 -0
- data/lib/better_auth/social_providers.rb +29 -0
- data/lib/better_auth/url_helpers.rb +195 -0
- data/lib/better_auth/version.rb +1 -1
- data/lib/better_auth.rb +8 -1
- metadata +38 -15
|
@@ -1,369 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
require "
|
|
3
|
+
begin
|
|
4
|
+
require "better_auth/mongo_adapter"
|
|
5
|
+
rescue LoadError => error
|
|
6
|
+
raise if error.path && error.path != "better_auth/mongo_adapter"
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
module Adapters
|
|
8
|
-
class MongoDB < Base
|
|
9
|
-
attr_reader :database, :client, :use_plural
|
|
10
|
-
|
|
11
|
-
def initialize(options = nil, database:, client: nil, transaction: nil, use_plural: false)
|
|
12
|
-
require "mongo" unless database
|
|
13
|
-
|
|
14
|
-
super(options || Configuration.new(secret: Configuration::DEFAULT_SECRET, database: :memory))
|
|
15
|
-
@database = database
|
|
16
|
-
@client = client
|
|
17
|
-
@transaction_enabled = transaction.nil? ? !client.nil? : !!transaction
|
|
18
|
-
@use_plural = !!use_plural
|
|
19
|
-
@session = nil
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def create(model:, data:, force_allow_id: false)
|
|
23
|
-
model = model.to_s
|
|
24
|
-
record = transform_input(model, data, "create", force_allow_id)
|
|
25
|
-
document = to_document(model, record)
|
|
26
|
-
collection_for(model).insert_one(document, session_options)
|
|
27
|
-
from_document(model, document)
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def find_one(model:, where: [], select: nil, join: nil)
|
|
31
|
-
find_many(model: model, where: where, select: select, join: join, limit: 1).first
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def find_many(model:, where: [], sort_by: nil, limit: nil, offset: nil, select: nil, join: nil)
|
|
35
|
-
model = model.to_s
|
|
36
|
-
records = documents_for(model)
|
|
37
|
-
.select { |document| matches_where?(model, document, where || []) }
|
|
38
|
-
.map { |document| from_document(model, document) }
|
|
39
|
-
records = records.map { |record| apply_join(model, record, join) } if join
|
|
40
|
-
records = sort_records(records, sort_by) if sort_by
|
|
41
|
-
records = records.drop(offset.to_i) if offset
|
|
42
|
-
records = records.first(limit.to_i) if limit
|
|
43
|
-
records = records.map { |record| select_fields(record, select, join) } if select && !select.empty?
|
|
44
|
-
records
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def update(model:, where:, update:)
|
|
48
|
-
model = model.to_s
|
|
49
|
-
records = update_matching(model, where || [], update, first_only: true)
|
|
50
|
-
records.first
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def update_many(model:, where:, update:)
|
|
54
|
-
update_matching(model.to_s, where || [], update, first_only: false).length
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def delete(model:, where:)
|
|
58
|
-
delete_many(model: model, where: where, first_only: true)
|
|
59
|
-
nil
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def delete_many(model:, where:, first_only: false)
|
|
63
|
-
model = model.to_s
|
|
64
|
-
documents = documents_for(model)
|
|
65
|
-
matches = documents.select { |document| matches_where?(model, document, where || []) }
|
|
66
|
-
matches = matches.first(1) if first_only
|
|
67
|
-
ids = matches.map { |document| document["_id"] }
|
|
68
|
-
remaining = documents.reject { |document| ids.include?(document["_id"]) }
|
|
69
|
-
replace_documents(model, remaining)
|
|
70
|
-
ids.length
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
def count(model:, where: nil)
|
|
74
|
-
find_many(model: model, where: where || []).length
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def transaction
|
|
78
|
-
return yield self unless client && @transaction_enabled && client.respond_to?(:start_session)
|
|
79
|
-
|
|
80
|
-
session = client.start_session
|
|
81
|
-
begin
|
|
82
|
-
session.start_transaction
|
|
83
|
-
@session = session
|
|
84
|
-
result = yield self
|
|
85
|
-
session.commit_transaction
|
|
86
|
-
result
|
|
87
|
-
rescue
|
|
88
|
-
session.abort_transaction
|
|
89
|
-
raise
|
|
90
|
-
ensure
|
|
91
|
-
@session = nil
|
|
92
|
-
session.end_session
|
|
93
|
-
end
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
private
|
|
97
|
-
|
|
98
|
-
def transform_input(model, data, action, force_allow_id)
|
|
99
|
-
fields = schema_for(model).fetch(:fields)
|
|
100
|
-
input = stringify_keys(data)
|
|
101
|
-
output = {}
|
|
102
|
-
|
|
103
|
-
fields.each do |field, attributes|
|
|
104
|
-
next if field == "id" && input.key?(field) && !force_allow_id
|
|
105
|
-
|
|
106
|
-
value_provided = input.key?(field)
|
|
107
|
-
value = input[field]
|
|
108
|
-
if value_provided && attributes[:input] == false && value && !force_allow_id
|
|
109
|
-
raise APIError.new("BAD_REQUEST", message: "#{field} is not allowed to be set")
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
if !value_provided && action == "create" && attributes.key?(:default_value)
|
|
113
|
-
value = resolve_default(attributes[:default_value])
|
|
114
|
-
value_provided = true
|
|
115
|
-
elsif !value_provided && action == "update" && attributes[:on_update]
|
|
116
|
-
value = resolve_default(attributes[:on_update])
|
|
117
|
-
value_provided = true
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
if !value_provided && action == "create" && attributes[:required]
|
|
121
|
-
raise APIError.new("BAD_REQUEST", message: "#{field} is required") unless field == "id"
|
|
122
|
-
end
|
|
123
|
-
output[field] = coerce_value(value, attributes) if value_provided
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
output["id"] = generated_id if action == "create" && !output.key?("id")
|
|
127
|
-
output
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
def update_matching(model, where, update, first_only:)
|
|
131
|
-
data = transform_input(model, update, "update", true)
|
|
132
|
-
documents = documents_for(model)
|
|
133
|
-
matches = documents.select { |document| matches_where?(model, document, where) }
|
|
134
|
-
matches = matches.first(1) if first_only
|
|
135
|
-
updates = to_document(model, data)
|
|
136
|
-
ids = matches.map { |document| document["_id"] }
|
|
137
|
-
updated = documents.map do |document|
|
|
138
|
-
ids.include?(document["_id"]) ? document.merge(updates) : document
|
|
139
|
-
end
|
|
140
|
-
replace_documents(model, updated)
|
|
141
|
-
updated.select { |document| ids.include?(document["_id"]) }.map { |document| from_document(model, document) }
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
def documents_for(model)
|
|
145
|
-
collection = collection_for(model)
|
|
146
|
-
if collection.respond_to?(:all_documents)
|
|
147
|
-
collection.all_documents
|
|
148
|
-
else
|
|
149
|
-
collection.find({}, session_options).to_a.map { |document| stringify_document(document) }
|
|
150
|
-
end
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
def replace_documents(model, documents)
|
|
154
|
-
collection = collection_for(model)
|
|
155
|
-
if collection.respond_to?(:replace_documents)
|
|
156
|
-
collection.replace_documents(documents)
|
|
157
|
-
else
|
|
158
|
-
collection.delete_many({}, session_options)
|
|
159
|
-
documents.each { |document| collection.insert_one(document, session_options) }
|
|
160
|
-
end
|
|
161
|
-
end
|
|
162
|
-
|
|
163
|
-
def collection_for(model)
|
|
164
|
-
database.collection(collection_name(model))
|
|
165
|
-
end
|
|
166
|
-
|
|
167
|
-
def collection_name(model)
|
|
168
|
-
return schema_for(model).fetch(:model_name) if use_plural
|
|
169
|
-
|
|
170
|
-
model.to_s
|
|
171
|
-
end
|
|
172
|
-
|
|
173
|
-
def to_document(model, record)
|
|
174
|
-
schema_for(model).fetch(:fields).each_with_object({}) do |(field, attributes), document|
|
|
175
|
-
next unless record.key?(field)
|
|
176
|
-
|
|
177
|
-
key = (field == "id") ? "_id" : storage_field(model, field)
|
|
178
|
-
document[key] = store_value(field, record[field], attributes)
|
|
179
|
-
end
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
def from_document(model, document)
|
|
183
|
-
fields = schema_for(model).fetch(:fields)
|
|
184
|
-
fields.each_with_object({}) do |(field, attributes), record|
|
|
185
|
-
key = (field == "id") ? "_id" : storage_field(model, field)
|
|
186
|
-
record[field] = output_value(field, fetch_document(document, key), attributes) if document_key?(document, key)
|
|
187
|
-
end
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
def stringify_document(document)
|
|
191
|
-
document.each_with_object({}) { |(key, value), result| result[key.to_s] = value }
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
def matches_where?(model, document, where)
|
|
195
|
-
clauses = Array(where)
|
|
196
|
-
return true if clauses.empty?
|
|
197
|
-
|
|
198
|
-
result = evaluate_clause(model, document, clauses.first)
|
|
199
|
-
clauses.drop(1).each do |clause|
|
|
200
|
-
clause_result = evaluate_clause(model, document, clause)
|
|
201
|
-
if fetch_key(clause, :connector).to_s.upcase == "OR"
|
|
202
|
-
result ||= clause_result
|
|
203
|
-
else
|
|
204
|
-
result &&= clause_result
|
|
205
|
-
end
|
|
206
|
-
end
|
|
207
|
-
result
|
|
208
|
-
end
|
|
209
|
-
|
|
210
|
-
def evaluate_clause(model, document, clause)
|
|
211
|
-
field = Schema.storage_key(fetch_key(clause, :field))
|
|
212
|
-
attributes = schema_for(model).fetch(:fields).fetch(field)
|
|
213
|
-
key = (field == "id") ? "_id" : storage_field(model, field)
|
|
214
|
-
expected = store_value(field, fetch_key(clause, :value), attributes)
|
|
215
|
-
current = fetch_document(document, key)
|
|
216
|
-
operator = (fetch_key(clause, :operator) || "eq").to_s
|
|
217
|
-
|
|
218
|
-
case operator
|
|
219
|
-
when "in"
|
|
220
|
-
Array(expected).any? { |value| same_value?(current, value) }
|
|
221
|
-
when "not_in"
|
|
222
|
-
Array(expected).none? { |value| same_value?(current, value) }
|
|
223
|
-
when "contains"
|
|
224
|
-
current.to_s.include?(expected.to_s)
|
|
225
|
-
when "starts_with"
|
|
226
|
-
current.to_s.start_with?(expected.to_s)
|
|
227
|
-
when "ends_with"
|
|
228
|
-
current.to_s.end_with?(expected.to_s)
|
|
229
|
-
when "ne"
|
|
230
|
-
!same_value?(current, expected)
|
|
231
|
-
when "gt"
|
|
232
|
-
!expected.nil? && current > expected
|
|
233
|
-
when "gte"
|
|
234
|
-
!expected.nil? && current >= expected
|
|
235
|
-
when "lt"
|
|
236
|
-
!expected.nil? && current < expected
|
|
237
|
-
when "lte"
|
|
238
|
-
!expected.nil? && current <= expected
|
|
239
|
-
else
|
|
240
|
-
same_value?(current, expected)
|
|
241
|
-
end
|
|
242
|
-
end
|
|
243
|
-
|
|
244
|
-
def same_value?(left, right)
|
|
245
|
-
left == right || left.to_s == right.to_s
|
|
246
|
-
end
|
|
247
|
-
|
|
248
|
-
def apply_join(model, record, join)
|
|
249
|
-
joined = record.dup
|
|
250
|
-
join.each_key do |join_model|
|
|
251
|
-
join_model = join_model.to_s
|
|
252
|
-
joined[join_model] = case [model, join_model]
|
|
253
|
-
when ["session", "user"], ["account", "user"]
|
|
254
|
-
find_one(model: "user", where: [{field: "id", value: record["userId"]}])
|
|
255
|
-
when ["user", "account"]
|
|
256
|
-
find_many(model: "account", where: [{field: "userId", value: record["id"]}])
|
|
257
|
-
end
|
|
258
|
-
end
|
|
259
|
-
joined
|
|
260
|
-
end
|
|
261
|
-
|
|
262
|
-
def sort_records(records, sort_by)
|
|
263
|
-
field = Schema.storage_key(fetch_key(sort_by, :field))
|
|
264
|
-
direction = fetch_key(sort_by, :direction).to_s
|
|
265
|
-
records.sort_by { |record| record[field].nil? ? "" : record[field] }.then do |sorted|
|
|
266
|
-
(direction == "desc") ? sorted.reverse : sorted
|
|
267
|
-
end
|
|
268
|
-
end
|
|
269
|
-
|
|
270
|
-
def select_fields(record, select, join)
|
|
271
|
-
fields = Array(select).map { |field| Schema.storage_key(field) }
|
|
272
|
-
selected = record.slice(*fields)
|
|
273
|
-
join&.each_key { |join_model| selected[join_model.to_s] = record[join_model.to_s] if record.key?(join_model.to_s) }
|
|
274
|
-
selected
|
|
275
|
-
end
|
|
276
|
-
|
|
277
|
-
def store_value(field, value, attributes)
|
|
278
|
-
return nil if value.nil?
|
|
279
|
-
return Array(value).map { |entry| store_value(field, entry, attributes) } if value.is_a?(Array)
|
|
280
|
-
|
|
281
|
-
if field == "id" || attributes.dig(:references, :field) == "id"
|
|
282
|
-
return value if custom_id_generator?
|
|
283
|
-
return bson_id(value)
|
|
284
|
-
end
|
|
285
|
-
|
|
286
|
-
coerce_value(value, attributes)
|
|
287
|
-
end
|
|
288
|
-
|
|
289
|
-
def output_value(field, value, attributes)
|
|
290
|
-
return nil if value.nil?
|
|
291
|
-
return value.to_s if field == "id" || attributes.dig(:references, :field) == "id"
|
|
292
|
-
|
|
293
|
-
coerce_value(value, attributes)
|
|
294
|
-
end
|
|
295
|
-
|
|
296
|
-
def bson_id(value)
|
|
297
|
-
return value unless defined?(BSON::ObjectId)
|
|
298
|
-
return value if value.is_a?(BSON::ObjectId)
|
|
299
|
-
|
|
300
|
-
BSON::ObjectId.from_string(value.to_s)
|
|
301
|
-
rescue
|
|
302
|
-
value
|
|
303
|
-
end
|
|
304
|
-
|
|
305
|
-
def generated_id
|
|
306
|
-
generator = options.advanced.dig(:database, :generate_id)
|
|
307
|
-
return generator.call.to_s if generator.respond_to?(:call)
|
|
308
|
-
return SecureRandom.uuid if generator == "uuid"
|
|
309
|
-
return BSON::ObjectId.new.to_s if defined?(BSON::ObjectId)
|
|
310
|
-
|
|
311
|
-
SecureRandom.hex(12)
|
|
312
|
-
end
|
|
313
|
-
|
|
314
|
-
def custom_id_generator?
|
|
315
|
-
options.advanced.dig(:database, :generate_id).respond_to?(:call)
|
|
316
|
-
end
|
|
317
|
-
|
|
318
|
-
def resolve_default(default)
|
|
319
|
-
default.respond_to?(:call) ? default.call : default
|
|
320
|
-
end
|
|
321
|
-
|
|
322
|
-
def coerce_value(value, attributes)
|
|
323
|
-
return value if value.nil?
|
|
324
|
-
return Time.parse(value) if attributes[:type] == "date" && value.is_a?(String)
|
|
325
|
-
|
|
326
|
-
value
|
|
327
|
-
end
|
|
328
|
-
|
|
329
|
-
def session_options
|
|
330
|
-
@session ? {session: @session} : {}
|
|
331
|
-
end
|
|
332
|
-
|
|
333
|
-
def document_key?(document, key)
|
|
334
|
-
document.key?(key) || document.key?(key.to_sym)
|
|
335
|
-
end
|
|
336
|
-
|
|
337
|
-
def fetch_document(document, key)
|
|
338
|
-
return document[key] if document.key?(key)
|
|
339
|
-
|
|
340
|
-
document[key.to_sym]
|
|
341
|
-
end
|
|
342
|
-
|
|
343
|
-
def stringify_keys(data)
|
|
344
|
-
data.each_with_object({}) do |(key, value), result|
|
|
345
|
-
result[Schema.storage_key(key)] = value
|
|
346
|
-
end
|
|
347
|
-
end
|
|
348
|
-
|
|
349
|
-
def fetch_key(hash, key)
|
|
350
|
-
hash[key] || hash[key.to_s] || hash[Schema.storage_key(key)] || hash[Schema.storage_key(key).to_sym]
|
|
351
|
-
end
|
|
352
|
-
|
|
353
|
-
def schema_for(model)
|
|
354
|
-
Schema.auth_tables(options).fetch(model.to_s)
|
|
355
|
-
end
|
|
356
|
-
|
|
357
|
-
def storage_field(model, field)
|
|
358
|
-
schema_for(model).fetch(:fields).fetch(field.to_s).fetch(:field_name, physical_name(field))
|
|
359
|
-
end
|
|
360
|
-
|
|
361
|
-
def physical_name(value)
|
|
362
|
-
value.to_s
|
|
363
|
-
.gsub(/([a-z\d])([A-Z])/, "\\1_\\2")
|
|
364
|
-
.tr("-", "_")
|
|
365
|
-
.downcase
|
|
366
|
-
end
|
|
367
|
-
end
|
|
368
|
-
end
|
|
8
|
+
raise LoadError, "BetterAuth::Adapters::MongoDB requires the better_auth-mongo-adapter gem. Add `gem \"better_auth-mongo-adapter\"` and `require \"better_auth/mongo_adapter\"`."
|
|
369
9
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "securerandom"
|
|
4
|
+
require "json"
|
|
4
5
|
require "time"
|
|
5
6
|
|
|
6
7
|
module BetterAuth
|
|
@@ -203,10 +204,11 @@ module BetterAuth
|
|
|
203
204
|
column = "#{quote(table_for(model))}.#{quote(storage_field(model, field))}"
|
|
204
205
|
operator = (fetch_key(clause, :operator) || "eq").to_s
|
|
205
206
|
value = fetch_key(clause, :value)
|
|
207
|
+
attributes = schema_for(model).fetch(:fields).fetch(field)
|
|
206
208
|
|
|
207
209
|
expression = case operator
|
|
208
210
|
when "in", "not_in"
|
|
209
|
-
values = Array(value)
|
|
211
|
+
values = Array(value).map { |entry| coerce_where_value(entry, attributes) }
|
|
210
212
|
placeholders = values.map do |entry|
|
|
211
213
|
params << entry
|
|
212
214
|
placeholder(params.length)
|
|
@@ -222,7 +224,7 @@ module BetterAuth
|
|
|
222
224
|
params << pattern
|
|
223
225
|
"#{column} LIKE #{placeholder(params.length)}"
|
|
224
226
|
else
|
|
225
|
-
params << value
|
|
227
|
+
params << coerce_where_value(value, attributes)
|
|
226
228
|
"#{column} #{sql_operator(operator)} #{placeholder(params.length)}"
|
|
227
229
|
end
|
|
228
230
|
|
|
@@ -379,18 +381,46 @@ module BetterAuth
|
|
|
379
381
|
return value ? 1 : 0 if dialect == :sqlite && attributes[:type] == "boolean"
|
|
380
382
|
return value.iso8601(6) if dialect == :sqlite && attributes[:type] == "date" && value.respond_to?(:iso8601)
|
|
381
383
|
return Time.parse(value) if attributes[:type] == "date" && value.is_a?(String)
|
|
384
|
+
return JSON.generate(value) if json_like?(attributes) && !value.is_a?(String)
|
|
382
385
|
|
|
383
386
|
value
|
|
384
387
|
end
|
|
385
388
|
|
|
389
|
+
def coerce_where_value(value, attributes)
|
|
390
|
+
return value if value.nil?
|
|
391
|
+
|
|
392
|
+
case attributes[:type]
|
|
393
|
+
when "boolean"
|
|
394
|
+
return coerce_value(false, attributes) if value == false || value == 0 || value.to_s.downcase == "false" || value.to_s == "0"
|
|
395
|
+
return coerce_value(true, attributes) if value == true || value == 1 || value.to_s.downcase == "true" || value.to_s == "1"
|
|
396
|
+
when "number"
|
|
397
|
+
return coerce_number(value)
|
|
398
|
+
when "date"
|
|
399
|
+
return Time.parse(value) if value.is_a?(String)
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
coerce_value(value, attributes)
|
|
403
|
+
end
|
|
404
|
+
|
|
386
405
|
def coerce_output_value(value, attributes)
|
|
387
406
|
return value if value.nil?
|
|
388
407
|
return coerce_boolean(value) if attributes[:type] == "boolean"
|
|
389
408
|
return Time.parse(value) if attributes[:type] == "date" && value.is_a?(String)
|
|
409
|
+
return parse_json_value(value) if json_like?(attributes) && value.is_a?(String)
|
|
390
410
|
|
|
391
411
|
value
|
|
392
412
|
end
|
|
393
413
|
|
|
414
|
+
def json_like?(attributes)
|
|
415
|
+
%w[json string[] number[]].include?(attributes[:type])
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
def parse_json_value(value)
|
|
419
|
+
JSON.parse(value)
|
|
420
|
+
rescue JSON::ParserError
|
|
421
|
+
value
|
|
422
|
+
end
|
|
423
|
+
|
|
394
424
|
def coerce_boolean(value)
|
|
395
425
|
return value if value == true || value == false
|
|
396
426
|
return false if value == 0 || value.to_s == "0" || value.to_s.downcase == "f" || value.to_s.downcase == "false"
|
|
@@ -399,6 +429,14 @@ module BetterAuth
|
|
|
399
429
|
value
|
|
400
430
|
end
|
|
401
431
|
|
|
432
|
+
def coerce_number(value)
|
|
433
|
+
return value unless value.is_a?(String)
|
|
434
|
+
return value.to_i if /\A-?\d+\z/.match?(value)
|
|
435
|
+
return value.to_f if /\A-?\d+\.\d+\z/.match?(value)
|
|
436
|
+
|
|
437
|
+
value
|
|
438
|
+
end
|
|
439
|
+
|
|
402
440
|
def stringify_keys(data)
|
|
403
441
|
data.each_with_object({}) do |(key, value), result|
|
|
404
442
|
result[storage_key(key)] = value
|
|
@@ -406,7 +444,10 @@ module BetterAuth
|
|
|
406
444
|
end
|
|
407
445
|
|
|
408
446
|
def fetch_key(hash, key)
|
|
409
|
-
|
|
447
|
+
[key, key.to_s, storage_key(key), storage_key(key).to_sym].each do |candidate|
|
|
448
|
+
return hash[candidate] if hash.key?(candidate)
|
|
449
|
+
end
|
|
450
|
+
nil
|
|
410
451
|
end
|
|
411
452
|
|
|
412
453
|
def storage_key(value)
|
data/lib/better_auth/api.rb
CHANGED
|
@@ -16,7 +16,7 @@ module BetterAuth
|
|
|
16
16
|
input = symbolize_keys(input || {})
|
|
17
17
|
endpoint_context = Endpoint::Context.new(
|
|
18
18
|
path: endpoint.path,
|
|
19
|
-
method: Array(endpoint.methods).first,
|
|
19
|
+
method: input[:method] || Array(endpoint.methods).first,
|
|
20
20
|
query: input[:query] || {},
|
|
21
21
|
body: input[:body] || {},
|
|
22
22
|
params: input[:params] || {},
|
|
@@ -211,7 +211,12 @@ module BetterAuth
|
|
|
211
211
|
return value unless value.is_a?(Hash)
|
|
212
212
|
|
|
213
213
|
value.each_with_object({}) do |(key, object_value), result|
|
|
214
|
-
|
|
214
|
+
normalized_key = normalize_key(key)
|
|
215
|
+
result[normalized_key] = if normalized_key == :metadata
|
|
216
|
+
object_value
|
|
217
|
+
else
|
|
218
|
+
object_value.is_a?(Hash) ? symbolize_keys(object_value) : object_value
|
|
219
|
+
end
|
|
215
220
|
end
|
|
216
221
|
end
|
|
217
222
|
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterAuth
|
|
4
|
+
module Async
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def map_concurrent(items, concurrency:, &mapper)
|
|
8
|
+
list = items.to_a
|
|
9
|
+
return [] if list.empty?
|
|
10
|
+
|
|
11
|
+
width = normalized_concurrency(concurrency, list.length)
|
|
12
|
+
results = Array.new(list.length)
|
|
13
|
+
next_index = 0
|
|
14
|
+
first_error = nil
|
|
15
|
+
mutex = Mutex.new
|
|
16
|
+
status = Queue.new
|
|
17
|
+
|
|
18
|
+
workers = Array.new(width) do
|
|
19
|
+
Thread.new do
|
|
20
|
+
loop do
|
|
21
|
+
index = mutex.synchronize do
|
|
22
|
+
break if first_error || next_index >= list.length
|
|
23
|
+
|
|
24
|
+
current = next_index
|
|
25
|
+
next_index += 1
|
|
26
|
+
current
|
|
27
|
+
end
|
|
28
|
+
break unless index
|
|
29
|
+
|
|
30
|
+
begin
|
|
31
|
+
results[index] = mapper.call(list[index], index)
|
|
32
|
+
rescue => error
|
|
33
|
+
mutex.synchronize { first_error ||= error }
|
|
34
|
+
status << [:error, error]
|
|
35
|
+
break
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
ensure
|
|
39
|
+
status << [:done, nil]
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
done = 0
|
|
44
|
+
while done < workers.length
|
|
45
|
+
type, error = status.pop
|
|
46
|
+
if type == :error
|
|
47
|
+
workers.each { |worker| worker.kill if worker.alive? }
|
|
48
|
+
workers.each(&:join)
|
|
49
|
+
raise error
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
done += 1
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
raise first_error if first_error
|
|
56
|
+
|
|
57
|
+
results
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def normalized_concurrency(concurrency, item_count)
|
|
61
|
+
raw = begin
|
|
62
|
+
Float(concurrency).floor
|
|
63
|
+
rescue ArgumentError, TypeError
|
|
64
|
+
1
|
|
65
|
+
end
|
|
66
|
+
raw = 1 if raw < 1
|
|
67
|
+
[raw, item_count].min
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
data/lib/better_auth/context.rb
CHANGED
|
@@ -128,8 +128,9 @@ module BetterAuth
|
|
|
128
128
|
end
|
|
129
129
|
|
|
130
130
|
scheme = request.get_header("rack.url_scheme") || request.scheme
|
|
131
|
+
scheme = "https" unless valid_proxy_proto?(scheme.to_s)
|
|
131
132
|
host_header = request.get_header("HTTP_HOST")
|
|
132
|
-
return "#{scheme}://#{host_header}" if host_header &&
|
|
133
|
+
return "#{scheme}://#{host_header}" if host_header && valid_proxy_host?(host_header.to_s)
|
|
133
134
|
|
|
134
135
|
host = request.get_header("SERVER_NAME") || request.host
|
|
135
136
|
port = (request.get_header("SERVER_PORT") || request.port).to_i
|
|
@@ -35,9 +35,9 @@ module BetterAuth
|
|
|
35
35
|
|
|
36
36
|
def delete(where, model, custom: nil, context: nil)
|
|
37
37
|
entity = adapter.find_one(model: model, where: where)
|
|
38
|
-
return
|
|
38
|
+
return nil unless entity
|
|
39
39
|
|
|
40
|
-
return
|
|
40
|
+
return false if before_hooks(model, :delete).any? { |hook| hook.call(entity, context) == false }
|
|
41
41
|
|
|
42
42
|
deleted = custom ? custom.call(where) : adapter.delete(model: model, where: where)
|
|
43
43
|
after_hooks(model, :delete).each { |hook| hook.call(entity, context) }
|
|
@@ -47,7 +47,7 @@ module BetterAuth
|
|
|
47
47
|
def delete_many(where, model, custom: nil, context: nil)
|
|
48
48
|
entities = adapter.find_many(model: model, where: where)
|
|
49
49
|
entities.each do |entity|
|
|
50
|
-
return
|
|
50
|
+
return false if before_hooks(model, :delete).any? { |hook| hook.call(entity, context) == false }
|
|
51
51
|
end
|
|
52
52
|
deleted = custom ? custom.call(where) : adapter.delete_many(model: model, where: where)
|
|
53
53
|
entities.each { |entity| after_hooks(model, :delete).each { |hook| hook.call(entity, context) } }
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterAuth
|
|
4
|
+
module Deprecate
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def wrap(message, logger: nil, &block)
|
|
8
|
+
warned = false
|
|
9
|
+
proc do |*args, **kwargs|
|
|
10
|
+
unless warned
|
|
11
|
+
warn_once("[Deprecation] #{message}", logger)
|
|
12
|
+
warned = true
|
|
13
|
+
end
|
|
14
|
+
kwargs.empty? ? block.call(*args) : block.call(*args, **kwargs)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def warn_once(message, logger)
|
|
19
|
+
if logger.respond_to?(:call)
|
|
20
|
+
logger.call(message)
|
|
21
|
+
elsif logger.respond_to?(:warn)
|
|
22
|
+
logger.warn(message)
|
|
23
|
+
else
|
|
24
|
+
warn(message)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
data/lib/better_auth/endpoint.rb
CHANGED
|
@@ -7,16 +7,18 @@ module BetterAuth
|
|
|
7
7
|
attr_reader :path,
|
|
8
8
|
:body_schema,
|
|
9
9
|
:query_schema,
|
|
10
|
+
:params_schema,
|
|
10
11
|
:headers_schema,
|
|
11
12
|
:metadata,
|
|
12
13
|
:use,
|
|
13
14
|
:handler
|
|
14
15
|
|
|
15
|
-
def initialize(path: nil, method: nil, body_schema: nil, query_schema: nil, headers_schema: nil, metadata: {}, use: [], &handler)
|
|
16
|
+
def initialize(path: nil, method: nil, body_schema: nil, query_schema: nil, params_schema: nil, headers_schema: nil, metadata: {}, use: [], &handler)
|
|
16
17
|
@path = path
|
|
17
18
|
@methods = Array(method || "*").map { |value| value.to_s.upcase }
|
|
18
19
|
@body_schema = body_schema
|
|
19
20
|
@query_schema = query_schema
|
|
21
|
+
@params_schema = params_schema
|
|
20
22
|
@headers_schema = headers_schema
|
|
21
23
|
@metadata = metadata || {}
|
|
22
24
|
@use = Array(use)
|
|
@@ -47,6 +49,7 @@ module BetterAuth
|
|
|
47
49
|
def apply_schemas!(context)
|
|
48
50
|
context.body = validate_schema(:body, body_schema, context.body)
|
|
49
51
|
context.query = validate_schema(:query, query_schema, context.query)
|
|
52
|
+
context.params = validate_schema(:params, params_schema, context.params)
|
|
50
53
|
context.headers = context.send(:normalize_headers, validate_schema(:headers, headers_schema, context.headers))
|
|
51
54
|
end
|
|
52
55
|
|
|
@@ -136,7 +139,7 @@ module BetterAuth
|
|
|
136
139
|
return @raw_response if raw_response?
|
|
137
140
|
|
|
138
141
|
body = if response.nil?
|
|
139
|
-
[
|
|
142
|
+
[JSON.generate(nil)]
|
|
140
143
|
elsif response.is_a?(String)
|
|
141
144
|
[response]
|
|
142
145
|
else
|