better_auth-mongodb 0.8.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 +7 -0
- data/CHANGELOG.md +25 -0
- data/README.md +108 -0
- data/lib/better_auth/mongo_adapter.rb +9 -0
- data/lib/better_auth/mongodb/version.rb +7 -0
- data/lib/better_auth/mongodb.rb +682 -0
- metadata +189 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 4ed1669dc271afab7b9e15d5abd0fdd7aeb5228ad4b602eeda5438f8f07d5270
|
|
4
|
+
data.tar.gz: a6efaca422c47a3e24ffc7c6b2bcf0d7633843b76b0db514da8588bee4233090
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: bb69f0da9105d78d720bdfca09f5c4e2fe99c9ee60a7f0ac392c4eab1f7613da8dc845ab23926499ff00107d455d8aa3a20fe2984fa6737fe603688cfc7bdde1
|
|
7
|
+
data.tar.gz: de14ab5fb5b7078d3803a5fed75c7415a195276b0dbde522845f5337e3194c0cac75534b49a66e794cf8ffec013d8d07926c3a0077594054848c56f2f79bf94c
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
- Rename the canonical Ruby gem to `better_auth-mongodb` while keeping
|
|
6
|
+
`better_auth-mongo-adapter` as a deprecated compatibility package.
|
|
7
|
+
|
|
8
|
+
## 0.7.0 - 2026-05-05
|
|
9
|
+
|
|
10
|
+
- Added explicit `ensure_indexes!` setup helper for Mongo indexes derived from Better Auth schema metadata.
|
|
11
|
+
- Updated MongoDB setup docs to use the lambda adapter form, clearer standalone/replica-set transaction guidance, and production index guidance.
|
|
12
|
+
- Consolidated Mongo fake test support and strengthened transaction rollback coverage for staged mutations.
|
|
13
|
+
- Apply `advanced[:database][:default_find_many_limit]` to uncapped `find_many` calls and one-to-many Mongo `$lookup` joins, defaulting to 100 when omitted.
|
|
14
|
+
- Match upstream Mongo where-clause semantics for mixed connectors by bucketing multi-clause filters into `$and` and `$or` arrays instead of left-fold nesting.
|
|
15
|
+
- Allow scalar values for `in` and `not_in` filters as an intentional Ruby adapter-family adaptation.
|
|
16
|
+
|
|
17
|
+
## 0.1.1 - 2026-04-30
|
|
18
|
+
|
|
19
|
+
- Fixed inferred limited joins so explicit relation and limit configuration is preserved.
|
|
20
|
+
- Added MongoDB upstream parity coverage using a fake Mongo adapter harness.
|
|
21
|
+
|
|
22
|
+
## 0.1.0
|
|
23
|
+
|
|
24
|
+
- Extract MongoDB adapter support into the `better_auth-mongo-adapter` package.
|
|
25
|
+
- Align MongoDB adapter behavior with upstream Better Auth v1.6.9, including where-clause key variants, falsey value handling, ID normalization, and external adapter compatibility coverage.
|
data/README.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# better_auth-mongodb
|
|
2
|
+
|
|
3
|
+
MongoDB database adapter package for Better Auth Ruby.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add the gem and require the package before configuring auth:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem "better_auth-mongodb"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
require "mongo"
|
|
15
|
+
require "better_auth/mongodb"
|
|
16
|
+
|
|
17
|
+
mongo_client = Mongo::Client.new(ENV.fetch("BETTER_AUTH_MONGODB_URL"))
|
|
18
|
+
|
|
19
|
+
auth = BetterAuth.auth(
|
|
20
|
+
secret: ENV.fetch("BETTER_AUTH_SECRET"),
|
|
21
|
+
database: ->(options) {
|
|
22
|
+
BetterAuth::Adapters::MongoDB.new(
|
|
23
|
+
options,
|
|
24
|
+
database: mongo_client.database,
|
|
25
|
+
client: mongo_client,
|
|
26
|
+
transaction: false
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The lambda form lets Better Auth pass the final configuration into the adapter,
|
|
33
|
+
including plugins, custom schemas, and advanced database options.
|
|
34
|
+
|
|
35
|
+
## Notes
|
|
36
|
+
|
|
37
|
+
This package depends on the official `mongo` gem. Keeping MongoDB support outside `better_auth` avoids installing MongoDB client dependencies for applications that only use SQL, Rails, Hanami, or in-memory storage.
|
|
38
|
+
|
|
39
|
+
The adapter stores Better Auth models in singular MongoDB collections by default, maps logical `id` values to Mongo `_id`, converts ObjectId-compatible ids through the Mongo driver, and supports the shared Better Auth database adapter contract.
|
|
40
|
+
|
|
41
|
+
Transactions are deployment-dependent. MongoDB multi-document transactions may
|
|
42
|
+
be unavailable on standalone servers and usually require a replica set plus
|
|
43
|
+
compatible driver/session settings. The setup example uses `transaction: false`;
|
|
44
|
+
enable transactions only when the MongoDB deployment supports them.
|
|
45
|
+
|
|
46
|
+
When using a replica set, remove `transaction: false` or pass
|
|
47
|
+
`transaction: true`. When using standalone local MongoDB, keep
|
|
48
|
+
`transaction: false`.
|
|
49
|
+
|
|
50
|
+
## Indexes
|
|
51
|
+
|
|
52
|
+
MongoDB does not run SQL-style migrations. The adapter can create recommended
|
|
53
|
+
indexes from Better Auth schema metadata, but this is an explicit setup step:
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
adapter = BetterAuth::Adapters::MongoDB.new(
|
|
57
|
+
options,
|
|
58
|
+
database: mongo_client.database,
|
|
59
|
+
client: mongo_client,
|
|
60
|
+
transaction: false
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
adapter.ensure_indexes!
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`ensure_indexes!` creates indexes for schema fields marked `unique: true` or
|
|
67
|
+
`index: true`, including plugin schemas and custom model or field names. It
|
|
68
|
+
skips Mongo `_id` because MongoDB creates that index automatically. The method
|
|
69
|
+
returns a summary of requested indexes so deployment scripts can log what was
|
|
70
|
+
applied.
|
|
71
|
+
|
|
72
|
+
## Limits
|
|
73
|
+
|
|
74
|
+
By default, `find_many` calls without an explicit `limit:` are capped at 100 records. Configure the default with Better Auth's advanced database option:
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
auth = BetterAuth.auth(
|
|
78
|
+
secret: ENV.fetch("BETTER_AUTH_SECRET"),
|
|
79
|
+
advanced: {
|
|
80
|
+
database: {
|
|
81
|
+
default_find_many_limit: 250
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
database: ->(options) {
|
|
85
|
+
BetterAuth::Adapters::MongoDB.new(
|
|
86
|
+
options,
|
|
87
|
+
database: mongo_client.database,
|
|
88
|
+
client: mongo_client,
|
|
89
|
+
transaction: false
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The same default applies to one-to-many join lookups when the join config does not set `limit:`. Passing an explicit `limit:` to `find_many` or to the join config overrides the default.
|
|
96
|
+
|
|
97
|
+
One-to-one joins ignore one-to-many limits. They are returned as a single object or `nil`.
|
|
98
|
+
|
|
99
|
+
Ruby's adapters accept scalar values for `in` and `not_in` filters and coerce
|
|
100
|
+
them to a one-element list. This is an intentional Ruby adapter-family behavior;
|
|
101
|
+
upstream's TypeScript adapter factory is stricter before the Mongo adapter sees
|
|
102
|
+
the query.
|
|
103
|
+
|
|
104
|
+
## Compatibility
|
|
105
|
+
|
|
106
|
+
The older `better_auth-mongo-adapter` gem and `require "better_auth/mongo_adapter"`
|
|
107
|
+
entrypoint are deprecated compatibility shims. New applications should use
|
|
108
|
+
`better_auth-mongodb` and `require "better_auth/mongodb"`.
|
|
@@ -0,0 +1,682 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "better_auth"
|
|
4
|
+
require "mongo"
|
|
5
|
+
require "securerandom"
|
|
6
|
+
require "time"
|
|
7
|
+
require_relative "mongodb/version"
|
|
8
|
+
|
|
9
|
+
module BetterAuth
|
|
10
|
+
module Adapters
|
|
11
|
+
class MongoDB < Base
|
|
12
|
+
class MongoAdapterError < Error
|
|
13
|
+
attr_reader :code
|
|
14
|
+
|
|
15
|
+
def initialize(code, message)
|
|
16
|
+
@code = code
|
|
17
|
+
super(message)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
attr_reader :database, :client, :use_plural
|
|
22
|
+
|
|
23
|
+
def initialize(options = nil, database:, client: nil, transaction: nil, use_plural: false, session: nil)
|
|
24
|
+
require "mongo" unless database
|
|
25
|
+
|
|
26
|
+
super(options || Configuration.new(secret: Configuration::DEFAULT_SECRET, database: :memory))
|
|
27
|
+
@database = database
|
|
28
|
+
@client = client
|
|
29
|
+
@transaction_enabled = transaction.nil? ? !client.nil? : !!transaction
|
|
30
|
+
@use_plural = !!use_plural
|
|
31
|
+
@session = session
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def create(model:, data:, force_allow_id: false)
|
|
35
|
+
model = model.to_s
|
|
36
|
+
record = transform_input(model, data, "create", force_allow_id)
|
|
37
|
+
document = to_document(model, record)
|
|
38
|
+
collection_for(model).insert_one(document, session_options)
|
|
39
|
+
from_document(model, document)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def find_one(model:, where: [], select: nil, join: nil)
|
|
43
|
+
find_many(model: model, where: where, select: select, join: join, limit: 1).first
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def find_many(model:, where: [], sort_by: nil, limit: nil, offset: nil, select: nil, join: nil)
|
|
47
|
+
model = model.to_s
|
|
48
|
+
pipeline = [{"$match" => mongo_filter(model, where || [])}]
|
|
49
|
+
pipeline.concat(join_stages(model, join)) if join
|
|
50
|
+
pipeline << {"$project" => projection_for(model, select, join)} if select && !select.empty?
|
|
51
|
+
pipeline << {"$sort" => {sort_field(model, sort_by) => sort_direction(sort_by)}} if sort_by
|
|
52
|
+
pipeline << {"$skip" => offset.to_i} if offset
|
|
53
|
+
effective_limit = limit.nil? ? default_find_many_limit : limit.to_i
|
|
54
|
+
pipeline << {"$limit" => effective_limit} if effective_limit.positive?
|
|
55
|
+
|
|
56
|
+
collection_for(model)
|
|
57
|
+
.aggregate(pipeline, session_options)
|
|
58
|
+
.to_a
|
|
59
|
+
.map { |document| from_document(model, stringify_document(document), join: join) }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def update(model:, where:, update:)
|
|
63
|
+
model = model.to_s
|
|
64
|
+
data = transform_input(model, update, "update", true)
|
|
65
|
+
document = to_document(model, data)
|
|
66
|
+
document.delete("_id")
|
|
67
|
+
result = collection_for(model).find_one_and_update(
|
|
68
|
+
mongo_filter(model, where || []),
|
|
69
|
+
{"$set" => document},
|
|
70
|
+
session_options.merge(return_document: :after)
|
|
71
|
+
)
|
|
72
|
+
result = unwrap_update_result(result)
|
|
73
|
+
result ? from_document(model, stringify_document(result)) : nil
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def update_many(model:, where:, update:)
|
|
77
|
+
model = model.to_s
|
|
78
|
+
data = transform_input(model, update, "update", true)
|
|
79
|
+
document = to_document(model, data)
|
|
80
|
+
document.delete("_id")
|
|
81
|
+
result = collection_for(model).update_many(
|
|
82
|
+
mongo_filter(model, where || []),
|
|
83
|
+
{"$set" => document},
|
|
84
|
+
session_options
|
|
85
|
+
)
|
|
86
|
+
result.respond_to?(:modified_count) ? result.modified_count : result.to_i
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def delete(model:, where:)
|
|
90
|
+
collection_for(model.to_s).delete_one(mongo_filter(model.to_s, where || []), session_options)
|
|
91
|
+
nil
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def delete_many(model:, where:)
|
|
95
|
+
result = collection_for(model.to_s).delete_many(mongo_filter(model.to_s, where || []), session_options)
|
|
96
|
+
result.respond_to?(:deleted_count) ? result.deleted_count : result.to_i
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def count(model:, where: nil)
|
|
100
|
+
pipeline = [
|
|
101
|
+
{"$match" => mongo_filter(model.to_s, where || [])},
|
|
102
|
+
{"$count" => "total"}
|
|
103
|
+
]
|
|
104
|
+
row = collection_for(model.to_s).aggregate(pipeline, session_options).to_a.first
|
|
105
|
+
return 0 unless row
|
|
106
|
+
|
|
107
|
+
(row["total"] || row[:total] || 0).to_i
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def ensure_indexes!
|
|
111
|
+
Schema.auth_tables(options).flat_map do |model, table|
|
|
112
|
+
table.fetch(:fields).filter_map do |field, attributes|
|
|
113
|
+
next if field == "id"
|
|
114
|
+
next unless attributes[:unique] || attributes[:index]
|
|
115
|
+
|
|
116
|
+
collection = collection_for(model)
|
|
117
|
+
key = storage_field(model, field)
|
|
118
|
+
index_options = attributes[:unique] ? {unique: true} : {}
|
|
119
|
+
collection.indexes.create_one({key => 1}, index_options)
|
|
120
|
+
{
|
|
121
|
+
collection: collection_name(model),
|
|
122
|
+
field: field,
|
|
123
|
+
keys: {key => 1},
|
|
124
|
+
unique: attributes[:unique] == true
|
|
125
|
+
}
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def transaction
|
|
131
|
+
return yield self unless client && @transaction_enabled && client.respond_to?(:start_session)
|
|
132
|
+
|
|
133
|
+
session = client.start_session
|
|
134
|
+
begin
|
|
135
|
+
session.start_transaction
|
|
136
|
+
adapter = self.class.new(options, database: database, client: client, transaction: @transaction_enabled, use_plural: use_plural, session: session)
|
|
137
|
+
result = yield adapter
|
|
138
|
+
session.commit_transaction
|
|
139
|
+
result
|
|
140
|
+
rescue
|
|
141
|
+
session.abort_transaction
|
|
142
|
+
raise
|
|
143
|
+
ensure
|
|
144
|
+
session.end_session
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
private
|
|
149
|
+
|
|
150
|
+
def transform_input(model, data, action, force_allow_id)
|
|
151
|
+
fields = fields_for(model)
|
|
152
|
+
input = stringify_keys(data)
|
|
153
|
+
output = {}
|
|
154
|
+
|
|
155
|
+
fields.each do |field, attributes|
|
|
156
|
+
next if field == "id" && input.key?(field) && !force_allow_id
|
|
157
|
+
|
|
158
|
+
value_provided = input.key?(field)
|
|
159
|
+
value = input[field]
|
|
160
|
+
if value_provided && attributes[:input] == false && value && !force_allow_id
|
|
161
|
+
raise APIError.new("BAD_REQUEST", message: "#{field} is not allowed to be set")
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
if !value_provided && action == "create" && attributes.key?(:default_value)
|
|
165
|
+
value = resolve_default(attributes[:default_value])
|
|
166
|
+
value_provided = true
|
|
167
|
+
elsif !value_provided && action == "update" && attributes[:on_update]
|
|
168
|
+
value = resolve_default(attributes[:on_update])
|
|
169
|
+
value_provided = true
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
if !value_provided && action == "create" && attributes[:required]
|
|
173
|
+
raise APIError.new("BAD_REQUEST", message: "#{field} is required") unless field == "id"
|
|
174
|
+
end
|
|
175
|
+
output[field] = coerce_value(value, attributes) if value_provided
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
output["id"] = generated_id if action == "create" && !output.key?("id")
|
|
179
|
+
output
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def mongo_filter(model, where)
|
|
183
|
+
clauses = Array(where)
|
|
184
|
+
return {} if clauses.empty?
|
|
185
|
+
|
|
186
|
+
conditions = clauses.map do |clause|
|
|
187
|
+
connector = if fetch_key(clause, :connector).to_s.upcase == "OR"
|
|
188
|
+
"OR"
|
|
189
|
+
else
|
|
190
|
+
"AND"
|
|
191
|
+
end
|
|
192
|
+
{condition: condition_for(model, clause), connector: connector}
|
|
193
|
+
end
|
|
194
|
+
return conditions.first.fetch(:condition) if conditions.one?
|
|
195
|
+
|
|
196
|
+
result = {}
|
|
197
|
+
and_conditions = conditions.select { |entry| entry.fetch(:connector) == "AND" }.map { |entry| entry.fetch(:condition) }
|
|
198
|
+
or_conditions = conditions.select { |entry| entry.fetch(:connector) == "OR" }.map { |entry| entry.fetch(:condition) }
|
|
199
|
+
result["$and"] = and_conditions if and_conditions.any?
|
|
200
|
+
result["$or"] = or_conditions if or_conditions.any?
|
|
201
|
+
result
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def default_find_many_limit
|
|
205
|
+
value = options.advanced.dig(:database, :default_find_many_limit)
|
|
206
|
+
return 100 if value.nil?
|
|
207
|
+
|
|
208
|
+
Integer(value)
|
|
209
|
+
rescue ArgumentError, TypeError
|
|
210
|
+
100
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def array_operator_values(value)
|
|
214
|
+
value.is_a?(Array) ? value : [value]
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def condition_for(model, clause)
|
|
218
|
+
operator = (fetch_key(clause, :operator) || "eq").to_s.downcase
|
|
219
|
+
value = fetch_key(clause, :value)
|
|
220
|
+
|
|
221
|
+
field = resolve_field(model, fetch_key(clause, :field))
|
|
222
|
+
attributes = fields_for(model).fetch(field)
|
|
223
|
+
key = (field == "id") ? "_id" : storage_field(model, field)
|
|
224
|
+
mode = (fetch_key(clause, :mode) || "sensitive").to_s
|
|
225
|
+
id_field = id_field?(field, attributes)
|
|
226
|
+
insensitive = !id_field && mode == "insensitive" && insensitive_value?(value)
|
|
227
|
+
value = coerce_where_value(value, attributes)
|
|
228
|
+
|
|
229
|
+
case operator
|
|
230
|
+
when "eq"
|
|
231
|
+
(insensitive && value.is_a?(String)) ? regex_condition(key, value, :eq, insensitive: true) : {key => store_value(field, value, attributes, strict_id: true)}
|
|
232
|
+
when "in"
|
|
233
|
+
(insensitive && value.is_a?(Array)) ? insensitive_in_condition(key, value) : {key => {"$in" => array_operator_values(value).map { |entry| store_value(field, entry, attributes, strict_id: true) }}}
|
|
234
|
+
when "not_in"
|
|
235
|
+
(insensitive && value.is_a?(Array)) ? insensitive_not_in_condition(key, value) : {key => {"$nin" => array_operator_values(value).map { |entry| store_value(field, entry, attributes, strict_id: true) }}}
|
|
236
|
+
when "ne"
|
|
237
|
+
(insensitive && value.is_a?(String)) ? {key => {"$not" => regex_for(value, :eq, insensitive: true)}} : {key => {"$ne" => store_value(field, value, attributes, strict_id: true)}}
|
|
238
|
+
when "gt", "gte", "lt", "lte"
|
|
239
|
+
{key => {"$#{operator}" => store_value(field, value, attributes, strict_id: true)}}
|
|
240
|
+
when "contains", "starts_with", "ends_with"
|
|
241
|
+
regex_condition(key, value.to_s, operator.to_sym, insensitive: insensitive)
|
|
242
|
+
else
|
|
243
|
+
raise MongoAdapterError.new("UNSUPPORTED_OPERATOR", "Unsupported operator: #{operator}")
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def insensitive_value?(value)
|
|
248
|
+
value.is_a?(String) || (value.is_a?(Array) && value.all? { |entry| entry.is_a?(String) })
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def insensitive_in_condition(key, values)
|
|
252
|
+
return {"$expr" => {"$eq" => [1, 0]}} if values.empty?
|
|
253
|
+
|
|
254
|
+
{"$or" => values.map { |value| regex_condition(key, value, :eq, insensitive: true) }}
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def insensitive_not_in_condition(key, values)
|
|
258
|
+
return {} if values.empty?
|
|
259
|
+
|
|
260
|
+
{"$nor" => values.map { |value| regex_condition(key, value, :eq, insensitive: true) }}
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def regex_condition(key, value, operator, insensitive:)
|
|
264
|
+
{key => regex_for(value, operator, insensitive: insensitive)}
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def regex_for(value, operator, insensitive:)
|
|
268
|
+
escaped = Regexp.escape(value.to_s[0, 256])
|
|
269
|
+
pattern = case operator.to_s
|
|
270
|
+
when "eq" then "\\A#{escaped}\\z"
|
|
271
|
+
when "starts_with" then "\\A#{escaped}"
|
|
272
|
+
when "ends_with" then "#{escaped}\\z"
|
|
273
|
+
else escaped
|
|
274
|
+
end
|
|
275
|
+
Regexp.new(pattern, insensitive ? Regexp::IGNORECASE : nil)
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def join_stages(model, join)
|
|
279
|
+
normalized_join(model, join).flat_map do |join_model, config|
|
|
280
|
+
local_field = storage_field_for_join(model, config.fetch(:from))
|
|
281
|
+
foreign_field = storage_field_for_join(join_model, config.fetch(:to))
|
|
282
|
+
relation = config[:relation]
|
|
283
|
+
limit = config.key?(:limit) ? config[:limit] : nil
|
|
284
|
+
effective_limit = limit.nil? ? default_find_many_limit : limit.to_i
|
|
285
|
+
unique = relation == "one-to-one" || config[:unique]
|
|
286
|
+
should_limit = !unique && effective_limit.positive?
|
|
287
|
+
|
|
288
|
+
lookup = if should_limit
|
|
289
|
+
{
|
|
290
|
+
"$lookup" => {
|
|
291
|
+
"from" => collection_name(join_model),
|
|
292
|
+
"let" => {"localFieldValue" => "$#{local_field}"},
|
|
293
|
+
"pipeline" => [
|
|
294
|
+
{"$match" => {"$expr" => {"$eq" => ["$#{foreign_field}", "$$localFieldValue"]}}},
|
|
295
|
+
{"$limit" => effective_limit}
|
|
296
|
+
],
|
|
297
|
+
"as" => join_model
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
else
|
|
301
|
+
{
|
|
302
|
+
"$lookup" => {
|
|
303
|
+
"from" => collection_name(join_model),
|
|
304
|
+
"localField" => local_field,
|
|
305
|
+
"foreignField" => foreign_field,
|
|
306
|
+
"as" => join_model
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
unique ? [lookup, {"$unwind" => {"path" => "$#{join_model}", "preserveNullAndEmptyArrays" => true}}] : [lookup]
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def normalized_join(model, join)
|
|
316
|
+
join.each_with_object({}) do |(join_model, config), result|
|
|
317
|
+
join_model = join_model.to_s
|
|
318
|
+
result[join_model] = normalize_join_config(model, join_model, config)
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def normalize_join_config(model, join_model, config)
|
|
323
|
+
if config.is_a?(Hash) && (config.key?(:on) || config.key?("on"))
|
|
324
|
+
on = config[:on] || config["on"]
|
|
325
|
+
relation = config[:relation] || config["relation"]
|
|
326
|
+
limit = config[:limit] || config["limit"]
|
|
327
|
+
from = fetch_key(on, :from)
|
|
328
|
+
to = fetch_key(on, :to)
|
|
329
|
+
return {from: Schema.storage_key(from), to: Schema.storage_key(to), relation: relation, limit: limit, unique: unique_join_field?(join_model, to)}
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
inferred = inferred_join_config(model, join_model)
|
|
333
|
+
if config.is_a?(Hash)
|
|
334
|
+
limit = config[:limit] || config["limit"]
|
|
335
|
+
relation = config[:relation] || config["relation"]
|
|
336
|
+
inferred = inferred.merge(limit: limit) if limit
|
|
337
|
+
inferred = inferred.merge(relation: relation) if relation
|
|
338
|
+
end
|
|
339
|
+
inferred
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def inferred_join_config(model, join_model)
|
|
343
|
+
base_model = default_model_name(model)
|
|
344
|
+
target_model = default_model_name(join_model)
|
|
345
|
+
foreign_keys = fields_for(target_model).select do |_field, attributes|
|
|
346
|
+
reference_model_matches?(attributes, base_model)
|
|
347
|
+
end
|
|
348
|
+
forward_join = true
|
|
349
|
+
|
|
350
|
+
if foreign_keys.empty?
|
|
351
|
+
foreign_keys = fields_for(base_model).select do |_field, attributes|
|
|
352
|
+
reference_model_matches?(attributes, target_model)
|
|
353
|
+
end
|
|
354
|
+
forward_join = false
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
if foreign_keys.empty?
|
|
358
|
+
raise Error, "No foreign key found for model #{join_model} and base model #{model} while performing join operation."
|
|
359
|
+
end
|
|
360
|
+
if foreign_keys.length > 1
|
|
361
|
+
raise Error, "Multiple foreign keys found for model #{join_model} and base model #{model} while performing join operation. Only one foreign key is supported."
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
foreign_key, attributes = foreign_keys.first
|
|
365
|
+
reference = attributes.fetch(:references)
|
|
366
|
+
if forward_join
|
|
367
|
+
unique = attributes[:unique] == true
|
|
368
|
+
{from: reference.fetch(:field).to_s, to: foreign_key, relation: unique ? "one-to-one" : "one-to-many", unique: unique}
|
|
369
|
+
else
|
|
370
|
+
{from: foreign_key, to: reference.fetch(:field).to_s, relation: "one-to-one", unique: true}
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def reference_model_matches?(attributes, model)
|
|
375
|
+
reference = attributes[:references]
|
|
376
|
+
return false unless reference
|
|
377
|
+
|
|
378
|
+
default_model_name(reference[:model] || reference["model"]) == model
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def unique_join_field?(model, field)
|
|
382
|
+
field = resolve_field(model, field)
|
|
383
|
+
field == "id" || fields_for(model).dig(field, :unique) == true
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def storage_field_for_join(model, field)
|
|
387
|
+
field = resolve_field(model, field)
|
|
388
|
+
(field == "id") ? "_id" : storage_field(model, field)
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
def projection_for(model, select, join)
|
|
392
|
+
selected_fields = Array(select).map { |field| storage_field_for_join(model, field) }
|
|
393
|
+
Array(select).each_with_object({}) do |field, projection|
|
|
394
|
+
projection[storage_field_for_join(model, field)] = 1
|
|
395
|
+
end.tap do |projection|
|
|
396
|
+
projection["_id"] = 0 unless selected_fields.include?("_id")
|
|
397
|
+
normalized_join(model, join).each_key { |join_model| projection[join_model] = 1 } if join
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
def sort_field(model, sort_by)
|
|
402
|
+
field = resolve_field(model, fetch_key(sort_by, :field))
|
|
403
|
+
storage_field_for_join(model, field)
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def sort_direction(sort_by)
|
|
407
|
+
(fetch_key(sort_by, :direction).to_s == "desc") ? -1 : 1
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
def collection_for(model)
|
|
411
|
+
database.collection(collection_name(model))
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
def collection_name(model)
|
|
415
|
+
model = default_model_name(model)
|
|
416
|
+
configured = configured_model_name(model)
|
|
417
|
+
return "#{configured}s" if configured && use_plural
|
|
418
|
+
return configured if configured
|
|
419
|
+
return schema_for(model).fetch(:model_name) if use_plural
|
|
420
|
+
|
|
421
|
+
model.to_s
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
def to_document(model, record)
|
|
425
|
+
fields_for(model).each_with_object({}) do |(field, attributes), document|
|
|
426
|
+
next unless record.key?(field)
|
|
427
|
+
|
|
428
|
+
key = (field == "id") ? "_id" : storage_field(model, field)
|
|
429
|
+
document[key] = store_value(field, record[field], attributes)
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
def from_document(model, document, join: nil)
|
|
434
|
+
fields = fields_for(model)
|
|
435
|
+
record = fields.each_with_object({}) do |(field, attributes), output|
|
|
436
|
+
key = (field == "id") ? "_id" : storage_field(model, field)
|
|
437
|
+
output[field] = output_value(field, fetch_document(document, key), attributes) if document_key?(document, key)
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
if join
|
|
441
|
+
normalized_join(model, join).each do |join_model, config|
|
|
442
|
+
next unless document_key?(document, join_model)
|
|
443
|
+
|
|
444
|
+
joined_value = fetch_document(document, join_model)
|
|
445
|
+
record[join_model] = if joined_value.is_a?(Array)
|
|
446
|
+
joined_value.map { |entry| from_document(join_model, stringify_document(entry)) }
|
|
447
|
+
elsif joined_value
|
|
448
|
+
from_document(join_model, stringify_document(joined_value))
|
|
449
|
+
elsif config[:relation] == "one-to-one"
|
|
450
|
+
nil
|
|
451
|
+
else
|
|
452
|
+
[]
|
|
453
|
+
end
|
|
454
|
+
end
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
record
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
def stringify_document(document)
|
|
461
|
+
document.each_with_object({}) { |(key, value), result| result[key.to_s] = value }
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
def unwrap_update_result(result)
|
|
465
|
+
return result unless result.is_a?(Hash)
|
|
466
|
+
return result if document_key?(result, "_id")
|
|
467
|
+
|
|
468
|
+
if result.key?("value") && (result.key?("ok") || result.key?("lastErrorObject"))
|
|
469
|
+
return result["value"]
|
|
470
|
+
end
|
|
471
|
+
if result.key?(:value) && (result.key?(:ok) || result.key?(:last_error_object))
|
|
472
|
+
return result[:value]
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
result
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
def store_value(field, value, attributes, strict_id: false)
|
|
479
|
+
return nil if value.nil?
|
|
480
|
+
return Array(value).map { |entry| store_value(field, entry, attributes, strict_id: strict_id) } if value.is_a?(Array)
|
|
481
|
+
|
|
482
|
+
if id_field?(field, attributes)
|
|
483
|
+
return value if custom_id_generator?
|
|
484
|
+
return bson_id(value, strict: strict_id)
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
input_value(value, attributes)
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
def output_value(field, value, attributes)
|
|
491
|
+
return nil if value.nil?
|
|
492
|
+
if id_field?(field, attributes)
|
|
493
|
+
return value.to_uuid if bson_uuid?(value)
|
|
494
|
+
return value.to_s if value.is_a?(BSON::ObjectId)
|
|
495
|
+
return value.map { |entry| output_value(field, entry, attributes) } if value.is_a?(Array)
|
|
496
|
+
return value
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
output_scalar_value(value, attributes)
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
def id_field?(field, attributes)
|
|
503
|
+
field.to_s == "id" || attributes.dig(:references, :field) == "id"
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
def bson_id(value, strict:)
|
|
507
|
+
if use_uuid_ids?
|
|
508
|
+
return value if bson_uuid?(value)
|
|
509
|
+
return BSON::Binary.from_uuid(value.to_s) if value.is_a?(String)
|
|
510
|
+
raise MongoAdapterError.new("INVALID_ID", "Invalid id value") if strict
|
|
511
|
+
|
|
512
|
+
return value
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
return value if value.is_a?(BSON::ObjectId)
|
|
516
|
+
return BSON::ObjectId.from_string(value.to_s) if value.is_a?(String)
|
|
517
|
+
raise MongoAdapterError.new("INVALID_ID", "Invalid id value") if strict
|
|
518
|
+
|
|
519
|
+
value
|
|
520
|
+
rescue BSON::Error::InvalidObjectId, ArgumentError
|
|
521
|
+
value
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
def bson_uuid?(value)
|
|
525
|
+
defined?(BSON::Binary) && value.is_a?(BSON::Binary) && value.respond_to?(:to_uuid) && value.type == :uuid
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
def generated_id
|
|
529
|
+
generator = options.advanced.dig(:database, :generate_id)
|
|
530
|
+
return generator.call if generator.respond_to?(:call)
|
|
531
|
+
return SecureRandom.uuid if use_uuid_ids?
|
|
532
|
+
return BSON::ObjectId.new.to_s if defined?(BSON::ObjectId)
|
|
533
|
+
|
|
534
|
+
SecureRandom.hex(12)
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
def use_uuid_ids?
|
|
538
|
+
options.advanced.dig(:database, :generate_id) == "uuid"
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
def custom_id_generator?
|
|
542
|
+
options.advanced.dig(:database, :generate_id).respond_to?(:call)
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
def resolve_default(default)
|
|
546
|
+
default.respond_to?(:call) ? default.call : default
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
def coerce_value(value, attributes)
|
|
550
|
+
return value if value.nil?
|
|
551
|
+
return Time.parse(value) if attributes[:type] == "date" && value.is_a?(String)
|
|
552
|
+
|
|
553
|
+
value
|
|
554
|
+
end
|
|
555
|
+
|
|
556
|
+
def input_value(value, attributes)
|
|
557
|
+
value = coerce_value(value, attributes)
|
|
558
|
+
return JSON.generate(value) if attributes[:type] == "json" && (value.is_a?(Hash) || value.is_a?(Array))
|
|
559
|
+
|
|
560
|
+
value
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
def output_scalar_value(value, attributes)
|
|
564
|
+
return JSON.parse(value) if attributes[:type] == "json" && value.is_a?(String)
|
|
565
|
+
|
|
566
|
+
coerce_value(value, attributes)
|
|
567
|
+
rescue JSON::ParserError
|
|
568
|
+
value
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
def coerce_where_value(value, attributes)
|
|
572
|
+
return value.map { |entry| coerce_where_value(entry, attributes) } if value.is_a?(Array)
|
|
573
|
+
return value == "true" if attributes[:type] == "boolean" && value.is_a?(String)
|
|
574
|
+
if attributes[:type] == "number" && value.is_a?(String) && !value.strip.empty?
|
|
575
|
+
parsed = Float(value)
|
|
576
|
+
return parsed.to_i if parsed.to_i == parsed
|
|
577
|
+
|
|
578
|
+
return parsed
|
|
579
|
+
end
|
|
580
|
+
return JSON.generate(value) if attributes[:type] == "json" && (value.is_a?(Hash) || value.is_a?(Array))
|
|
581
|
+
|
|
582
|
+
value
|
|
583
|
+
rescue ArgumentError
|
|
584
|
+
value
|
|
585
|
+
end
|
|
586
|
+
|
|
587
|
+
def session_options
|
|
588
|
+
@session ? {session: @session} : {}
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
def document_key?(document, key)
|
|
592
|
+
document.key?(key) || document.key?(key.to_sym)
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
def fetch_document(document, key)
|
|
596
|
+
return document[key] if document.key?(key)
|
|
597
|
+
|
|
598
|
+
document[key.to_sym]
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
def stringify_keys(data)
|
|
602
|
+
data.each_with_object({}) do |(key, value), result|
|
|
603
|
+
result[Schema.storage_key(key)] = value
|
|
604
|
+
end
|
|
605
|
+
end
|
|
606
|
+
|
|
607
|
+
def fetch_key(hash, key)
|
|
608
|
+
[key, key.to_s, Schema.storage_key(key), Schema.storage_key(key).to_sym].each do |candidate|
|
|
609
|
+
return hash[candidate] if hash.key?(candidate)
|
|
610
|
+
end
|
|
611
|
+
nil
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
def schema_for(model)
|
|
615
|
+
Schema.auth_tables(options).fetch(default_model_name(model))
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
def fields_for(model)
|
|
619
|
+
schema_for(model).fetch(:fields).merge("id" => {type: "string", required: true})
|
|
620
|
+
end
|
|
621
|
+
|
|
622
|
+
def default_model_name(model)
|
|
623
|
+
model = model.to_s
|
|
624
|
+
tables = Schema.auth_tables(options)
|
|
625
|
+
return model if tables.key?(model)
|
|
626
|
+
|
|
627
|
+
pluraless = model.end_with?("s") ? model[0...-1] : nil
|
|
628
|
+
return pluraless if pluraless && tables.key?(pluraless)
|
|
629
|
+
|
|
630
|
+
matched = tables.find { |_key, table| table[:model_name].to_s == model }
|
|
631
|
+
return matched.first if matched
|
|
632
|
+
|
|
633
|
+
raise Error, "Model \"#{model}\" not found in schema"
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
def configured_model_name(model)
|
|
637
|
+
configured = configured_model_option(model, :model_name)
|
|
638
|
+
return configured.to_s if configured
|
|
639
|
+
|
|
640
|
+
return nil if core_model?(model)
|
|
641
|
+
|
|
642
|
+
table_model_name = schema_for(model).fetch(:model_name).to_s
|
|
643
|
+
(table_model_name == physical_name(model)) ? nil : table_model_name
|
|
644
|
+
end
|
|
645
|
+
|
|
646
|
+
def configured_model_option(model, key)
|
|
647
|
+
data = options.respond_to?(model.to_sym) ? options.public_send(model.to_sym) : nil
|
|
648
|
+
data[key] || data[key.to_s] if data.respond_to?(:[])
|
|
649
|
+
end
|
|
650
|
+
|
|
651
|
+
def core_model?(model)
|
|
652
|
+
["user", "session", "account", "verification", "rateLimit"].include?(model.to_s)
|
|
653
|
+
end
|
|
654
|
+
|
|
655
|
+
def resolve_field(model, field)
|
|
656
|
+
field = Schema.storage_key(field)
|
|
657
|
+
return "id" if field == "id" || field == "_id"
|
|
658
|
+
|
|
659
|
+
fields = fields_for(model)
|
|
660
|
+
return field if fields.key?(field)
|
|
661
|
+
|
|
662
|
+
matched = fields.find { |_key, attributes| attributes[:field_name].to_s == field.to_s }
|
|
663
|
+
return matched.first if matched
|
|
664
|
+
|
|
665
|
+
raise Error, "Field #{field} not found in model #{model}"
|
|
666
|
+
end
|
|
667
|
+
|
|
668
|
+
def storage_field(model, field)
|
|
669
|
+
fields_for(model).fetch(field.to_s).fetch(:field_name, physical_name(field))
|
|
670
|
+
end
|
|
671
|
+
|
|
672
|
+
def physical_name(value)
|
|
673
|
+
value.to_s
|
|
674
|
+
.gsub(/([a-z\d])([A-Z])/, "\\1_\\2")
|
|
675
|
+
.tr("-", "_")
|
|
676
|
+
.downcase
|
|
677
|
+
end
|
|
678
|
+
end
|
|
679
|
+
end
|
|
680
|
+
|
|
681
|
+
MongoAdapter = MongoDB unless const_defined?(:MongoAdapter, false)
|
|
682
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: better_auth-mongodb
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.8.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Sebastian Sala
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: better_auth
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.1'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.1'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: bigdecimal
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '3.1'
|
|
33
|
+
- - "<"
|
|
34
|
+
- !ruby/object:Gem::Version
|
|
35
|
+
version: '5.0'
|
|
36
|
+
type: :runtime
|
|
37
|
+
prerelease: false
|
|
38
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
39
|
+
requirements:
|
|
40
|
+
- - ">="
|
|
41
|
+
- !ruby/object:Gem::Version
|
|
42
|
+
version: '3.1'
|
|
43
|
+
- - "<"
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: '5.0'
|
|
46
|
+
- !ruby/object:Gem::Dependency
|
|
47
|
+
name: logger
|
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
|
49
|
+
requirements:
|
|
50
|
+
- - ">="
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: '1.6'
|
|
53
|
+
- - "<"
|
|
54
|
+
- !ruby/object:Gem::Version
|
|
55
|
+
version: '2.0'
|
|
56
|
+
type: :runtime
|
|
57
|
+
prerelease: false
|
|
58
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
59
|
+
requirements:
|
|
60
|
+
- - ">="
|
|
61
|
+
- !ruby/object:Gem::Version
|
|
62
|
+
version: '1.6'
|
|
63
|
+
- - "<"
|
|
64
|
+
- !ruby/object:Gem::Version
|
|
65
|
+
version: '2.0'
|
|
66
|
+
- !ruby/object:Gem::Dependency
|
|
67
|
+
name: mongo
|
|
68
|
+
requirement: !ruby/object:Gem::Requirement
|
|
69
|
+
requirements:
|
|
70
|
+
- - "~>"
|
|
71
|
+
- !ruby/object:Gem::Version
|
|
72
|
+
version: '2.21'
|
|
73
|
+
type: :runtime
|
|
74
|
+
prerelease: false
|
|
75
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
76
|
+
requirements:
|
|
77
|
+
- - "~>"
|
|
78
|
+
- !ruby/object:Gem::Version
|
|
79
|
+
version: '2.21'
|
|
80
|
+
- !ruby/object:Gem::Dependency
|
|
81
|
+
name: bundler
|
|
82
|
+
requirement: !ruby/object:Gem::Requirement
|
|
83
|
+
requirements:
|
|
84
|
+
- - "~>"
|
|
85
|
+
- !ruby/object:Gem::Version
|
|
86
|
+
version: '2.5'
|
|
87
|
+
type: :development
|
|
88
|
+
prerelease: false
|
|
89
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
90
|
+
requirements:
|
|
91
|
+
- - "~>"
|
|
92
|
+
- !ruby/object:Gem::Version
|
|
93
|
+
version: '2.5'
|
|
94
|
+
- !ruby/object:Gem::Dependency
|
|
95
|
+
name: minitest
|
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
|
97
|
+
requirements:
|
|
98
|
+
- - "~>"
|
|
99
|
+
- !ruby/object:Gem::Version
|
|
100
|
+
version: '5.25'
|
|
101
|
+
type: :development
|
|
102
|
+
prerelease: false
|
|
103
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
104
|
+
requirements:
|
|
105
|
+
- - "~>"
|
|
106
|
+
- !ruby/object:Gem::Version
|
|
107
|
+
version: '5.25'
|
|
108
|
+
- !ruby/object:Gem::Dependency
|
|
109
|
+
name: rake
|
|
110
|
+
requirement: !ruby/object:Gem::Requirement
|
|
111
|
+
requirements:
|
|
112
|
+
- - "~>"
|
|
113
|
+
- !ruby/object:Gem::Version
|
|
114
|
+
version: '13.2'
|
|
115
|
+
type: :development
|
|
116
|
+
prerelease: false
|
|
117
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
118
|
+
requirements:
|
|
119
|
+
- - "~>"
|
|
120
|
+
- !ruby/object:Gem::Version
|
|
121
|
+
version: '13.2'
|
|
122
|
+
- !ruby/object:Gem::Dependency
|
|
123
|
+
name: standardrb
|
|
124
|
+
requirement: !ruby/object:Gem::Requirement
|
|
125
|
+
requirements:
|
|
126
|
+
- - "~>"
|
|
127
|
+
- !ruby/object:Gem::Version
|
|
128
|
+
version: '1.0'
|
|
129
|
+
type: :development
|
|
130
|
+
prerelease: false
|
|
131
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
132
|
+
requirements:
|
|
133
|
+
- - "~>"
|
|
134
|
+
- !ruby/object:Gem::Version
|
|
135
|
+
version: '1.0'
|
|
136
|
+
- !ruby/object:Gem::Dependency
|
|
137
|
+
name: simplecov
|
|
138
|
+
requirement: !ruby/object:Gem::Requirement
|
|
139
|
+
requirements:
|
|
140
|
+
- - "~>"
|
|
141
|
+
- !ruby/object:Gem::Version
|
|
142
|
+
version: '0.22'
|
|
143
|
+
type: :development
|
|
144
|
+
prerelease: false
|
|
145
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
146
|
+
requirements:
|
|
147
|
+
- - "~>"
|
|
148
|
+
- !ruby/object:Gem::Version
|
|
149
|
+
version: '0.22'
|
|
150
|
+
description: Adds a MongoDB database adapter for Better Auth Ruby. Better Auth Ruby
|
|
151
|
+
is an independent modern authentication framework for Ruby inspired by Better Auth.
|
|
152
|
+
Keeps MongoDB dependencies out of the core gem.
|
|
153
|
+
email:
|
|
154
|
+
- sebastian.sala.tech@gmail.com
|
|
155
|
+
executables: []
|
|
156
|
+
extensions: []
|
|
157
|
+
extra_rdoc_files: []
|
|
158
|
+
files:
|
|
159
|
+
- CHANGELOG.md
|
|
160
|
+
- README.md
|
|
161
|
+
- lib/better_auth/mongo_adapter.rb
|
|
162
|
+
- lib/better_auth/mongodb.rb
|
|
163
|
+
- lib/better_auth/mongodb/version.rb
|
|
164
|
+
homepage: https://github.com/sebasxsala/better-auth
|
|
165
|
+
licenses:
|
|
166
|
+
- MIT
|
|
167
|
+
metadata:
|
|
168
|
+
homepage_uri: https://github.com/sebasxsala/better-auth
|
|
169
|
+
source_code_uri: https://github.com/sebasxsala/better-auth
|
|
170
|
+
changelog_uri: https://github.com/sebasxsala/better-auth/blob/main/packages/better_auth-mongodb/CHANGELOG.md
|
|
171
|
+
bug_tracker_uri: https://github.com/sebasxsala/better-auth/issues
|
|
172
|
+
rdoc_options: []
|
|
173
|
+
require_paths:
|
|
174
|
+
- lib
|
|
175
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
176
|
+
requirements:
|
|
177
|
+
- - ">="
|
|
178
|
+
- !ruby/object:Gem::Version
|
|
179
|
+
version: 3.2.0
|
|
180
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
181
|
+
requirements:
|
|
182
|
+
- - ">="
|
|
183
|
+
- !ruby/object:Gem::Version
|
|
184
|
+
version: '0'
|
|
185
|
+
requirements: []
|
|
186
|
+
rubygems_version: 3.6.9
|
|
187
|
+
specification_version: 4
|
|
188
|
+
summary: MongoDB adapter package for Better Auth Ruby
|
|
189
|
+
test_files: []
|