jsonapionify 0.9.0 → 0.9.1
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 +13 -5
- data/.rubocop.yml +1 -0
- data/.ruby-version +1 -1
- data/.travis.yml +8 -0
- data/README.md +85 -3
- data/Rakefile +14 -0
- data/jsonapionify.gemspec +3 -0
- data/lib/jsonapionify/api/action.rb +84 -121
- data/lib/jsonapionify/api/attribute.rb +97 -20
- data/lib/jsonapionify/api/base/class_methods.rb +5 -4
- data/lib/jsonapionify/api/base/delegation.rb +20 -4
- data/lib/jsonapionify/api/base/doc_helper.rb +3 -3
- data/lib/jsonapionify/api/base/reloader.rb +1 -1
- data/lib/jsonapionify/api/base/resource_definitions.rb +28 -15
- data/lib/jsonapionify/api/base.rb +6 -0
- data/lib/jsonapionify/api/context.rb +18 -5
- data/lib/jsonapionify/api/context_delegate.rb +24 -7
- data/lib/jsonapionify/api/errors.rb +2 -0
- data/lib/jsonapionify/api/errors_object.rb +6 -5
- data/lib/jsonapionify/api/relationship/blocks.rb +1 -1
- data/lib/jsonapionify/api/relationship/many.rb +35 -11
- data/lib/jsonapionify/api/relationship/one.rb +17 -7
- data/lib/jsonapionify/api/relationship.rb +20 -6
- data/lib/jsonapionify/api/resource/builders.rb +81 -30
- data/lib/jsonapionify/api/resource/caching.rb +28 -0
- data/lib/jsonapionify/api/resource/caller.rb +61 -0
- data/lib/jsonapionify/api/resource/class_methods.rb +6 -2
- data/lib/jsonapionify/api/resource/defaults/actions.rb +47 -0
- data/lib/jsonapionify/api/resource/defaults/errors.rb +61 -15
- data/lib/jsonapionify/api/resource/defaults/hooks.rb +68 -0
- data/lib/jsonapionify/api/resource/defaults/options.rb +16 -28
- data/lib/jsonapionify/api/resource/defaults/params.rb +3 -0
- data/lib/jsonapionify/api/resource/defaults/request_contexts.rb +80 -32
- data/lib/jsonapionify/api/resource/defaults/response_contexts.rb +13 -6
- data/lib/jsonapionify/api/resource/defaults.rb +1 -1
- data/lib/jsonapionify/api/resource/definitions/actions.rb +81 -55
- data/lib/jsonapionify/api/resource/definitions/attributes.rb +46 -10
- data/lib/jsonapionify/api/resource/definitions/contexts.rb +6 -2
- data/lib/jsonapionify/api/resource/definitions/helpers.rb +1 -1
- data/lib/jsonapionify/api/resource/definitions/pagination.rb +47 -56
- data/lib/jsonapionify/api/resource/definitions/params.rb +11 -15
- data/lib/jsonapionify/api/resource/definitions/relationships.rb +43 -7
- data/lib/jsonapionify/api/resource/definitions/request_headers.rb +6 -3
- data/lib/jsonapionify/api/resource/definitions/response_headers.rb +1 -1
- data/lib/jsonapionify/api/resource/definitions/scopes.rb +5 -5
- data/lib/jsonapionify/api/resource/definitions/sorting.rb +12 -11
- data/lib/jsonapionify/api/resource/definitions.rb +1 -1
- data/lib/jsonapionify/api/resource/error_handling.rb +92 -20
- data/lib/jsonapionify/api/resource/exec.rb +11 -0
- data/lib/jsonapionify/api/resource/includer.rb +89 -1
- data/lib/jsonapionify/api/resource.rb +55 -8
- data/lib/jsonapionify/api/response.rb +43 -14
- data/lib/jsonapionify/api/server/media_type.rb +36 -0
- data/lib/jsonapionify/api/server/request.rb +25 -11
- data/lib/jsonapionify/api/server.rb +8 -4
- data/lib/jsonapionify/api/sort_field.rb +18 -0
- data/lib/jsonapionify/api/sort_field_set.rb +1 -1
- data/lib/jsonapionify/api/test_helper.rb +46 -0
- data/lib/jsonapionify/documentation/template.erb +2 -2
- data/lib/jsonapionify/documentation.rb +10 -0
- data/lib/jsonapionify/structure/collections/base.rb +10 -3
- data/lib/jsonapionify/structure/helpers/object_defaults.rb +5 -10
- data/lib/jsonapionify/structure/maps/relationships.rb +4 -0
- data/lib/jsonapionify/structure/objects/attributes.rb +4 -0
- data/lib/jsonapionify/structure/objects/base.rb +22 -9
- data/lib/jsonapionify/structure/objects/error.rb +2 -0
- data/lib/jsonapionify/structure/objects/jsonapi.rb +1 -0
- data/lib/jsonapionify/structure/objects/link.rb +1 -0
- data/lib/jsonapionify/structure/objects/relationship.rb +2 -0
- data/lib/jsonapionify/structure/objects/resource.rb +2 -0
- data/lib/jsonapionify/structure/objects/resource_identifier.rb +12 -4
- data/lib/jsonapionify/structure/objects/top_level.rb +4 -2
- data/lib/jsonapionify/types/array_type.rb +16 -11
- data/lib/jsonapionify/types/boolean_type.rb +9 -4
- data/lib/jsonapionify/types/date_string_type.rb +7 -10
- data/lib/jsonapionify/types/float_type.rb +13 -0
- data/lib/jsonapionify/types/integer_type.rb +12 -0
- data/lib/jsonapionify/types/object_type.rb +7 -2
- data/lib/jsonapionify/types/string_type.rb +12 -0
- data/lib/jsonapionify/types/time_string_type.rb +8 -10
- data/lib/jsonapionify/types.rb +43 -5
- data/lib/jsonapionify/version.rb +1 -1
- data/lib/jsonapionify.rb +36 -1
- metadata +121 -74
|
@@ -9,82 +9,109 @@ module JSONAPIonify::Api
|
|
|
9
9
|
klass.class_eval do
|
|
10
10
|
extend JSONAPIonify::InheritedAttributes
|
|
11
11
|
include JSONAPIonify::Callbacks
|
|
12
|
-
define_callbacks
|
|
13
|
-
|
|
12
|
+
define_callbacks(
|
|
13
|
+
:request, :exception, :response,
|
|
14
|
+
:list, :commit_list,
|
|
15
|
+
:create, :commit_create,
|
|
16
|
+
:read, :commit_read,
|
|
17
|
+
:update, :commit_update,
|
|
18
|
+
:delete, :commit_delete,
|
|
19
|
+
:show, :commit_show,
|
|
20
|
+
:add, :commit_add,
|
|
21
|
+
:remove, :commit_remove,
|
|
22
|
+
:replace, :commit_replace
|
|
23
|
+
)
|
|
14
24
|
inherited_array_attribute :action_definitions
|
|
15
25
|
end
|
|
16
26
|
end
|
|
17
27
|
|
|
18
|
-
def list(
|
|
28
|
+
def list(content_type: nil, only_associated: false, callbacks: true, &block)
|
|
29
|
+
options = {
|
|
30
|
+
content_type: content_type,
|
|
31
|
+
only_associated: only_associated,
|
|
32
|
+
callbacks: callbacks,
|
|
33
|
+
cacheable: true
|
|
34
|
+
}
|
|
19
35
|
define_action(:list, 'GET', **options, &block).tap do |action|
|
|
20
36
|
action.response status: 200 do |context|
|
|
37
|
+
builder = context.respond_to?(:builder) ? context.builder : nil
|
|
21
38
|
context.response_object[:data] = build_collection(
|
|
22
|
-
context
|
|
39
|
+
context,
|
|
23
40
|
context.response_collection,
|
|
24
|
-
fields:
|
|
41
|
+
fields: context.fields,
|
|
42
|
+
include_cursors: (context.links.keys & [:first, :last, :next, :prev]).present?,
|
|
43
|
+
&builder
|
|
25
44
|
)
|
|
26
45
|
context.response_object.to_json
|
|
27
46
|
end
|
|
28
47
|
end
|
|
29
48
|
end
|
|
30
49
|
|
|
31
|
-
def
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
50
|
+
def create(content_type: nil, only_associated: false, callbacks: true, &block)
|
|
51
|
+
options = {
|
|
52
|
+
content_type: content_type,
|
|
53
|
+
only_associated: only_associated,
|
|
54
|
+
callbacks: callbacks,
|
|
55
|
+
cacheable: false,
|
|
56
|
+
example_input: :resource
|
|
57
|
+
}
|
|
37
58
|
define_action(:create, 'POST', **options, &block).tap do |action|
|
|
38
59
|
action.response status: 201 do |context|
|
|
39
|
-
context.
|
|
40
|
-
|
|
60
|
+
builder = context.respond_to?(:builder) ? context.builder : nil
|
|
61
|
+
context.response_object[:data] = build_resource(context, context.instance, fields: context.fields, &builder)
|
|
62
|
+
response_headers['Location'] = build_url(context, context.instance)
|
|
41
63
|
context.response_object.to_json
|
|
42
64
|
end
|
|
43
65
|
end
|
|
44
66
|
end
|
|
45
67
|
|
|
46
|
-
def read(
|
|
68
|
+
def read(content_type: nil, only_associated: false, callbacks: true, &block)
|
|
69
|
+
options = {
|
|
70
|
+
content_type: content_type,
|
|
71
|
+
only_associated: only_associated,
|
|
72
|
+
callbacks: callbacks,
|
|
73
|
+
cacheable: true
|
|
74
|
+
}
|
|
47
75
|
define_action(:read, 'GET', '/:id', **options, &block).tap do |action|
|
|
48
76
|
action.response status: 200 do |context|
|
|
49
|
-
context.
|
|
77
|
+
builder = context.respond_to?(:builder) ? context.builder : nil
|
|
78
|
+
context.response_object[:data] = build_resource(context, context.instance, fields: context.fields, &builder)
|
|
50
79
|
context.response_object.to_json
|
|
51
80
|
end
|
|
52
81
|
end
|
|
53
82
|
end
|
|
54
83
|
|
|
55
|
-
def update(
|
|
84
|
+
def update(content_type: nil, only_associated: false, callbacks: true, &block)
|
|
85
|
+
options = {
|
|
86
|
+
content_type: content_type,
|
|
87
|
+
only_associated: only_associated,
|
|
88
|
+
callbacks: callbacks,
|
|
89
|
+
cacheable: false,
|
|
90
|
+
example_input: :resource
|
|
91
|
+
}
|
|
56
92
|
define_action(:update, 'PATCH', '/:id', **options, &block).tap do |action|
|
|
57
93
|
action.response status: 200 do |context|
|
|
58
|
-
context.
|
|
94
|
+
builder = context.respond_to?(:builder) ? context.builder : nil
|
|
95
|
+
context.response_object[:data] = build_resource(context, context.instance, fields: context.fields, &builder)
|
|
59
96
|
context.response_object.to_json
|
|
60
97
|
end
|
|
61
98
|
end
|
|
62
99
|
end
|
|
63
100
|
|
|
64
|
-
def delete(
|
|
101
|
+
def delete(content_type: nil, only_associated: false, callbacks: true, &block)
|
|
102
|
+
options = {
|
|
103
|
+
content_type: content_type,
|
|
104
|
+
only_associated: only_associated,
|
|
105
|
+
callbacks: callbacks,
|
|
106
|
+
cacheable: false
|
|
107
|
+
}
|
|
65
108
|
define_action(:delete, 'DELETE', '/:id', **options, &block).tap do |action|
|
|
66
109
|
action.response status: 204
|
|
67
110
|
end
|
|
68
111
|
end
|
|
69
112
|
|
|
70
113
|
def process(request)
|
|
71
|
-
|
|
72
|
-
if request.options? && path_actions.present?
|
|
73
|
-
allow = [*path_actions.map(&:request_method), 'OPTIONS']
|
|
74
|
-
requests = allow.each_with_object({}) do |method, h|
|
|
75
|
-
h[method] = options_for_method(method)
|
|
76
|
-
end
|
|
77
|
-
Action.dummy do
|
|
78
|
-
response_headers['Allow'] = allow.join(', ')
|
|
79
|
-
end.response(status: 200, accept: 'application/vnd.api+json') do
|
|
80
|
-
JSONAPIonify.new_object(
|
|
81
|
-
meta: {
|
|
82
|
-
type: self.class.type,
|
|
83
|
-
requests: requests
|
|
84
|
-
}
|
|
85
|
-
).to_json
|
|
86
|
-
end.call(self, request)
|
|
87
|
-
elsif (action = find_supported_action(request))
|
|
114
|
+
if (action = find_supported_action(request))
|
|
88
115
|
action.call(self, request)
|
|
89
116
|
elsif (rel = find_supported_relationship(request))
|
|
90
117
|
relationship(rel.name).process(request)
|
|
@@ -93,24 +120,18 @@ module JSONAPIonify::Api
|
|
|
93
120
|
end
|
|
94
121
|
end
|
|
95
122
|
|
|
96
|
-
def
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
{
|
|
100
|
-
when 'POST', 'PUT', 'PATCH'
|
|
101
|
-
{ attributes: attributes.select(&:write).map(&:options_json) }
|
|
102
|
-
else
|
|
103
|
-
{}
|
|
123
|
+
def after(*action_names, &block)
|
|
124
|
+
return after_request &block if action_names.blank?
|
|
125
|
+
action_names.each do |action_name|
|
|
126
|
+
send("after_#{action_name}", &block)
|
|
104
127
|
end
|
|
105
128
|
end
|
|
106
129
|
|
|
107
|
-
def before(
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
action_name
|
|
130
|
+
def before(*action_names, &block)
|
|
131
|
+
return before_request &block if action_names.blank?
|
|
132
|
+
action_names.each do |action_name|
|
|
133
|
+
send("before_#{action_name}", &block)
|
|
111
134
|
end
|
|
112
|
-
return before_request &block if action_name == nil
|
|
113
|
-
send("before_#{action_name}", &block)
|
|
114
135
|
end
|
|
115
136
|
|
|
116
137
|
def define_action(name, *args, **options, &block)
|
|
@@ -150,22 +171,27 @@ module JSONAPIonify::Api
|
|
|
150
171
|
|
|
151
172
|
def find_supported_relationship(request)
|
|
152
173
|
relationship_definitions.find do |rel|
|
|
153
|
-
relationship(rel.name)
|
|
174
|
+
relationship = self.relationship(rel.name)
|
|
175
|
+
relationship != self && relationship.path_actions(request).present?
|
|
154
176
|
end
|
|
155
177
|
end
|
|
156
178
|
|
|
157
179
|
def remove_action(*names)
|
|
158
|
-
if names.include? :index
|
|
159
|
-
warn 'the `index` action will soon be deprecated, use `list` instead!'
|
|
160
|
-
names << :list
|
|
161
|
-
end
|
|
162
180
|
action_definitions.delete_if do |action_definition|
|
|
163
181
|
names.include? action_definition.name
|
|
164
182
|
end
|
|
165
183
|
end
|
|
166
184
|
|
|
185
|
+
def call_action(name, request, **context_overrides)
|
|
186
|
+
action(name).call(self, request, **context_overrides)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def action(name)
|
|
190
|
+
actions.find { |action| action.name == name }
|
|
191
|
+
end
|
|
192
|
+
|
|
167
193
|
def actions
|
|
168
|
-
return if action_definitions.blank?
|
|
194
|
+
return [] if action_definitions.blank?
|
|
169
195
|
action_definitions.select do |action|
|
|
170
196
|
action.only_associated == false ||
|
|
171
197
|
(respond_to?(:rel) && action.only_associated == true)
|
|
@@ -9,19 +9,31 @@ module JSONAPIonify::Api
|
|
|
9
9
|
delegate :id_attribute, :attributes, to: :class
|
|
10
10
|
|
|
11
11
|
context(:fields, readonly: true) do |context|
|
|
12
|
-
should_error
|
|
13
|
-
|
|
12
|
+
should_error = false
|
|
13
|
+
input_fields = context.request.params['fields'] || {}
|
|
14
|
+
actionable_fields = self.class.fields_for_action(context.action_name, context)
|
|
15
|
+
input_fields.each_with_object(
|
|
16
|
+
actionable_fields
|
|
17
|
+
) do |(type, fields), field_map|
|
|
14
18
|
type_sym = type.to_sym
|
|
19
|
+
field_symbols = fields.to_s.split(',').map(&:to_sym)
|
|
15
20
|
field_map[type_sym] =
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
21
|
+
field_symbols.each_with_object([]) do |field, field_list|
|
|
22
|
+
type_attributes = self.class.api.resource(type_sym).attributes
|
|
23
|
+
attribute = type_attributes.find do |attribute|
|
|
24
|
+
attribute.name == field &&
|
|
25
|
+
attribute.supports_read_for_action?(context.action_name, context)
|
|
26
|
+
end
|
|
27
|
+
if attribute
|
|
28
|
+
field_list << attribute.name
|
|
29
|
+
else
|
|
30
|
+
error(:field_not_permitted, type, field)
|
|
31
|
+
should_error = true
|
|
19
32
|
end
|
|
20
|
-
attribute ? field_list << attribute.name : error(:field_not_permitted, type, field) && (should_error = true)
|
|
21
33
|
end
|
|
34
|
+
end.tap do
|
|
35
|
+
halt if should_error
|
|
22
36
|
end
|
|
23
|
-
raise Errors::RequestError if should_error
|
|
24
|
-
fields
|
|
25
37
|
end
|
|
26
38
|
end
|
|
27
39
|
end
|
|
@@ -32,20 +44,44 @@ module JSONAPIonify::Api
|
|
|
32
44
|
end
|
|
33
45
|
end
|
|
34
46
|
|
|
35
|
-
def attribute(name, type, description = '', **options)
|
|
36
|
-
Attribute.new(
|
|
47
|
+
def attribute(name, type, description = '', **options, &block)
|
|
48
|
+
Attribute.new(
|
|
49
|
+
name, type, description, **options, &block
|
|
50
|
+
).tap do |new_attribute|
|
|
37
51
|
attributes.delete(new_attribute)
|
|
38
52
|
attributes << new_attribute
|
|
39
53
|
end
|
|
40
54
|
end
|
|
41
55
|
|
|
56
|
+
def remove_attribute(name)
|
|
57
|
+
attributes.delete_if { |attr| attr.name == name.to_sym }
|
|
58
|
+
end
|
|
59
|
+
|
|
42
60
|
def fields
|
|
43
61
|
attributes.select(&:read?).map(&:name)
|
|
44
62
|
end
|
|
45
63
|
|
|
64
|
+
def builder(&block)
|
|
65
|
+
context :builder, readonly: true, persisted: true do |context|
|
|
66
|
+
proc do |resource, instance|
|
|
67
|
+
block.call resource, instance, context
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
46
72
|
def field_valid?(name)
|
|
47
73
|
fields.include? name.to_sym
|
|
48
74
|
end
|
|
49
75
|
|
|
76
|
+
def fields_for_action(action, context)
|
|
77
|
+
api.fields.each_with_object({}) do |(type, attrs), fields|
|
|
78
|
+
fields[type] = attrs.select do |attr|
|
|
79
|
+
api.resource(type).attributes.find do |type_attr|
|
|
80
|
+
type_attr.name == attr
|
|
81
|
+
end.supports_read_for_action? action, context
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
50
86
|
end
|
|
51
87
|
end
|
|
@@ -8,8 +8,12 @@ module JSONAPIonify::Api
|
|
|
8
8
|
end
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
-
def context(name,
|
|
12
|
-
self.context_definitions[name.to_sym] = Context.new(
|
|
11
|
+
def context(name, **opts, &block)
|
|
12
|
+
self.context_definitions[name.to_sym] = Context.new(
|
|
13
|
+
**opts,
|
|
14
|
+
existing_context: self.context_definitions[name.to_sym],
|
|
15
|
+
&block
|
|
16
|
+
)
|
|
13
17
|
end
|
|
14
18
|
|
|
15
19
|
end
|
|
@@ -6,7 +6,7 @@ module JSONAPIonify::Api
|
|
|
6
6
|
end
|
|
7
7
|
|
|
8
8
|
def authentication(&block)
|
|
9
|
-
context :authentication, readonly: true do |context|
|
|
9
|
+
context :authentication, readonly: true, persisted: true do |context|
|
|
10
10
|
OpenStruct.new.tap do |authentication_object|
|
|
11
11
|
if instance_exec(context.request, authentication_object, &block) == false
|
|
12
12
|
error_now :forbidden
|
|
@@ -12,7 +12,7 @@ module JSONAPIonify::Api
|
|
|
12
12
|
define_method method do |**options|
|
|
13
13
|
@links[method] = URI.parse(@url).tap do |uri|
|
|
14
14
|
page_params = { page: options }.deep_stringify_keys
|
|
15
|
-
uri.query = @params.
|
|
15
|
+
uri.query = @params.merge(page_params).to_param
|
|
16
16
|
end.to_s
|
|
17
17
|
end
|
|
18
18
|
end
|
|
@@ -35,22 +35,14 @@ module JSONAPIonify::Api
|
|
|
35
35
|
slice =
|
|
36
36
|
if (params['before'] && params['first']) || (params['after'] && params['last'])
|
|
37
37
|
error :forbidden do
|
|
38
|
-
|
|
38
|
+
detail 'Illegal combination of parameters'
|
|
39
39
|
end
|
|
40
40
|
elsif (after = params['after'])
|
|
41
41
|
key_values = parse_and_validate_cursor(:after, after, context)
|
|
42
|
-
array_select_past_cursor(
|
|
43
|
-
collection,
|
|
44
|
-
context.sort_params,
|
|
45
|
-
key_values
|
|
46
|
-
).first(size)
|
|
42
|
+
array_select_past_cursor(collection, context.sort_params, key_values).first(size)
|
|
47
43
|
elsif (before = params['before'])
|
|
48
44
|
key_values = parse_and_validate_cursor(:before, before, context)
|
|
49
|
-
array_select_past_cursor(
|
|
50
|
-
collection,
|
|
51
|
-
context.sort_params.reverse,
|
|
52
|
-
key_values
|
|
53
|
-
).last(size)
|
|
45
|
+
array_select_past_cursor(collection, context.sort_params.invert, key_values).last(size)
|
|
54
46
|
elsif params['last']
|
|
55
47
|
collection.last(size)
|
|
56
48
|
else
|
|
@@ -59,8 +51,8 @@ module JSONAPIonify::Api
|
|
|
59
51
|
|
|
60
52
|
links.first first: size
|
|
61
53
|
links.last last: size
|
|
62
|
-
links.prev before: build_cursor_from_instance(context.request, slice.first), last: size unless slice.first == collection.first
|
|
63
|
-
links.next after: build_cursor_from_instance(context.request, slice.last), first: size unless slice.last == collection.last
|
|
54
|
+
links.prev before: build_cursor_from_instance(context.request, slice.first), last: size unless !slice.first || slice.first == collection.first
|
|
55
|
+
links.next after: build_cursor_from_instance(context.request, slice.last), first: size unless !slice.last || slice.last == collection.last
|
|
64
56
|
|
|
65
57
|
slice
|
|
66
58
|
end
|
|
@@ -71,22 +63,15 @@ module JSONAPIonify::Api
|
|
|
71
63
|
slice =
|
|
72
64
|
if (params['before'] && params['first']) || (params['after'] && params['last'])
|
|
73
65
|
error :forbidden do
|
|
74
|
-
|
|
66
|
+
detail 'Illegal combination of parameters'
|
|
75
67
|
end
|
|
76
68
|
elsif (after = params['after'])
|
|
77
69
|
key_values = parse_and_validate_cursor(:after, after, context)
|
|
78
|
-
arel_select_past_cursor(
|
|
79
|
-
collection,
|
|
80
|
-
context.sort_params,
|
|
81
|
-
key_values
|
|
82
|
-
).limit(size)
|
|
70
|
+
arel_select_past_cursor(collection, context.sort_params, key_values).limit(size)
|
|
83
71
|
elsif (before = params['before'])
|
|
84
72
|
key_values = parse_and_validate_cursor(:before, before, context)
|
|
85
|
-
ids = arel_select_past_cursor(
|
|
86
|
-
|
|
87
|
-
context.sort_params.reverse,
|
|
88
|
-
key_values
|
|
89
|
-
).reverse_order.limit(size).pluck(:id)
|
|
73
|
+
ids = arel_select_past_cursor(collection, context.sort_params.invert, key_values)
|
|
74
|
+
.reverse_order.limit(size).pluck(id_attribute)
|
|
90
75
|
collection.where(id_attribute => ids)
|
|
91
76
|
elsif params['last']
|
|
92
77
|
ids = collection.reverse_order.limit(size).pluck(id_attribute)
|
|
@@ -97,8 +82,8 @@ module JSONAPIonify::Api
|
|
|
97
82
|
|
|
98
83
|
links.first first: size
|
|
99
84
|
links.last last: size
|
|
100
|
-
links.prev before: build_cursor_from_instance(context.request, slice.first), last: size unless slice.first == collection.first
|
|
101
|
-
links.next after: build_cursor_from_instance(context.request, slice.last), first: size unless slice.last == collection.last
|
|
85
|
+
links.prev before: build_cursor_from_instance(context.request, slice.first), last: size unless !slice.first || slice.first == collection.first
|
|
86
|
+
links.next after: build_cursor_from_instance(context.request, slice.last), first: size unless !slice.last || slice.last == collection.last
|
|
102
87
|
|
|
103
88
|
slice
|
|
104
89
|
end
|
|
@@ -114,26 +99,30 @@ module JSONAPIonify::Api
|
|
|
114
99
|
param :page, :before, actions: %i{list}
|
|
115
100
|
param :page, :first, actions: %i{list}
|
|
116
101
|
param :page, :last, actions: %i{list}
|
|
117
|
-
context :paginated_collection do |context|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
102
|
+
context :paginated_collection, readonly: true do |context|
|
|
103
|
+
if context.root_request?
|
|
104
|
+
collection = context.sorted_collection
|
|
105
|
+
_, block = pagination_strategies.to_a.reverse.to_h.find do |mod, _|
|
|
106
|
+
Object.const_defined?(mod, false) && context.collection.class <= Object.const_get(mod, false)
|
|
107
|
+
end
|
|
122
108
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
109
|
+
links_delegate = PaginationLinksDelegate.new(
|
|
110
|
+
context.request.url,
|
|
111
|
+
context.params,
|
|
112
|
+
context.links
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
instance_exec(
|
|
116
|
+
collection,
|
|
117
|
+
context.request.params['page'] || {},
|
|
118
|
+
links_delegate,
|
|
119
|
+
per,
|
|
120
|
+
context,
|
|
121
|
+
&block
|
|
122
|
+
)
|
|
123
|
+
else
|
|
124
|
+
context.collection
|
|
125
|
+
end
|
|
137
126
|
end
|
|
138
127
|
end
|
|
139
128
|
|
|
@@ -166,18 +155,20 @@ module JSONAPIonify::Api
|
|
|
166
155
|
end
|
|
167
156
|
|
|
168
157
|
def arel_select_past_cursor(collection, sort_params, key_values)
|
|
169
|
-
|
|
158
|
+
subquery = sort_params.length.times.map do |i|
|
|
170
159
|
set = sort_params[0..i]
|
|
171
160
|
*contains_fields, outside_field = set
|
|
172
161
|
contains_fields.reduce(collection.reorder(nil)) do |relation, field|
|
|
173
|
-
relation.where
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
end.where(
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
end.join(' UNION ')
|
|
180
|
-
collection.from("(#{
|
|
162
|
+
relation.where(
|
|
163
|
+
collection.arel_table[field.name].send(field.contains_arel, key_values[field.name.to_s])
|
|
164
|
+
)
|
|
165
|
+
end.where(
|
|
166
|
+
collection.arel_table[outside_field.name].send(outside_field.outside_arel, key_values[outside_field.name.to_s])
|
|
167
|
+
)
|
|
168
|
+
end.map { |rel| "( #{rel.to_sql} )" }.join(' UNION ')
|
|
169
|
+
collection.from("(#{subquery}) AS #{collection.table_name}").tap(&:first)
|
|
170
|
+
rescue ActiveRecord::StatementInvalid
|
|
171
|
+
collection.where(id: collection.find_by_sql(subquery).map(&:id))
|
|
181
172
|
end
|
|
182
173
|
|
|
183
174
|
def parse_and_validate_cursor(param, cursor, context)
|
|
@@ -199,7 +190,7 @@ module JSONAPIonify::Api
|
|
|
199
190
|
detail 'The cursor sort does not match the request sort'
|
|
200
191
|
end
|
|
201
192
|
end
|
|
202
|
-
|
|
193
|
+
halt if should_error
|
|
203
194
|
|
|
204
195
|
options['a']
|
|
205
196
|
end
|
|
@@ -6,7 +6,7 @@ module JSONAPIonify::Api
|
|
|
6
6
|
extend JSONAPIonify::InheritedAttributes
|
|
7
7
|
inherited_hash_attribute :param_definitions
|
|
8
8
|
|
|
9
|
-
context(:params, readonly: true) do |context|
|
|
9
|
+
context(:params, readonly: true, persisted: true) do |context|
|
|
10
10
|
should_error = false
|
|
11
11
|
|
|
12
12
|
params = self.class.param_definitions.select do |_, v|
|
|
@@ -27,18 +27,23 @@ module JSONAPIonify::Api
|
|
|
27
27
|
reserved = ParamOptions.reserved?(k)
|
|
28
28
|
allowed = params.keys.include? keypath
|
|
29
29
|
valid = ParamOptions.valid?(k) || v.is_a?(Hash)
|
|
30
|
-
unless reserved || (allowed && valid)
|
|
30
|
+
unless reserved || (allowed && valid) || !context.root_request?
|
|
31
31
|
should_error = true
|
|
32
32
|
error :parameter_invalid, ParamOptions.keypath_to_string(*keypath)
|
|
33
33
|
end
|
|
34
|
-
end
|
|
34
|
+
end unless context.request.options?
|
|
35
35
|
|
|
36
36
|
# Check for requirement
|
|
37
|
-
|
|
37
|
+
missing_params =
|
|
38
|
+
ParamOptions.missing_parameters(
|
|
39
|
+
context.request.params,
|
|
40
|
+
required_params.values.map(&:keypath)
|
|
41
|
+
)
|
|
42
|
+
if context.root_request? && missing_params.present?
|
|
38
43
|
error :parameters_missing, missing_params
|
|
39
44
|
end
|
|
40
45
|
|
|
41
|
-
|
|
46
|
+
halt if should_error
|
|
42
47
|
|
|
43
48
|
# Return the params
|
|
44
49
|
context.request.params
|
|
@@ -58,22 +63,13 @@ module JSONAPIonify::Api
|
|
|
58
63
|
definition.keypath == keypath
|
|
59
64
|
end
|
|
60
65
|
next {} unless definition
|
|
61
|
-
value
|
|
66
|
+
value = definition.extract_value(params)
|
|
62
67
|
if definition.default_value?(value)
|
|
63
68
|
{}
|
|
64
69
|
else
|
|
65
70
|
definition.with_value(value)
|
|
66
71
|
end
|
|
67
72
|
end.reduce(:deep_merge)
|
|
68
|
-
|
|
69
|
-
# sticky_param_definitions = param_definitions.values.select(&:sticky)
|
|
70
|
-
# params.each_with_object do |k, v|
|
|
71
|
-
# definition = sticky_param_definitions.find do |definition|
|
|
72
|
-
# definition.keypath == ParamOptions.hash_to_keypaths(k => v)[0]
|
|
73
|
-
# end
|
|
74
|
-
# binding.pry
|
|
75
|
-
# definition && !definition.default_value?(v)
|
|
76
|
-
# end
|
|
77
73
|
end
|
|
78
74
|
|
|
79
75
|
end
|
|
@@ -8,18 +8,54 @@ module JSONAPIonify::Api
|
|
|
8
8
|
end
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
-
def relates_to_many(name,
|
|
12
|
-
define_relationship(name, Relationship::Many,
|
|
11
|
+
def relates_to_many(name, count_attribute: false, include_count: true, **opts, &block)
|
|
12
|
+
define_relationship(name, Relationship::Many, **opts, &block).tap do
|
|
13
|
+
define_relationship_counter(
|
|
14
|
+
name,
|
|
15
|
+
count_attribute === true ? "#{name.to_s.singularize}_count" : count_attribute.to_s,
|
|
16
|
+
include: include_count
|
|
17
|
+
) if count_attribute
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def relates_to_one(name, **opts, &block)
|
|
22
|
+
opts[:resource] ||= name.to_s.pluralize.to_sym
|
|
23
|
+
define_relationship(name, Relationship::One, **opts, &block)
|
|
13
24
|
end
|
|
14
25
|
|
|
15
|
-
def
|
|
16
|
-
|
|
26
|
+
def define_relationship_counter(rel_name, name, include: true)
|
|
27
|
+
before :response do |context|
|
|
28
|
+
if (context.scope.is_a?(ActiveRecord::Relation) || context.scope.is_a?(ActiveRecord::Base)) && context.scope._reflect_on_association(rel_name)
|
|
29
|
+
context.scope = context.scope.includes(rel_name)
|
|
30
|
+
end if context.fields[type&.to_sym].include? name.to_sym
|
|
31
|
+
end if include
|
|
32
|
+
attribute name.to_sym, types.Integer, "The number of #{rel_name}.", write: false do |_, instance, context|
|
|
33
|
+
rel = context.resource.class.relationship(rel_name)
|
|
34
|
+
blank_fields = context.fields.map { |k, _| [k, {}] }.to_h
|
|
35
|
+
rel_context = rel.new(
|
|
36
|
+
request: context.request,
|
|
37
|
+
context_overrides: {
|
|
38
|
+
owner: instance,
|
|
39
|
+
fields: blank_fields,
|
|
40
|
+
params: {}
|
|
41
|
+
}
|
|
42
|
+
).exec { |c| c }
|
|
43
|
+
count = rel_context.collection.uniq.count
|
|
44
|
+
case count
|
|
45
|
+
when Hash
|
|
46
|
+
count.values.reduce(:+)
|
|
47
|
+
when Fixnum
|
|
48
|
+
count
|
|
49
|
+
else
|
|
50
|
+
error :internal_server_error
|
|
51
|
+
end
|
|
52
|
+
end
|
|
17
53
|
end
|
|
18
54
|
|
|
19
|
-
def define_relationship(name, klass,
|
|
55
|
+
def define_relationship(name, klass, **opts, &block)
|
|
20
56
|
const_name = name.to_s.camelcase + 'Relationship'
|
|
21
57
|
remove_const(const_name) if const_defined? const_name
|
|
22
|
-
klass.new(self, name,
|
|
58
|
+
klass.new(self, name, **opts, &block).tap do |new_relationship|
|
|
23
59
|
relationship_definitions.delete new_relationship
|
|
24
60
|
relationship_definitions << new_relationship
|
|
25
61
|
end
|
|
@@ -34,7 +70,7 @@ module JSONAPIonify::Api
|
|
|
34
70
|
const_name = name.to_s.camelcase + 'Relationship'
|
|
35
71
|
return const_get(const_name, false) if const_defined? const_name
|
|
36
72
|
relationship_definition = relationship_definitions.find { |rel| rel.name == name }
|
|
37
|
-
raise Errors::
|
|
73
|
+
raise Errors::RelationshipNotFound, "Relationship not found: #{name}" unless relationship_definition
|
|
38
74
|
const_set const_name, relationship_definition.resource_class
|
|
39
75
|
end
|
|
40
76
|
|
|
@@ -6,7 +6,7 @@ module JSONAPIonify::Api
|
|
|
6
6
|
extend JSONAPIonify::InheritedAttributes
|
|
7
7
|
inherited_hash_attribute :request_header_definitions
|
|
8
8
|
|
|
9
|
-
context(:request_headers) do |context|
|
|
9
|
+
context(:request_headers, persisted: true, readonly: true) do |context|
|
|
10
10
|
should_error = false
|
|
11
11
|
|
|
12
12
|
# Check for validity
|
|
@@ -17,12 +17,15 @@ module JSONAPIonify::Api
|
|
|
17
17
|
v.required
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
missing_keys =
|
|
21
|
+
required_headers.keys.map(&:downcase) -
|
|
22
|
+
context.request.headers.keys.map(&:downcase)
|
|
23
|
+
if context.root_request? && missing_keys.present?
|
|
21
24
|
should_error = true
|
|
22
25
|
error :headers_missing, missing_keys
|
|
23
26
|
end
|
|
24
27
|
|
|
25
|
-
|
|
28
|
+
halt if should_error
|
|
26
29
|
|
|
27
30
|
context.request.headers
|
|
28
31
|
end
|
|
@@ -6,7 +6,7 @@ module JSONAPIonify::Api
|
|
|
6
6
|
extend JSONAPIonify::InheritedAttributes
|
|
7
7
|
inherited_hash_attribute :response_header_definitions
|
|
8
8
|
|
|
9
|
-
context(:response_headers) do |context|
|
|
9
|
+
context(:response_headers, persisted: true) do |context|
|
|
10
10
|
self.class.response_header_definitions.each_with_object({}) do |(name, block), headers|
|
|
11
11
|
headers[name.to_s] = instance_exec(context, &block)
|
|
12
12
|
end
|