yes-read-api 1.0.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/MIT-LICENSE +20 -0
- data/README.md +159 -0
- data/Rakefile +18 -0
- data/app/controllers/yes/read/api/application_controller.rb +23 -0
- data/app/controllers/yes/read/api/queries_controller.rb +133 -0
- data/config/initializers/api_pagination.rb +34 -0
- data/config/routes.rb +80 -0
- data/lib/tasks/yousty/read/api_tasks.rake +6 -0
- data/lib/yes/read/api/advanced_filter_validator.rb +39 -0
- data/lib/yes/read/api/api_pagination_patch.rb +32 -0
- data/lib/yes/read/api/engine.rb +17 -0
- data/lib/yes/read/api/model_constraints.rb +16 -0
- data/lib/yes/read/api/version.rb +9 -0
- data/lib/yes/read/api.rb +40 -0
- metadata +128 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 5d731425b16d1120ca5bfd9d17e660f4b4c458e72e453379083747ebc63a4bf6
|
|
4
|
+
data.tar.gz: bec6d4e104ec6a00c9ca6cf1b88da37495883d0fa5d2062067f1793ce0bfd7e9
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 34dd0f0016acaba53fcb2ac80e00fa6f68b116d9a930eeb2ec5ddbe3f23966b3569605379ed21140f47745cb0a513bc9bc4f7530c33ac3e4c20aae944a804c4c
|
|
7
|
+
data.tar.gz: 326cf251bdb5634a9f5bded5b6441abf14ca0054c19a543b97164eaad246481979833670c75eff0a4fa3259eeac52d1070a32f3404567fc42e89eb8e11e56e4b
|
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright 2023 Arek Swidrak
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# Yes Read API
|
|
2
|
+
|
|
3
|
+
This gem implements an endpoint to query read models.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add a gem to your Gemfile
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem 'yes-read-api'
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
and run `bundle install`
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
See the [root README](../README.md) for the full DSL documentation and aggregate definition.
|
|
18
|
+
|
|
19
|
+
There are a few steps you need to do in order to integrate this gem. In the examples below we assume you have an `Apprenticeship` read model class. The module structure is strict.
|
|
20
|
+
|
|
21
|
+
- Mount gem's endpoint to use it. Example(in `routes.rb`):
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
Rails.application.routes.draw do
|
|
25
|
+
mount Yes::Read::Api::Engine => '/queries'
|
|
26
|
+
end
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
- Define a set of registered models. You can do it as follows (in `application.rb`)
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
config.yes_read_api.read_models = ['apprenticeships']
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
- Define an authorizer for your read model's request:
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
module ReadModels
|
|
39
|
+
module Apprenticeship
|
|
40
|
+
class RequestAuthorizer < Yes::Core::ReadModel::RequestAuthorizer
|
|
41
|
+
def self.call(params, auth_data)
|
|
42
|
+
auth_data['scopes'].include?('admin')
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
- Define a filter class for your read model. You can declare various ActiveRecord filters which can be applied to your collection based on request params. Assuming you have a `by_id` scope in your `Apprenticeship` model:
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
module ReadModels
|
|
53
|
+
module Apprenticeship
|
|
54
|
+
class Filter < Yes::Core::ReadModel::Filter
|
|
55
|
+
has_scope :ids do |controller, scope, value|
|
|
56
|
+
scope.by_id(value.split(','))
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def read_model_class
|
|
62
|
+
::Apprenticeship
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
- _Optional._ Define an authorizer for your read model. You can inherit from `Yes::Core::ReadModelAuthorizer` or define your own base class:
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
module ReadModels
|
|
73
|
+
module Apprenticeship
|
|
74
|
+
class Authorizer < ReadModels::Authorizer
|
|
75
|
+
class << self
|
|
76
|
+
def call(record, auth_data)
|
|
77
|
+
raise ReadModels::Authorizer::NotAuthorized, 'You need to be a company admin' unless company_admin?(auth_data)
|
|
78
|
+
|
|
79
|
+
true
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Now you can query your read model via `GET /queries/apprenticeships` request.
|
|
88
|
+
|
|
89
|
+
### Pagination
|
|
90
|
+
Read responses are always paginated. You can supply pagination parameter in case you want to change the default
|
|
91
|
+
|
|
92
|
+
| Name | Default |
|
|
93
|
+
|---------------------|---------|
|
|
94
|
+
| page[number] | 1 |
|
|
95
|
+
| page[size] | 20 |
|
|
96
|
+
| page[include_total] | false |
|
|
97
|
+
|
|
98
|
+
The read response includes pagination information in the following headers
|
|
99
|
+
|
|
100
|
+
| Name | Description |
|
|
101
|
+
|------------|------------------------------------------------------------------------------------------------------------------|
|
|
102
|
+
| X-Page | page number |
|
|
103
|
+
| X-Per-Page | page size |
|
|
104
|
+
| X-Total | Total number of items. By default not included(null) If you need a total number set `page[include_total]` to `true` |
|
|
105
|
+
|
|
106
|
+
By default pagination is using `countless_minimal` mode.
|
|
107
|
+
The `X-Total` header is not returned in the response. Count query is not produced to DB.
|
|
108
|
+
|
|
109
|
+
You can change this behavior per each request by adding `page[include_total]=true` params to the request query.
|
|
110
|
+
|
|
111
|
+
If you wish to change the default behavior globally, so `X-Total` header is returned for the every request response by default, you can set globally `Pagy::DEFAULT[:countless_minimal] = false` or example in the `pagy_initializer.rb`. In this case `page[include_total]` param is ignored.
|
|
112
|
+
|
|
113
|
+
More you can read here: [Pagy Countless](https://ddnexus.github.io/pagy/docs/extras/countless/)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
## Development
|
|
118
|
+
|
|
119
|
+
### Prerequisites
|
|
120
|
+
|
|
121
|
+
- Docker and Docker Compose
|
|
122
|
+
- Ruby >= 3.2.0
|
|
123
|
+
- Bundler
|
|
124
|
+
|
|
125
|
+
### Setup
|
|
126
|
+
|
|
127
|
+
Start PostgreSQL from the **repository root**:
|
|
128
|
+
|
|
129
|
+
```shell
|
|
130
|
+
docker compose up -d
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Install dependencies:
|
|
134
|
+
|
|
135
|
+
```shell
|
|
136
|
+
bundle install
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Set up the test database:
|
|
140
|
+
|
|
141
|
+
```shell
|
|
142
|
+
RAILS_ENV=test bundle exec rake db:create db:migrate
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The `.env` file at `spec/dummy/.env` is loaded automatically and contains JWT test keys and database configuration.
|
|
146
|
+
|
|
147
|
+
### Running Specs
|
|
148
|
+
|
|
149
|
+
```shell
|
|
150
|
+
bundle exec rspec
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Contributing
|
|
154
|
+
|
|
155
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/yousty/yes.
|
|
156
|
+
|
|
157
|
+
## License
|
|
158
|
+
|
|
159
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'bundler/setup'
|
|
4
|
+
require 'dotenv'
|
|
5
|
+
|
|
6
|
+
Dotenv.load('spec/.env')
|
|
7
|
+
|
|
8
|
+
APP_RAKEFILE = File.expand_path('spec/dummy/Rakefile', __dir__)
|
|
9
|
+
load 'rails/tasks/engine.rake'
|
|
10
|
+
|
|
11
|
+
load 'rails/tasks/statistics.rake'
|
|
12
|
+
|
|
13
|
+
require 'bundler/gem_tasks'
|
|
14
|
+
require 'rspec/core/rake_task'
|
|
15
|
+
|
|
16
|
+
RSpec::Core::RakeTask.new(:spec)
|
|
17
|
+
|
|
18
|
+
task default: :spec
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yes
|
|
4
|
+
module Read
|
|
5
|
+
module Api
|
|
6
|
+
class ApplicationController < ActionController::API
|
|
7
|
+
before_action :set_locale
|
|
8
|
+
|
|
9
|
+
def set_locale
|
|
10
|
+
I18n.locale = params[:locale] || I18n.default_locale
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def append_info_to_payload(payload)
|
|
14
|
+
super
|
|
15
|
+
payload[:request_id] = request.uuid
|
|
16
|
+
payload[:request_headers] = request.env.select do |k, _v|
|
|
17
|
+
k.match(/\A(HTTP.*|CONTENT.*|REMOTE.*|REQUEST.*|AUTHORIZATION.*|SCRIPT.*|SERVER.*)/)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'yes/read/api/advanced_filter_validator'
|
|
4
|
+
|
|
5
|
+
module Yes
|
|
6
|
+
module Read
|
|
7
|
+
module Api
|
|
8
|
+
class QueriesController < ApplicationController
|
|
9
|
+
before_action :authenticate_with_token
|
|
10
|
+
before_action :validate_advanced_payload, only: :advanced
|
|
11
|
+
before_action :process_own_filter, only: :call
|
|
12
|
+
|
|
13
|
+
rescue_from(Yes::Core::AuthenticationError, with: :auth_error_response)
|
|
14
|
+
|
|
15
|
+
rescue_from(
|
|
16
|
+
Yes::Core::Authorization::ReadModelsAuthorizer::NotAuthorized,
|
|
17
|
+
Yes::Core::Authorization::ReadRequestAuthorizer::NotAuthorized,
|
|
18
|
+
with: :read_models_unauthorized_response
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
def call
|
|
22
|
+
persisted_filter = filter(read_model_name).persisted_filter_scope.find_by(id: params[:filter_id]) if params[:filter_id].present?
|
|
23
|
+
|
|
24
|
+
render json: response_json(persisted_filter:, filter_type: persisted_filter ? :advanced : :basic).to_json
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def advanced
|
|
28
|
+
render json: response_json(filter_type: :advanced).to_json
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def process_own_filter
|
|
34
|
+
return if params.dig(:filters, :own).blank?
|
|
35
|
+
return unless defined?(::IdentityUser)
|
|
36
|
+
|
|
37
|
+
identity_user = ::IdentityUser.find_by(id: auth_data[:identity_id])
|
|
38
|
+
return if identity_user.blank?
|
|
39
|
+
return unless identity_user.respond_to?("own_#{read_model_name.singularize}_ids")
|
|
40
|
+
|
|
41
|
+
owned_ids = identity_user.send("own_#{read_model_name.singularize}_ids")
|
|
42
|
+
params[:filters][:ids] = owned_ids.presence&.join(',') || 'none'
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def response_json(filter_type: :basic, persisted_filter: nil)
|
|
46
|
+
filter_options = persisted_filter&.body&.deep_symbolize_keys&.merge(model: params[:model]) || params
|
|
47
|
+
|
|
48
|
+
request_authorizer.call(filter_options, auth_data)
|
|
49
|
+
|
|
50
|
+
filter_options[:filters] ||= {}
|
|
51
|
+
records = filter(read_model_name).new(filter_options, type: filter_type).call
|
|
52
|
+
paginated_records = paginate(records, filter_options[:page] || {})
|
|
53
|
+
|
|
54
|
+
Yes::Core::Authorization::ReadModelsAuthorizer.call(read_model_name, paginated_records, auth_data)
|
|
55
|
+
|
|
56
|
+
serialize(paginated_records, filter_options)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def request_authorizer
|
|
60
|
+
authorizer_class = "ReadModels::#{read_model_name.classify}::RequestAuthorizer"
|
|
61
|
+
Kernel.const_get(authorizer_class)
|
|
62
|
+
rescue NameError
|
|
63
|
+
raise Yes::Core::Authorization::ReadRequestAuthorizer::NotAuthorized, 'Not allowed'
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def read_model_name
|
|
67
|
+
params[:model].underscore
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def filter(read_model_name)
|
|
71
|
+
filter_class = "ReadModels::#{read_model_name.classify}::Filter"
|
|
72
|
+
Kernel.const_get(filter_class)
|
|
73
|
+
rescue NameError
|
|
74
|
+
Yes::Core::ReadModel::Filter
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def serialize(records, filter_options)
|
|
78
|
+
options = {
|
|
79
|
+
params: filter_options,
|
|
80
|
+
include: filter_options[:include]&.split(',')&.map(&:to_sym),
|
|
81
|
+
auth_data:
|
|
82
|
+
}.compact
|
|
83
|
+
|
|
84
|
+
serializer.new(records, options)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def serializer
|
|
88
|
+
serializer_class = "ReadModels::#{read_model_name.classify}::Serializers::#{read_model_name.classify}"
|
|
89
|
+
Kernel.const_get(serializer_class)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def params
|
|
93
|
+
@params ||= request.parameters.deep_symbolize_keys
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def authenticate_with_token
|
|
97
|
+
adapter = Yes::Core.configuration.auth_adapter
|
|
98
|
+
raise Yes::Core::AuthenticationError, 'No auth adapter configured' if adapter.nil?
|
|
99
|
+
|
|
100
|
+
@auth_data = adapter.authenticate(request)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
attr_reader :auth_data
|
|
104
|
+
|
|
105
|
+
def auth_error_response(error)
|
|
106
|
+
render(
|
|
107
|
+
json: { title: 'Auth Token Invalid', detail: error.message }.to_json,
|
|
108
|
+
status: :unauthorized
|
|
109
|
+
)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def read_models_unauthorized_response(error)
|
|
113
|
+
render(
|
|
114
|
+
json: { title: 'Unauthorized', detail: error.message }.to_json,
|
|
115
|
+
status: :unauthorized
|
|
116
|
+
)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def validate_advanced_payload
|
|
120
|
+
return if params[:filter_definition].blank?
|
|
121
|
+
|
|
122
|
+
validation_result = Yes::Read::Api::AdvancedFilterValidator.call(params)
|
|
123
|
+
return if validation_result.success?
|
|
124
|
+
|
|
125
|
+
render(
|
|
126
|
+
json: { title: 'Invalid Payload', detail: validation_result.errors.to_h }.to_json,
|
|
127
|
+
status: :unprocessable_content
|
|
128
|
+
)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
ApiPagination.configure do |config|
|
|
4
|
+
# If you have more than one gem included, you can choose a paginator.
|
|
5
|
+
config.paginator = :pagy # or :will_paginate
|
|
6
|
+
|
|
7
|
+
# By default, this is set to 'Total'
|
|
8
|
+
config.total_header = 'X-Total'
|
|
9
|
+
|
|
10
|
+
# By default, this is set to 'Per-Page'
|
|
11
|
+
config.per_page_header = 'X-Per-Page'
|
|
12
|
+
|
|
13
|
+
# Optional: set this to add a header with the current page number.
|
|
14
|
+
config.page_header = 'X-Page'
|
|
15
|
+
|
|
16
|
+
# Optional: set this to add other response format. Useful with tools that define :jsonapi format
|
|
17
|
+
config.response_formats = %i[json xml jsonapi]
|
|
18
|
+
|
|
19
|
+
config.page_param do |params|
|
|
20
|
+
params.dig(:page, :number)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
config.per_page_param do |params|
|
|
24
|
+
params.dig(:page, :size)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Optional: Include the total and last_page link header
|
|
28
|
+
# By default, this is set to true
|
|
29
|
+
# Note: When using kaminari, this prevents the count call to the database
|
|
30
|
+
config.include_total = true
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
Pagy::DEFAULT[:max_per_page] = 100
|
|
34
|
+
Pagy::DEFAULT[:countless_minimal] = true
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Instantiates an ActionDispatch::Request from the env.
|
|
4
|
+
# Extracts controller and action.
|
|
5
|
+
# Decides whether to:
|
|
6
|
+
# Just call the controller if no tracer is configured.
|
|
7
|
+
# Otherwise, start or enrich an OpenTelemetry span with authentication and request data.
|
|
8
|
+
# Returns the controller’s response (controller.action(action).call(env)).
|
|
9
|
+
# It’s being used as a route handler directly in Rails routes:
|
|
10
|
+
# get '/:model', to: OtlTrackableRequest.new
|
|
11
|
+
# post '/:model', to: OtlTrackableRequest.new
|
|
12
|
+
#
|
|
13
|
+
|
|
14
|
+
require 'yes/core'
|
|
15
|
+
|
|
16
|
+
module Yes
|
|
17
|
+
module Read
|
|
18
|
+
module Api
|
|
19
|
+
class OtlTrackableRequest
|
|
20
|
+
attr_accessor :action_name, :controller_class
|
|
21
|
+
|
|
22
|
+
def initialize(action_name:, controller_class: Yes::Read::Api::QueriesController)
|
|
23
|
+
@action_name = action_name
|
|
24
|
+
@controller_class = controller_class
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def call(env)
|
|
28
|
+
tracer = Yes::Core.configuration.otl_tracer
|
|
29
|
+
request = ActionDispatch::Request.new(env)
|
|
30
|
+
|
|
31
|
+
self.controller_class ||= request.controller_class
|
|
32
|
+
self.action_name ||= request.params[:action] || :index
|
|
33
|
+
|
|
34
|
+
return controller_class.action(action_name).call(env) if tracer.nil?
|
|
35
|
+
|
|
36
|
+
otl_request_data = request.get? || request.delete? ? get_otl_auth_data(request, env) : otl_auth_data(request, env)
|
|
37
|
+
|
|
38
|
+
tracer.in_span("Request #{controller_class.name}", kind: :client) do |request_span|
|
|
39
|
+
request_span.add_attributes(otl_request_data)
|
|
40
|
+
|
|
41
|
+
return controller_class.action(action_name).call(env)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def otl_auth_data(request, env)
|
|
48
|
+
request.body.rewind
|
|
49
|
+
params = request.body.read
|
|
50
|
+
request.body.rewind # restore the cursor to the beginning able read again body
|
|
51
|
+
|
|
52
|
+
auth_token = env['HTTP_AUTHORIZATION'] || ''
|
|
53
|
+
auth_data = auth_token.present? ? JWT.decode(auth_token.gsub('Bearer ', ''), nil, false) : {}
|
|
54
|
+
{
|
|
55
|
+
auth_token:,
|
|
56
|
+
auth_data: auth_data.to_json,
|
|
57
|
+
params:
|
|
58
|
+
}.stringify_keys
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def get_otl_auth_data(request, env)
|
|
62
|
+
auth_token = env['HTTP_AUTHORIZATION'] || ''
|
|
63
|
+
auth_data = auth_token.present? ? JWT.decode(auth_token.gsub('Bearer ', ''), nil, false) : {}
|
|
64
|
+
{
|
|
65
|
+
auth_token:,
|
|
66
|
+
auth_data: auth_data.to_json,
|
|
67
|
+
params: request.params.to_json
|
|
68
|
+
}.stringify_keys
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
Yes::Read::Api::Engine.routes.draw do
|
|
76
|
+
constraints(Yes::Read::Api::ModelConstraints) do
|
|
77
|
+
get '/:model', to: Yes::Read::Api::OtlTrackableRequest.new(action_name: :call)
|
|
78
|
+
post '/:model', to: Yes::Read::Api::OtlTrackableRequest.new(action_name: :advanced)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'dry-schema'
|
|
4
|
+
|
|
5
|
+
module Yes
|
|
6
|
+
module Read
|
|
7
|
+
module Api
|
|
8
|
+
class AdvancedFilterValidator
|
|
9
|
+
PaginationSchema = Dry::Schema.Params do
|
|
10
|
+
required(:size).value(:integer)
|
|
11
|
+
required(:number).value(:integer)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
AdvancedEndpointPayloadSchema = Dry::Schema.Params do
|
|
15
|
+
required(:filter_definition).hash(Yes::Core::ReadModel::FilterQueryBuilder::FilterSetSchema)
|
|
16
|
+
optional(:page).hash(PaginationSchema)
|
|
17
|
+
optional(:order).value(:hash)
|
|
18
|
+
optional(:include).value(:string)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class << self
|
|
22
|
+
def call(payload)
|
|
23
|
+
new(payload).call
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
attr_reader :payload
|
|
28
|
+
|
|
29
|
+
def initialize(payload)
|
|
30
|
+
@payload = payload
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def call
|
|
34
|
+
AdvancedEndpointPayloadSchema.call(payload)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'pagy/extras/countless'
|
|
4
|
+
require 'api-pagination'
|
|
5
|
+
|
|
6
|
+
module Yes
|
|
7
|
+
module Read
|
|
8
|
+
module Api
|
|
9
|
+
module ApiPaginationPatch
|
|
10
|
+
include Pagy::CountlessExtra
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def pagy_from(collection, options)
|
|
15
|
+
return countless_pagy(collection, options) if Pagy::DEFAULT[:countless_minimal] && options[:include_total] != 'true'
|
|
16
|
+
|
|
17
|
+
super
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def countless_pagy(collection, options)
|
|
21
|
+
options[:countless_minimal] = true
|
|
22
|
+
options[:items] = options[:per_page]
|
|
23
|
+
|
|
24
|
+
pagy, = pagy_countless(collection, options)
|
|
25
|
+
pagy
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
ApiPagination.singleton_class.prepend(Yes::Read::Api::ApiPaginationPatch)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yes
|
|
4
|
+
module Read
|
|
5
|
+
module Api
|
|
6
|
+
class Engine < ::Rails::Engine
|
|
7
|
+
isolate_namespace Yes::Read::Api
|
|
8
|
+
config.generators.api_only = true
|
|
9
|
+
|
|
10
|
+
config.generators do |g|
|
|
11
|
+
g.test_framework :rspec
|
|
12
|
+
end
|
|
13
|
+
config.yes_read_api = Yes::Read::Api.config
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yes
|
|
4
|
+
module Read
|
|
5
|
+
module Api
|
|
6
|
+
class ModelConstraints
|
|
7
|
+
class << self
|
|
8
|
+
# @param request [ActionDispatch::Request]
|
|
9
|
+
def matches?(request)
|
|
10
|
+
Rails.application.config.yes_read_api.read_models.include?(request.params['model'])
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
data/lib/yes/read/api.rb
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'yes/core'
|
|
4
|
+
require 'zeitwerk'
|
|
5
|
+
|
|
6
|
+
module Yes
|
|
7
|
+
module Read
|
|
8
|
+
module Api
|
|
9
|
+
class Error < StandardError; end
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
def loader
|
|
13
|
+
@loader ||= begin
|
|
14
|
+
loader = Zeitwerk::Loader.new
|
|
15
|
+
loader.tag = 'yes-read-api'
|
|
16
|
+
loader.push_dir(File.expand_path('../..', __dir__))
|
|
17
|
+
loader.ignore("#{File.expand_path('..', __dir__)}/read/api/version.rb")
|
|
18
|
+
loader.setup
|
|
19
|
+
loader
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @return [ActiveSupport::OrderedOptions]
|
|
24
|
+
def config
|
|
25
|
+
@config ||= ActiveSupport::OrderedOptions.new.tap do |opts|
|
|
26
|
+
opts.read_models = [] # E.g. ['apprenticeships', 'companies', 'professions']
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def configure
|
|
31
|
+
yield config
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
require 'yes/read/api/version'
|
|
39
|
+
|
|
40
|
+
Yes::Read::Api.loader.eager_load
|
metadata
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: yes-read-api
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Nico Ritsche
|
|
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: api-pagination
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '5.0'
|
|
19
|
+
type: :runtime
|
|
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: pagy
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '6.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '6.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rails
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '7.1'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '7.1'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: yes-core
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: zeitwerk
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '2.6'
|
|
75
|
+
type: :runtime
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '2.6'
|
|
82
|
+
description: Read API for the Yes event sourcing framework
|
|
83
|
+
email:
|
|
84
|
+
- nico.ritsche@yousty.ch
|
|
85
|
+
executables: []
|
|
86
|
+
extensions: []
|
|
87
|
+
extra_rdoc_files: []
|
|
88
|
+
files:
|
|
89
|
+
- MIT-LICENSE
|
|
90
|
+
- README.md
|
|
91
|
+
- Rakefile
|
|
92
|
+
- app/controllers/yes/read/api/application_controller.rb
|
|
93
|
+
- app/controllers/yes/read/api/queries_controller.rb
|
|
94
|
+
- config/initializers/api_pagination.rb
|
|
95
|
+
- config/routes.rb
|
|
96
|
+
- lib/tasks/yousty/read/api_tasks.rake
|
|
97
|
+
- lib/yes/read/api.rb
|
|
98
|
+
- lib/yes/read/api/advanced_filter_validator.rb
|
|
99
|
+
- lib/yes/read/api/api_pagination_patch.rb
|
|
100
|
+
- lib/yes/read/api/engine.rb
|
|
101
|
+
- lib/yes/read/api/model_constraints.rb
|
|
102
|
+
- lib/yes/read/api/version.rb
|
|
103
|
+
homepage: https://github.com/yousty/yes
|
|
104
|
+
licenses:
|
|
105
|
+
- MIT
|
|
106
|
+
metadata:
|
|
107
|
+
homepage_uri: https://github.com/yousty/yes
|
|
108
|
+
source_code_uri: https://github.com/yousty/yes/tree/main/yes-read-api
|
|
109
|
+
changelog_uri: https://github.com/yousty/yes/blob/main/yes-read-api/CHANGELOG.md
|
|
110
|
+
rubygems_mfa_required: 'true'
|
|
111
|
+
rdoc_options: []
|
|
112
|
+
require_paths:
|
|
113
|
+
- lib
|
|
114
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
115
|
+
requirements:
|
|
116
|
+
- - ">="
|
|
117
|
+
- !ruby/object:Gem::Version
|
|
118
|
+
version: 3.2.0
|
|
119
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
120
|
+
requirements:
|
|
121
|
+
- - ">="
|
|
122
|
+
- !ruby/object:Gem::Version
|
|
123
|
+
version: '0'
|
|
124
|
+
requirements: []
|
|
125
|
+
rubygems_version: 3.6.9
|
|
126
|
+
specification_version: 4
|
|
127
|
+
summary: Read API for the Yes event sourcing framework
|
|
128
|
+
test_files: []
|