active-query 0.1.1 → 0.1.2
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/README.md +222 -89
- data/lib/active-query.rb +3 -0
- data/lib/active_query/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 597183debb31f2b4490361200975eccbeeec9ef03e5d844abfe193e4114284d2
|
4
|
+
data.tar.gz: c82fd9b968a2dffcdd83914ea08bdc7b332453322008e41436d3f7ff7221e53e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c8abf99aad689c83d93223dfa5de1e5e7c2ece56b6235ca6594b50f2b5413a03a06e8fb44dbf815137c3217e5eebf18335f52204450b3aab7899e7064a969d65
|
7
|
+
data.tar.gz: 8c5165ccf7c2463198e39748fcf47f5c92bc916dd5ce6f6b448e5f682c18b88fdfe05d774e385a0f716cb6bc776f822357705834878f1fe96114b2eed74bdf4e
|
data/README.md
CHANGED
@@ -1,14 +1,16 @@
|
|
1
1
|
# ActiveQuery
|
2
2
|
|
3
|
-
ActiveQuery is a gem that helps you create query objects
|
3
|
+
ActiveQuery is a Ruby gem that helps you create clean, reusable query objects with a simple DSL. It provides type validation, conditional logic, and seamless ActiveRecord integration.
|
4
4
|
|
5
5
|
## Features
|
6
6
|
|
7
|
-
- **Query
|
8
|
-
- **Type
|
9
|
-
- **
|
10
|
-
- **Operations**:
|
11
|
-
- **
|
7
|
+
- **Clean Query DSL**: Define queries with clear syntax and descriptions
|
8
|
+
- **Type Safety**: Built-in argument type validation (String, Integer, Float, Boolean)
|
9
|
+
- **Optional & Default Arguments**: Flexible argument handling
|
10
|
+
- **Custom Operations**: Extended query operations like `gt`, `lt`, `like`, etc.
|
11
|
+
- **Conditional Logic**: Apply scopes conditionally with `if` and `unless`
|
12
|
+
- **Resolver Pattern**: Support for complex query logic in separate classes
|
13
|
+
- **Custom Scopes**: Define reusable scopes within query objects
|
12
14
|
- **ActiveRecord Integration**: Works seamlessly with ActiveRecord models
|
13
15
|
|
14
16
|
## Installation
|
@@ -25,162 +27,293 @@ And then execute:
|
|
25
27
|
bundle install
|
26
28
|
```
|
27
29
|
|
28
|
-
Or install it yourself as:
|
29
|
-
|
30
|
-
```bash
|
31
|
-
gem install active-query
|
32
|
-
```
|
33
|
-
|
34
30
|
## Usage
|
35
31
|
|
36
32
|
### Basic Query Object
|
37
33
|
|
38
|
-
Create a query object by including `ActiveQuery::Base
|
34
|
+
Create a query object by including `ActiveQuery::Base` and defining queries with the `query` method:
|
39
35
|
|
40
36
|
```ruby
|
41
37
|
class UserQuery
|
42
38
|
include ActiveQuery::Base
|
43
39
|
|
44
40
|
# The model is automatically inferred from the class name (User)
|
45
|
-
# Or
|
46
|
-
|
41
|
+
# Or explicitly set it:
|
42
|
+
model_name 'User'
|
47
43
|
|
48
|
-
|
49
|
-
|
50
|
-
end
|
44
|
+
# Simple query without arguments
|
45
|
+
query :active, 'Returns all active users', -> { scope.where(active: true) }
|
51
46
|
|
52
|
-
|
53
|
-
|
54
|
-
|
47
|
+
# Query with arguments and type validation
|
48
|
+
query :by_email, 'Find users by email address',
|
49
|
+
{ email: { type: String } },
|
50
|
+
-> (email:) { scope.where(email: email) }
|
55
51
|
end
|
56
52
|
```
|
57
53
|
|
58
54
|
### Using Query Objects
|
59
55
|
|
60
56
|
```ruby
|
61
|
-
#
|
62
|
-
users = UserQuery.by_email(email: 'john@example.com')
|
63
|
-
|
64
|
-
# Find active users
|
57
|
+
# Execute queries
|
65
58
|
active_users = UserQuery.active
|
59
|
+
user = UserQuery.by_email(email: 'john@example.com')
|
66
60
|
|
67
|
-
# Chain
|
68
|
-
|
61
|
+
# Chain with ActiveRecord methods
|
62
|
+
recent_active_users = UserQuery.active.where('created_at > ?', 1.week.ago)
|
69
63
|
```
|
70
64
|
|
71
|
-
###
|
65
|
+
### Argument Types and Validation
|
66
|
+
|
67
|
+
ActiveQuery supports several argument types with automatic validation:
|
72
68
|
|
73
69
|
```ruby
|
74
70
|
class ProductQuery
|
75
71
|
include ActiveQuery::Base
|
76
72
|
|
77
|
-
query :
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
73
|
+
query :filter_products, 'Filter products by various criteria',
|
74
|
+
{
|
75
|
+
name: { type: String },
|
76
|
+
price: { type: Float },
|
77
|
+
quantity: { type: Integer },
|
78
|
+
available: { type: ActiveQuery::Base::Boolean }
|
79
|
+
},
|
80
|
+
-> (name:, price:, quantity:, available:) {
|
81
|
+
scope.where(name: name)
|
82
|
+
.where(price: price)
|
83
|
+
.where(quantity: quantity)
|
84
|
+
.where(available: available)
|
85
|
+
}
|
86
86
|
end
|
87
87
|
|
88
|
-
# Usage
|
89
|
-
|
90
|
-
|
88
|
+
# Usage with type validation
|
89
|
+
ProductQuery.filter_products(
|
90
|
+
name: 'Widget',
|
91
|
+
price: 19.99,
|
92
|
+
quantity: 10,
|
93
|
+
available: true
|
94
|
+
)
|
91
95
|
|
92
|
-
|
96
|
+
# This will raise ArgumentError due to type mismatch
|
97
|
+
ProductQuery.filter_products(name: 123, price: 'invalid', quantity: true, available: 'yes')
|
98
|
+
```
|
93
99
|
|
94
|
-
|
100
|
+
### Optional Arguments and Defaults
|
95
101
|
|
96
102
|
```ruby
|
97
103
|
class OrderQuery
|
98
104
|
include ActiveQuery::Base
|
99
|
-
include ActiveQuery::Scopes
|
100
|
-
|
101
|
-
scope :recent, -> { where('created_at > ?', 1.week.ago) }
|
102
|
-
scope :completed, -> { where(status: 'completed') }
|
103
105
|
|
104
|
-
query :
|
105
|
-
|
106
|
-
|
106
|
+
query :search_orders, 'Search orders with optional filters',
|
107
|
+
{
|
108
|
+
status: { type: String },
|
109
|
+
paid: { type: ActiveQuery::Base::Boolean, default: true },
|
110
|
+
customer_name: { type: String, optional: true }
|
111
|
+
},
|
112
|
+
-> (status:, paid:, customer_name:) {
|
113
|
+
scope.where(status: status)
|
114
|
+
.where(paid: paid)
|
115
|
+
.then { |s| customer_name ? s.where('customer_name LIKE ?', "%#{customer_name}%") : s }
|
116
|
+
}
|
107
117
|
end
|
118
|
+
|
119
|
+
# Usage - customer_name is optional, paid defaults to true
|
120
|
+
OrderQuery.search_orders(status: 'pending')
|
121
|
+
OrderQuery.search_orders(status: 'completed', customer_name: 'John')
|
122
|
+
OrderQuery.search_orders(status: 'pending', paid: false, customer_name: 'Jane')
|
108
123
|
```
|
109
124
|
|
110
|
-
###
|
125
|
+
### Conditional Query Logic
|
111
126
|
|
112
|
-
|
127
|
+
Use `if` and `unless` methods for conditional query building:
|
113
128
|
|
114
129
|
```ruby
|
115
130
|
class UserQuery
|
116
131
|
include ActiveQuery::Base
|
117
132
|
|
118
|
-
query :
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
133
|
+
query :search_users, 'Search users with conditional filters',
|
134
|
+
{
|
135
|
+
name: { type: String, optional: true },
|
136
|
+
active: { type: ActiveQuery::Base::Boolean, optional: true }
|
137
|
+
},
|
138
|
+
-> (name:, active:) {
|
139
|
+
scope.if(name.present?, -> { where('name LIKE ?', "%#{name}%") })
|
140
|
+
.if(active == true, -> { where(active: true) })
|
141
|
+
}
|
125
142
|
|
126
|
-
query :
|
127
|
-
|
128
|
-
|
143
|
+
query :filter_unless_admin, 'Filter users unless they are admin',
|
144
|
+
{
|
145
|
+
role: { type: String, optional: true }
|
146
|
+
},
|
147
|
+
-> (role:) {
|
148
|
+
scope.unless(role == 'admin', -> { where.not(role: 'admin') })
|
149
|
+
}
|
129
150
|
end
|
130
151
|
```
|
131
152
|
|
132
|
-
|
153
|
+
### Extended Query Operations
|
154
|
+
|
155
|
+
ActiveQuery provides additional query operations beyond standard ActiveRecord:
|
156
|
+
|
157
|
+
```ruby
|
158
|
+
class ProductQuery
|
159
|
+
include ActiveQuery::Base
|
160
|
+
|
161
|
+
# Comparison operations
|
162
|
+
query :expensive_products, 'Products above price threshold', -> { scope.gt(:price, 100) }
|
163
|
+
query :affordable_products, 'Products within budget', -> { scope.lteq(:price, 50) }
|
164
|
+
|
165
|
+
# Text search operations
|
166
|
+
query :search_by_name, 'Search products by name pattern', -> { scope.like(:name, 'Phone') }
|
167
|
+
query :products_starting_with, 'Products starting with prefix', -> { scope.start_like(:name, 'iPhone') }
|
168
|
+
query :products_ending_with, 'Products ending with suffix', -> { scope.end_like(:name, 'Pro') }
|
169
|
+
|
170
|
+
# Dynamic filtering
|
171
|
+
query :by_price_range, 'Filter by price range',
|
172
|
+
{ min_price: { type: Float }, max_price: { type: Float } },
|
173
|
+
-> (min_price:, max_price:) {
|
174
|
+
scope.gteq(:price, min_price)
|
175
|
+
.lteq(:price, max_price)
|
176
|
+
}
|
177
|
+
end
|
178
|
+
```
|
179
|
+
|
180
|
+
**Available operations:**
|
133
181
|
- `gt(column, value)` - greater than
|
134
|
-
- `gteq(column, value)` - greater than or equal
|
135
|
-
- `lt(column, value)` - less than
|
182
|
+
- `gteq(column, value)` - greater than or equal
|
183
|
+
- `lt(column, value)` - less than
|
136
184
|
- `lteq(column, value)` - less than or equal
|
137
|
-
- `like(column, value)` - contains pattern
|
138
|
-
- `start_like(column, value)` - starts with pattern
|
185
|
+
- `like(column, value)` - contains pattern (wraps with %)
|
186
|
+
- `start_like(column, value)` - starts with pattern
|
139
187
|
- `end_like(column, value)` - ends with pattern
|
140
188
|
|
141
|
-
###
|
189
|
+
### Custom Scopes
|
142
190
|
|
143
|
-
|
191
|
+
Define reusable scopes within your query objects:
|
144
192
|
|
145
193
|
```ruby
|
194
|
+
class UserQuery
|
195
|
+
include ActiveQuery::Base
|
196
|
+
include ActiveQuery::Scopes
|
197
|
+
|
198
|
+
# Define custom scopes
|
199
|
+
module Scopes
|
200
|
+
include ActiveQuery::Scopes
|
201
|
+
|
202
|
+
scope :recent, -> { where('created_at > ?', 1.month.ago) }
|
203
|
+
scope :by_role, -> (role:) { where(role: role) }
|
204
|
+
end
|
205
|
+
|
206
|
+
# Use scopes in queries
|
207
|
+
query :recent_admins, 'Find recent admin users', -> { scope.recent.by_role(role: 'admin') }
|
208
|
+
|
209
|
+
query :count_recent, 'Count recent users', -> { scope.recent.count }
|
210
|
+
end
|
211
|
+
```
|
212
|
+
|
213
|
+
### Resolver Pattern
|
214
|
+
|
215
|
+
For complex query logic, use the resolver pattern to keep your query objects clean:
|
216
|
+
|
217
|
+
```ruby
|
218
|
+
# Define a resolver
|
146
219
|
class UserStatsResolver < ActiveQuery::Resolver
|
147
|
-
def resolve(
|
148
|
-
|
149
|
-
|
150
|
-
|
220
|
+
def resolve(min_orders: 5)
|
221
|
+
scope.joins(:orders)
|
222
|
+
.group('users.id')
|
223
|
+
.having('COUNT(orders.id) >= ?', min_orders)
|
224
|
+
.select('users.*, COUNT(orders.id) as order_count')
|
151
225
|
end
|
152
226
|
end
|
153
227
|
|
228
|
+
# Use resolver in query object
|
154
229
|
class UserQuery
|
155
230
|
include ActiveQuery::Base
|
156
231
|
|
157
|
-
|
158
|
-
|
159
|
-
|
232
|
+
# Resolver without arguments
|
233
|
+
query :high_value_users, 'Users with many orders',
|
234
|
+
resolver: UserStatsResolver
|
235
|
+
|
236
|
+
# Resolver with arguments
|
237
|
+
query :users_with_orders, 'Users with minimum order count',
|
238
|
+
{ min_orders: { type: Integer } },
|
239
|
+
resolver: UserStatsResolver
|
160
240
|
end
|
161
241
|
|
162
242
|
# Usage
|
163
|
-
|
243
|
+
UserQuery.high_value_users
|
244
|
+
UserQuery.users_with_orders(min_orders: 10)
|
164
245
|
```
|
165
246
|
|
166
|
-
###
|
247
|
+
### Query Introspection
|
167
248
|
|
168
|
-
|
249
|
+
Query objects provide metadata about available queries:
|
169
250
|
|
170
251
|
```ruby
|
171
|
-
class
|
252
|
+
class UserQuery
|
172
253
|
include ActiveQuery::Base
|
173
254
|
|
174
|
-
query :
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
255
|
+
query :active, 'Find active users', -> { scope.where(active: true) }
|
256
|
+
query :by_name, 'Find by name', { name: { type: String } }, -> (name:) { scope.where(name: name) }
|
257
|
+
end
|
258
|
+
|
259
|
+
# Get all available queries
|
260
|
+
UserQuery.queries
|
261
|
+
# => [
|
262
|
+
# { name: :active, description: "Find active users", args_def: {} },
|
263
|
+
# { name: :by_name, description: "Find by name", args_def: { name: { type: String } } }
|
264
|
+
# ]
|
265
|
+
```
|
266
|
+
|
267
|
+
### Error Handling
|
268
|
+
|
269
|
+
ActiveQuery provides clear error messages for common mistakes:
|
270
|
+
|
271
|
+
```ruby
|
272
|
+
# Missing required arguments
|
273
|
+
UserQuery.by_email
|
274
|
+
# => ArgumentError: Params missing: [:email]
|
275
|
+
|
276
|
+
# Wrong argument type
|
277
|
+
UserQuery.by_email(email: 123)
|
278
|
+
# => ArgumentError: :email must be of type String
|
279
|
+
|
280
|
+
# Unknown arguments
|
281
|
+
UserQuery.by_email(email: 'test@example.com', invalid_param: 'value')
|
282
|
+
# => ArgumentError: Unknown params: [:invalid_param]
|
283
|
+
|
284
|
+
# Optional and default together (validation error)
|
285
|
+
query :invalid_query, 'This will fail',
|
286
|
+
{ param: { type: String, optional: true, default: 'value' } },
|
287
|
+
-> (param:) { scope }
|
288
|
+
# => ArgumentError: Optional and Default params can't be present together: [:param]
|
289
|
+
```
|
290
|
+
|
291
|
+
### Integration with Rails
|
292
|
+
|
293
|
+
ActiveQuery works seamlessly with Rails applications:
|
294
|
+
|
295
|
+
```ruby
|
296
|
+
# app/queries/user_query.rb
|
297
|
+
class UserQuery
|
298
|
+
include ActiveQuery::Base
|
299
|
+
|
300
|
+
query :active, 'Active users', -> { scope.where(active: true) }
|
301
|
+
query :by_role, 'Users by role', { role: { type: String } }, -> (role:) { scope.where(role: role) }
|
302
|
+
end
|
303
|
+
|
304
|
+
# In controllers
|
305
|
+
class UsersController < ApplicationController
|
306
|
+
def index
|
307
|
+
@users = UserQuery.active
|
308
|
+
end
|
309
|
+
|
310
|
+
def admins
|
311
|
+
@admins = UserQuery.by_role(role: 'admin')
|
182
312
|
end
|
183
313
|
end
|
314
|
+
|
315
|
+
# In views or anywhere else
|
316
|
+
<%= UserQuery.active.count %> active users
|
184
317
|
```
|
185
318
|
|
186
319
|
## Requirements
|
@@ -191,9 +324,9 @@ end
|
|
191
324
|
|
192
325
|
## Development
|
193
326
|
|
194
|
-
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt
|
327
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt.
|
195
328
|
|
196
|
-
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release
|
329
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`.
|
197
330
|
|
198
331
|
## Contributing
|
199
332
|
|
data/lib/active-query.rb
ADDED
data/lib/active_query/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: active-query
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Matias Asis
|
@@ -151,6 +151,7 @@ files:
|
|
151
151
|
- README.md
|
152
152
|
- Rakefile
|
153
153
|
- exe/active-query
|
154
|
+
- lib/active-query.rb
|
154
155
|
- lib/active_query.rb
|
155
156
|
- lib/active_query/resolver.rb
|
156
157
|
- lib/active_query/version.rb
|