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 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,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ # desc "Explaining what the task does"
4
+ # task :yes_read_api do
5
+ # # Task goes here
6
+ # 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
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yes
4
+ module Read
5
+ module Api
6
+ VERSION = '1.0.0'
7
+ end
8
+ end
9
+ end
@@ -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: []