rails_query 0.1.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 +5 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +186 -0
- data/Rakefile +12 -0
- data/lib/generators/rails_query/install/install_generator.rb +57 -0
- data/lib/generators/rails_query/install/templates/find_user_query.rb.tt +18 -0
- data/lib/generators/rails_query/install/templates/rails_query.rb.tt +11 -0
- data/lib/generators/rails_query/install/templates/user_provider.rb.tt +8 -0
- data/lib/generators/rails_query/mutation/mutation_generator.rb +60 -0
- data/lib/generators/rails_query/mutation/templates/mutation.rb.tt +13 -0
- data/lib/generators/rails_query/mutation/templates/provider.rb.tt +5 -0
- data/lib/generators/rails_query/query/query_generator.rb +60 -0
- data/lib/generators/rails_query/query/templates/provider.rb.tt +5 -0
- data/lib/generators/rails_query/query/templates/query.rb.tt +18 -0
- data/lib/rails_query/adapter.rb +114 -0
- data/lib/rails_query/client.rb +56 -0
- data/lib/rails_query/configuration.rb +33 -0
- data/lib/rails_query/mutation.rb +43 -0
- data/lib/rails_query/query.rb +49 -0
- data/lib/rails_query/railtie.rb +12 -0
- data/lib/rails_query/version.rb +5 -0
- data/lib/rails_query.rb +39 -0
- metadata +143 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: daae5d37d67d79fc88c2998323b2510c2e95b95492f593ddade503840b32959e
|
|
4
|
+
data.tar.gz: 144a41267c8a5694b667d92121f7282376a6e0185abadfa263cc20cd7b944aef
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 90d663027ae3c7d84992bb2b75a45296b89a7f62373c2f33df91200320e33db77c4ac2efbff4a4bac831c5c34ba6c5d17e15ecf0102fa6bc6214ee6c32bb5f94
|
|
7
|
+
data.tar.gz: 7977909232374487763e24486a183cf008faa2f9f129899aa12b98d6492269e5ba0e9b702fddc8a790a39e5fce40ae61522c20a61015ca19226157f6f5890e19
|
data/CHANGELOG.md
ADDED
data/CODE_OF_CONDUCT.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Code of Conduct
|
|
2
|
+
|
|
3
|
+
"rails_query" follows [The Ruby Community Conduct Guideline](https://www.ruby-lang.org/en/conduct) in all "collaborative space", which is defined as community communications channels (such as mailing lists, submitted patches, commit comments, etc.):
|
|
4
|
+
|
|
5
|
+
* Participants will be tolerant of opposing views.
|
|
6
|
+
* Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks.
|
|
7
|
+
* When interpreting the words and actions of others, participants should always assume good intentions.
|
|
8
|
+
* Behaviour which can be reasonably considered harassment will not be tolerated.
|
|
9
|
+
|
|
10
|
+
If you have any concerns about behaviour within this project, please contact us at ["ilson.lasmar@gmail.com"](mailto:"ilson.lasmar@gmail.com").
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ilson Lasmar (ilson.lasmar@gmail.com)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# RailsQuery
|
|
2
|
+
|
|
3
|
+
[](http://badge.fury.io/rb/rails_query)
|
|
4
|
+
[](https://github.com/ilsonlasmar/rails_query/actions/workflows/main.yml)
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
A lightweight data-fetching and mutation layer for Ruby on Rails.
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
RailsQuery introduces a clear pattern for handling external APIs using:
|
|
11
|
+
|
|
12
|
+
- Providers (configuration + context) - **how to connect**
|
|
13
|
+
- Queries (read + cache) - **how to read**
|
|
14
|
+
- Mutations (write + invalidate) - **how to write**
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
## Why RailsQuery?
|
|
18
|
+
|
|
19
|
+
In most Rails projects, external API calls end up:
|
|
20
|
+
|
|
21
|
+
- scattered across services, models, and controllers
|
|
22
|
+
- duplicated HTTP setup (Faraday, headers, auth)
|
|
23
|
+
- hard to track and debug
|
|
24
|
+
- inconsistent caching strategies
|
|
25
|
+
- tightly coupled and difficult to test
|
|
26
|
+
|
|
27
|
+
RailsQuery solves this by centralizing and standardizing all external interactions.
|
|
28
|
+
|
|
29
|
+
architecture:
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
app/providers/
|
|
33
|
+
user_provider.rb
|
|
34
|
+
user/
|
|
35
|
+
queries/
|
|
36
|
+
find_user_query.rb
|
|
37
|
+
mutations/
|
|
38
|
+
create_user_mutation.rb
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Index
|
|
44
|
+
- [Installation](#installation)
|
|
45
|
+
- [Usage](#usage)
|
|
46
|
+
- [Provider](#provider)
|
|
47
|
+
- [Query](#query)
|
|
48
|
+
- [Mutation](#mutation)
|
|
49
|
+
- [Generators](#generators)
|
|
50
|
+
- [Caching](#caching)
|
|
51
|
+
- [Invalidation](#invalidation)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
## Installation
|
|
55
|
+
|
|
56
|
+
Add the gem to your project
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
gem "rails_query"
|
|
60
|
+
```
|
|
61
|
+
Then
|
|
62
|
+
```bash
|
|
63
|
+
bundle install
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Usage
|
|
67
|
+
|
|
68
|
+
### Install setup
|
|
69
|
+
```bash
|
|
70
|
+
rails g rails_query:install
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Provider
|
|
74
|
+
Providers define configuration and and context (HTTP client, base URL, auth)
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
class UserProvider
|
|
78
|
+
include RailsQuery::Adapter
|
|
79
|
+
|
|
80
|
+
base_url ENV["USER_API_URL"]
|
|
81
|
+
|
|
82
|
+
client do
|
|
83
|
+
Faraday.new(url: base_url) do |f|
|
|
84
|
+
f.request :json
|
|
85
|
+
f.response :json
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
query :find_user
|
|
90
|
+
mutation :create_user
|
|
91
|
+
end
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Query
|
|
95
|
+
Queries are responsible for reading data and caching results.
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
class User::Queries::FindUserQuery < RailsQuery::Query
|
|
99
|
+
ttl 5.minutes
|
|
100
|
+
|
|
101
|
+
key do |id|
|
|
102
|
+
["users", id]
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def resolve(id, opts = {})
|
|
106
|
+
client = opts.fetch(:client)
|
|
107
|
+
client.get("/users/#{id}").body
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
```
|
|
111
|
+
Basic usage:
|
|
112
|
+
```ruby
|
|
113
|
+
UserProvider.find_user(1)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Mutation
|
|
117
|
+
Mutations are responsible for writing data and invalidating cache.
|
|
118
|
+
|
|
119
|
+
```ruby
|
|
120
|
+
class User::Mutations::CreateUserMutation < RailsQuery::Mutation
|
|
121
|
+
invalidates "UserProvider"
|
|
122
|
+
|
|
123
|
+
def resolve(params, opts = {})
|
|
124
|
+
client = opts.fetch(:client)
|
|
125
|
+
client.post("/users", params).body
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Generators
|
|
131
|
+
Generate queries:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
rails g rails_query:query User list_users
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Generate mutations:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
rails g rails_query:mutation User update_user
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Caching
|
|
144
|
+
Queries are cached automatically based on:
|
|
145
|
+
|
|
146
|
+
- class name
|
|
147
|
+
- arguments
|
|
148
|
+
- keyword arguments (kwargs)
|
|
149
|
+
|
|
150
|
+
```ruby
|
|
151
|
+
UserProvider.find_user(1)
|
|
152
|
+
UserProvider.find_user(1) # cached
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
You can configure TTL:
|
|
156
|
+
|
|
157
|
+
```ruby
|
|
158
|
+
ttl 5.minutes
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Invalidation
|
|
162
|
+
|
|
163
|
+
Mutations invalidate cache after execution.
|
|
164
|
+
|
|
165
|
+
```ruby
|
|
166
|
+
invalidates "UserProvider"
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
## Development
|
|
171
|
+
|
|
172
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
173
|
+
|
|
174
|
+
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`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
175
|
+
|
|
176
|
+
## Contributing
|
|
177
|
+
|
|
178
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/ilsonlasmar/rails_query. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/ilsonlasmar/rails_query/blob/main/CODE_OF_CONDUCT.md).
|
|
179
|
+
|
|
180
|
+
## License
|
|
181
|
+
|
|
182
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
183
|
+
|
|
184
|
+
## Code of Conduct
|
|
185
|
+
|
|
186
|
+
Everyone interacting in the RailsQuery project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/ilsonlasmar/rails_query/blob/main/CODE_OF_CONDUCT.md).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
|
|
5
|
+
module RailsQuery
|
|
6
|
+
module Generators
|
|
7
|
+
# Generator that installs RailsQuery with a provider and example query.
|
|
8
|
+
class InstallGenerator < Rails::Generators::Base
|
|
9
|
+
source_root File.expand_path("templates", __dir__)
|
|
10
|
+
|
|
11
|
+
desc "Install RailsQuery with provider + example query"
|
|
12
|
+
|
|
13
|
+
def create_initializer
|
|
14
|
+
if File.exist?("config/initializers/rails_query.rb")
|
|
15
|
+
say_status("skipped", "Initializer already exists", :yellow)
|
|
16
|
+
else
|
|
17
|
+
@query_namespace = app_name.underscore
|
|
18
|
+
template "rails_query.rb.tt", "config/initializers/rails_query.rb"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def create_directories
|
|
23
|
+
empty_directory "app/providers"
|
|
24
|
+
empty_directory "app/providers/user"
|
|
25
|
+
empty_directory "app/providers/user/queries"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def create_provider
|
|
29
|
+
template "user_provider.rb.tt", "app/providers/user_provider.rb"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def create_example_query
|
|
33
|
+
template(
|
|
34
|
+
"find_user_query.rb.tt",
|
|
35
|
+
"app/providers/user/queries/find_user_query.rb"
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def add_autoload_path
|
|
40
|
+
inject_into_file "config/application.rb",
|
|
41
|
+
after: "class Application < Rails::Application\n" do
|
|
42
|
+
" config.autoload_paths << Rails.root.join('app/providers')\n"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def show_readme
|
|
47
|
+
say "\nRailsQuery installed successfully!", :green
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def app_name
|
|
53
|
+
Rails.application.class.module_parent_name
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Fake query to simulate fetching user data.
|
|
4
|
+
class User::Queries::FindUserQuery < RailsQuery::Query
|
|
5
|
+
ttl 30.seconds
|
|
6
|
+
|
|
7
|
+
key do |id|
|
|
8
|
+
[ "user", id ]
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def resolve(id)
|
|
12
|
+
{
|
|
13
|
+
id: id,
|
|
14
|
+
name: "User #{id}",
|
|
15
|
+
fetched_at: Time.current
|
|
16
|
+
}
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RailsQuery.configure do |config|
|
|
4
|
+
config.default_ttl = 30.seconds
|
|
5
|
+
config.namespace = "<%= @query_namespace %>"
|
|
6
|
+
|
|
7
|
+
# Example with Redis:
|
|
8
|
+
# config.cache_store = ActiveSupport::Cache::RedisCacheStore.new(
|
|
9
|
+
# url: ENV["REDIS_URL"]
|
|
10
|
+
# )
|
|
11
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
|
|
5
|
+
module RailsQuery
|
|
6
|
+
module Generators
|
|
7
|
+
# Generator that creates a provider and mutations based on the provided name and mutation list.
|
|
8
|
+
class MutationGenerator < Rails::Generators::NamedBase
|
|
9
|
+
source_root File.expand_path("templates", __dir__)
|
|
10
|
+
|
|
11
|
+
argument :mutations, type: :array, default: []
|
|
12
|
+
|
|
13
|
+
def create_provider_directory
|
|
14
|
+
empty_directory "app/providers"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def create_mutations_directory
|
|
18
|
+
empty_directory "app/providers/#{file_name}/mutations"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def create_provider_file
|
|
22
|
+
@provider_module = class_name
|
|
23
|
+
template "provider.rb.tt", "app/providers/#{file_name}_provider.rb"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def create_mutation_files
|
|
27
|
+
mutations.each do |mutation|
|
|
28
|
+
@mutation_name = mutation
|
|
29
|
+
@provider_module = class_name
|
|
30
|
+
|
|
31
|
+
template(
|
|
32
|
+
"mutation.rb.tt",
|
|
33
|
+
"app/providers/#{file_name}/mutations/#{mutation}_mutation.rb"
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def add_mutations_to_provider
|
|
39
|
+
provider_path = "app/providers/#{file_name}_provider.rb"
|
|
40
|
+
return unless File.exist?(provider_path)
|
|
41
|
+
|
|
42
|
+
content = File.read(provider_path)
|
|
43
|
+
|
|
44
|
+
pending_mutations = mutations.reject do |mutation|
|
|
45
|
+
content.match?(/^\s*mutation\s+:#{Regexp.escape(mutation)}\b/)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
return if pending_mutations.empty?
|
|
49
|
+
|
|
50
|
+
inject_into_file provider_path, after: "include RailsQuery::Adapter\n" do
|
|
51
|
+
pending_mutations.map { |mutation| "\n mutation :#{mutation}\n" }.join
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def show_usage
|
|
56
|
+
say "\nMutations created successfully!", :green
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class <%= @provider_module %>::Mutations::<%= @mutation_name.camelize %>Mutation < RailsQuery::Mutation
|
|
4
|
+
invalidates "<%= @provider_module %>Provider"
|
|
5
|
+
|
|
6
|
+
def resolve(*args, **opts)
|
|
7
|
+
# TODO: implement the actual API call to mutate
|
|
8
|
+
|
|
9
|
+
# client = opts.fetch(:client)
|
|
10
|
+
# response = client.post("/users")
|
|
11
|
+
# response.body
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
|
|
5
|
+
module RailsQuery
|
|
6
|
+
module Generators
|
|
7
|
+
# Generator that creates a provider and queries based on the provided name and query list.
|
|
8
|
+
class QueryGenerator < Rails::Generators::NamedBase
|
|
9
|
+
source_root File.expand_path("templates", __dir__)
|
|
10
|
+
|
|
11
|
+
argument :queries, type: :array, default: []
|
|
12
|
+
|
|
13
|
+
def create_provider_directory
|
|
14
|
+
empty_directory "app/providers"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def create_queries_directory
|
|
18
|
+
empty_directory "app/providers/#{file_name}/queries"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def create_provider_file
|
|
22
|
+
@provider_module = class_name
|
|
23
|
+
template "provider.rb.tt", "app/providers/#{file_name}_provider.rb"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def create_query_files
|
|
27
|
+
queries.each do |query|
|
|
28
|
+
@query_name = query
|
|
29
|
+
@provider_module = class_name
|
|
30
|
+
|
|
31
|
+
template(
|
|
32
|
+
"query.rb.tt",
|
|
33
|
+
"app/providers/#{file_name}/queries/#{query}_query.rb"
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def add_queries_to_provider
|
|
39
|
+
provider_path = "app/providers/#{file_name}_provider.rb"
|
|
40
|
+
return unless File.exist?(provider_path)
|
|
41
|
+
|
|
42
|
+
content = File.read(provider_path)
|
|
43
|
+
|
|
44
|
+
pending_queries = queries.reject do |query|
|
|
45
|
+
content.match?(/^\s*query\s+:#{Regexp.escape(query)}\b/)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
return if pending_queries.empty?
|
|
49
|
+
|
|
50
|
+
inject_into_file provider_path, after: "include RailsQuery::Adapter\n" do
|
|
51
|
+
pending_queries.map { |query| "\n query :#{query}\n" }.join
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def show_usage
|
|
56
|
+
say "\nQueries created successfully!", :green
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class <%= @provider_module %>::Queries::<%= @query_name.camelize %>Query < RailsQuery::Query
|
|
4
|
+
ttl 30.seconds
|
|
5
|
+
|
|
6
|
+
# TODO: implement cache key strategy
|
|
7
|
+
# key do |id|
|
|
8
|
+
# [ "<%= file_name %>", id ]
|
|
9
|
+
# end
|
|
10
|
+
|
|
11
|
+
def resolve(*args, **opts)
|
|
12
|
+
# TODO: implement the actual API call to fetch
|
|
13
|
+
|
|
14
|
+
# client = opts.fetch(:client)
|
|
15
|
+
# response = client.get("/users/#{args.first}")
|
|
16
|
+
# response.body
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsQuery
|
|
4
|
+
# Adapter module to be included in provider classes for defining queries and mutations
|
|
5
|
+
module Adapter
|
|
6
|
+
def self.included(base)
|
|
7
|
+
base.extend(ClassMethods)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Class methods for defining base URL, client, queries, and mutations
|
|
11
|
+
module ClassMethods
|
|
12
|
+
def base_url(url = nil)
|
|
13
|
+
return @base_url if url.nil?
|
|
14
|
+
|
|
15
|
+
@base_url = url
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def client(&block)
|
|
19
|
+
return @client_block if block.nil?
|
|
20
|
+
|
|
21
|
+
@client_block = block
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# rubocop:disable Metrics/MethodLength
|
|
25
|
+
def query(name, ttl: nil, &block)
|
|
26
|
+
define_method(name) do |*args, **opts|
|
|
27
|
+
opts_with_context = inject_context(**opts)
|
|
28
|
+
|
|
29
|
+
if block
|
|
30
|
+
execute_with_cache(name, ttl, args) do
|
|
31
|
+
instance_exec(*args, **opts_with_context, &block)
|
|
32
|
+
end
|
|
33
|
+
else
|
|
34
|
+
query_class = resolve_query_class(name)
|
|
35
|
+
query_class.call(*args, **opts_with_context)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
define_singleton_method(name) do |*args, **opts|
|
|
40
|
+
instance.public_send(name, *args, **opts)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def mutation(name, &block)
|
|
45
|
+
define_method(name) do |*args, **opts|
|
|
46
|
+
opts_with_context = inject_context(**opts)
|
|
47
|
+
|
|
48
|
+
if block
|
|
49
|
+
instance_exec(*args, **opts_with_context, &block)
|
|
50
|
+
else
|
|
51
|
+
mutation_class = resolve_mutation_class(name)
|
|
52
|
+
mutation_class.call(*args, **opts_with_context)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
define_singleton_method(name) do |*args, **opts|
|
|
57
|
+
instance.public_send(name, *args, **opts)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
# rubocop:enable Metrics/MethodLength
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def instance
|
|
65
|
+
RailsQuery::Adapter.instance_for(self)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def self.instance_for(klass)
|
|
70
|
+
@instances ||= {}
|
|
71
|
+
@instances[klass] ||= klass.new
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def base_url
|
|
75
|
+
self.class.base_url
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def client
|
|
79
|
+
@client ||= instance_exec(&self.class.client) if self.class.client
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def execute_with_cache(name, ttl, args, &block)
|
|
85
|
+
return yield unless ttl
|
|
86
|
+
|
|
87
|
+
key = RailsQuery::KeyBuilder.build([self.class.name, name, args])
|
|
88
|
+
|
|
89
|
+
RailsQuery.client.fetch(key, ttl: ttl, provider: self.class.name, &block)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def inject_context(**opts)
|
|
93
|
+
{
|
|
94
|
+
client: (respond_to?(:client) ? client : nil),
|
|
95
|
+
base_url: (respond_to?(:base_url) ? base_url : nil),
|
|
96
|
+
provider: self.class.name
|
|
97
|
+
}.merge(opts)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def resolve_query_class(name)
|
|
101
|
+
provider = self.class.name.sub("Provider", "")
|
|
102
|
+
class_name = "#{name.to_s.camelize}Query"
|
|
103
|
+
|
|
104
|
+
"#{provider}::Queries::#{class_name}".constantize
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def resolve_mutation_class(name)
|
|
108
|
+
provider = self.class.name.sub("Provider", "")
|
|
109
|
+
class_name = "#{name.to_s.camelize}Mutation"
|
|
110
|
+
|
|
111
|
+
"#{provider}::Mutations::#{class_name}".constantize
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsQuery
|
|
4
|
+
# Internal client class responsible for cache interactions
|
|
5
|
+
class Client
|
|
6
|
+
def initialize(config)
|
|
7
|
+
@cache = config.cache_store
|
|
8
|
+
@default_ttl = config.default_ttl
|
|
9
|
+
@namespace = config.namespace
|
|
10
|
+
@logger = config.logger
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def fetch(key, ttl: @default_ttl, provider: nil, &block)
|
|
14
|
+
store_index(provider, key) if provider
|
|
15
|
+
namespaced = namespaced_key(key)
|
|
16
|
+
@cache.fetch(namespaced, expires_in: ttl, &block)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def store_index(provider, key)
|
|
20
|
+
return unless provider
|
|
21
|
+
|
|
22
|
+
idx_key = index_key(provider)
|
|
23
|
+
|
|
24
|
+
keys = @cache.read(idx_key) || []
|
|
25
|
+
keys << key
|
|
26
|
+
|
|
27
|
+
@cache.write(idx_key, keys.uniq)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def invalidate_provider(provider)
|
|
31
|
+
index_key = index_key(provider)
|
|
32
|
+
|
|
33
|
+
keys = @cache.read(index_key) || []
|
|
34
|
+
|
|
35
|
+
keys.each do |digest|
|
|
36
|
+
@cache.delete(namespaced_key(digest))
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
@cache.delete(index_key)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def invalidate(key)
|
|
43
|
+
@cache.delete(namespaced_key(key))
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def namespaced_key(key)
|
|
49
|
+
"#{@namespace}:#{Array(key).join(":")}"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def index_key(provider)
|
|
53
|
+
"#{@namespace}:index:#{provider.to_s.underscore}"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsQuery
|
|
4
|
+
# Configuration class for RailsQuery
|
|
5
|
+
class Configuration
|
|
6
|
+
attr_accessor :cache_store, :default_ttl, :namespace, :logger
|
|
7
|
+
|
|
8
|
+
def initialize
|
|
9
|
+
@cache_store = default_cache_store
|
|
10
|
+
@default_ttl = 30.seconds
|
|
11
|
+
@namespace = "rails_query"
|
|
12
|
+
@logger = default_logger
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def default_cache_store
|
|
18
|
+
if defined?(Rails) && Rails.respond_to?(:cache)
|
|
19
|
+
Rails.cache
|
|
20
|
+
else
|
|
21
|
+
ActiveSupport::Cache::MemoryStore.new
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def default_logger
|
|
26
|
+
if defined?(Rails) && Rails.respond_to?(:logger)
|
|
27
|
+
Rails.logger
|
|
28
|
+
else
|
|
29
|
+
Logger.new($stdout)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsQuery
|
|
4
|
+
# Base class for mutations
|
|
5
|
+
class Mutation
|
|
6
|
+
class << self
|
|
7
|
+
def call(*args, **opts)
|
|
8
|
+
new.call(*args, **opts)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def invalidates(*providers)
|
|
12
|
+
@invalidates = providers
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def invalidation_targets
|
|
16
|
+
@invalidates || []
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def call(*args, **opts)
|
|
21
|
+
result = kwargs? ? resolve(*args, **opts) : resolve(*args)
|
|
22
|
+
invalidate!
|
|
23
|
+
|
|
24
|
+
result
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def kwargs?
|
|
28
|
+
method(:resolve).parameters.any? { |type, _| %i[keyrest opt].include?(type) }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def resolve(*)
|
|
32
|
+
raise NotImplementedError
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def invalidate!
|
|
38
|
+
self.class.invalidation_targets.each do |provider|
|
|
39
|
+
RailsQuery.invalidate_provider(provider)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsQuery
|
|
4
|
+
# Base class for queries
|
|
5
|
+
class Query
|
|
6
|
+
class << self
|
|
7
|
+
def call(*args, **opts)
|
|
8
|
+
new.call(*args, **opts)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def ttl(value = nil)
|
|
12
|
+
@ttl = value if value
|
|
13
|
+
@ttl
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def key(&block)
|
|
17
|
+
@key_block = block if block
|
|
18
|
+
@key_block
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def call(*args, **opts)
|
|
23
|
+
key = build_key(*args, **opts)
|
|
24
|
+
provider_class = opts[:provider] || self.class.name
|
|
25
|
+
RailsQuery.client.fetch(key, ttl: ttl, provider: provider_class) do
|
|
26
|
+
kwargs? ? resolve(*args, **opts) : resolve(*args)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def ttl
|
|
31
|
+
self.class.ttl || RailsQuery.configuration.default_ttl
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def build_key(*args, **opts)
|
|
35
|
+
opts = opts.select { |k, _| self.class.key.parameters.map(&:last).include?(k) }
|
|
36
|
+
return instance_exec(*args, **opts, &self.class.key) if self.class.key
|
|
37
|
+
|
|
38
|
+
[self.class.name, args]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def kwargs?
|
|
42
|
+
method(:resolve).parameters.any? { |type, _| %i[keyrest opt].include?(type) }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def resolve(*)
|
|
46
|
+
raise NotImplementedError
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsQuery
|
|
4
|
+
# Railtie to integrate with Rails applications
|
|
5
|
+
class Railtie < Rails::Railtie
|
|
6
|
+
initializer "rails_query.setup" do
|
|
7
|
+
RailsQuery.configure do |config|
|
|
8
|
+
config.cache_store = Rails.cache if defined?(Rails.cache)
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
data/lib/rails_query.rb
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support"
|
|
4
|
+
require "active_support/cache"
|
|
5
|
+
require "active_support/core_ext/numeric/time"
|
|
6
|
+
|
|
7
|
+
require_relative "rails_query/version"
|
|
8
|
+
require_relative "rails_query/configuration"
|
|
9
|
+
require_relative "rails_query/client"
|
|
10
|
+
require_relative "rails_query/query"
|
|
11
|
+
require_relative "rails_query/mutation"
|
|
12
|
+
require_relative "rails_query/adapter"
|
|
13
|
+
|
|
14
|
+
# Main module for RailsQuery
|
|
15
|
+
module RailsQuery
|
|
16
|
+
class << self
|
|
17
|
+
def configuration
|
|
18
|
+
@configuration ||= Configuration.new
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def configure
|
|
22
|
+
yield(configuration) if block_given?
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def reset_configuration!
|
|
26
|
+
@_configuration = nil
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def client
|
|
30
|
+
@client ||= Client.new(configuration)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def invalidate_provider(provider)
|
|
34
|
+
client.invalidate_provider(provider)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
require_relative "rails_query/railtie" if defined?(Rails)
|
metadata
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: rails_query
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Ilson Lasmar
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: minitest
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '5.0'
|
|
19
|
+
type: :development
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '5.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: mocha
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '3.1'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '3.1'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: pry
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0.14'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0.14'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: rails
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '7.1'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '7.1'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: simplecov
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '0.22'
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '0.22'
|
|
82
|
+
description: |
|
|
83
|
+
RailsQuery provides a declarative abstraction for fetching, caching, and
|
|
84
|
+
managing remote or expensive data in Rails applications.
|
|
85
|
+
|
|
86
|
+
Inspired by modern data-fetching patterns, it introduces query lifecycle
|
|
87
|
+
concepts such as caching, stale-while-revalidate, automatic invalidation,
|
|
88
|
+
and request deduplication, while remaining idiomatic to the Rails ecosystem.
|
|
89
|
+
email:
|
|
90
|
+
- ilson.lasmar@gmail.com
|
|
91
|
+
executables: []
|
|
92
|
+
extensions: []
|
|
93
|
+
extra_rdoc_files: []
|
|
94
|
+
files:
|
|
95
|
+
- CHANGELOG.md
|
|
96
|
+
- CODE_OF_CONDUCT.md
|
|
97
|
+
- LICENSE.txt
|
|
98
|
+
- README.md
|
|
99
|
+
- Rakefile
|
|
100
|
+
- lib/generators/rails_query/install/install_generator.rb
|
|
101
|
+
- lib/generators/rails_query/install/templates/find_user_query.rb.tt
|
|
102
|
+
- lib/generators/rails_query/install/templates/rails_query.rb.tt
|
|
103
|
+
- lib/generators/rails_query/install/templates/user_provider.rb.tt
|
|
104
|
+
- lib/generators/rails_query/mutation/mutation_generator.rb
|
|
105
|
+
- lib/generators/rails_query/mutation/templates/mutation.rb.tt
|
|
106
|
+
- lib/generators/rails_query/mutation/templates/provider.rb.tt
|
|
107
|
+
- lib/generators/rails_query/query/query_generator.rb
|
|
108
|
+
- lib/generators/rails_query/query/templates/provider.rb.tt
|
|
109
|
+
- lib/generators/rails_query/query/templates/query.rb.tt
|
|
110
|
+
- lib/rails_query.rb
|
|
111
|
+
- lib/rails_query/adapter.rb
|
|
112
|
+
- lib/rails_query/client.rb
|
|
113
|
+
- lib/rails_query/configuration.rb
|
|
114
|
+
- lib/rails_query/mutation.rb
|
|
115
|
+
- lib/rails_query/query.rb
|
|
116
|
+
- lib/rails_query/railtie.rb
|
|
117
|
+
- lib/rails_query/version.rb
|
|
118
|
+
homepage: https://github.com/ilsonlasmar/rails_query
|
|
119
|
+
licenses:
|
|
120
|
+
- MIT
|
|
121
|
+
metadata:
|
|
122
|
+
allowed_push_host: https://rubygems.org
|
|
123
|
+
homepage_uri: https://github.com/ilsonlasmar/rails_query
|
|
124
|
+
source_code_uri: https://github.com/ilsonlasmar/rails_query
|
|
125
|
+
changelog_uri: https://github.com/ilsonlasmar/rails_query/blob/main/CHANGELOG.md
|
|
126
|
+
rdoc_options: []
|
|
127
|
+
require_paths:
|
|
128
|
+
- lib
|
|
129
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
130
|
+
requirements:
|
|
131
|
+
- - ">="
|
|
132
|
+
- !ruby/object:Gem::Version
|
|
133
|
+
version: 3.2.0
|
|
134
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
135
|
+
requirements:
|
|
136
|
+
- - ">="
|
|
137
|
+
- !ruby/object:Gem::Version
|
|
138
|
+
version: '0'
|
|
139
|
+
requirements: []
|
|
140
|
+
rubygems_version: 4.0.6
|
|
141
|
+
specification_version: 4
|
|
142
|
+
summary: Declarative query caching layer with lifecycle management for Rails
|
|
143
|
+
test_files: []
|