jsonapi_compliable 0.6.4 → 0.6.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +1 -1
- data/.travis.yml +11 -3
- data/.yardopts +1 -0
- data/README.md +10 -1
- data/Rakefile +1 -0
- data/docs/JsonapiCompliable.html +202 -0
- data/docs/JsonapiCompliable/Adapters.html +119 -0
- data/docs/JsonapiCompliable/Adapters/Abstract.html +2285 -0
- data/docs/JsonapiCompliable/Adapters/ActiveRecord.html +2151 -0
- data/docs/JsonapiCompliable/Adapters/ActiveRecordSideloading.html +582 -0
- data/docs/JsonapiCompliable/Adapters/Null.html +1682 -0
- data/docs/JsonapiCompliable/Base.html +1395 -0
- data/docs/JsonapiCompliable/Deserializer.html +835 -0
- data/docs/JsonapiCompliable/Errors.html +115 -0
- data/docs/JsonapiCompliable/Errors/BadFilter.html +124 -0
- data/docs/JsonapiCompliable/Errors/StatNotFound.html +266 -0
- data/docs/JsonapiCompliable/Errors/UnsupportedPageSize.html +264 -0
- data/docs/JsonapiCompliable/Errors/ValidationError.html +124 -0
- data/docs/JsonapiCompliable/Extensions.html +117 -0
- data/docs/JsonapiCompliable/Extensions/BooleanAttribute.html +212 -0
- data/docs/JsonapiCompliable/Extensions/BooleanAttribute/ClassMethods.html +229 -0
- data/docs/JsonapiCompliable/Extensions/ExtraAttribute.html +242 -0
- data/docs/JsonapiCompliable/Extensions/ExtraAttribute/ClassMethods.html +237 -0
- data/docs/JsonapiCompliable/Query.html +1099 -0
- data/docs/JsonapiCompliable/Rails.html +211 -0
- data/docs/JsonapiCompliable/Resource.html +5241 -0
- data/docs/JsonapiCompliable/Scope.html +703 -0
- data/docs/JsonapiCompliable/Scoping.html +117 -0
- data/docs/JsonapiCompliable/Scoping/Base.html +843 -0
- data/docs/JsonapiCompliable/Scoping/DefaultFilter.html +318 -0
- data/docs/JsonapiCompliable/Scoping/ExtraFields.html +301 -0
- data/docs/JsonapiCompliable/Scoping/Filter.html +313 -0
- data/docs/JsonapiCompliable/Scoping/Filterable.html +364 -0
- data/docs/JsonapiCompliable/Scoping/Paginate.html +613 -0
- data/docs/JsonapiCompliable/Scoping/Sort.html +454 -0
- data/docs/JsonapiCompliable/SerializableTempId.html +216 -0
- data/docs/JsonapiCompliable/Sideload.html +2484 -0
- data/docs/JsonapiCompliable/Stats.html +117 -0
- data/docs/JsonapiCompliable/Stats/DSL.html +999 -0
- data/docs/JsonapiCompliable/Stats/Payload.html +391 -0
- data/docs/JsonapiCompliable/Util.html +117 -0
- data/docs/JsonapiCompliable/Util/FieldParams.html +228 -0
- data/docs/JsonapiCompliable/Util/Hash.html +471 -0
- data/docs/JsonapiCompliable/Util/IncludeParams.html +299 -0
- data/docs/JsonapiCompliable/Util/Persistence.html +435 -0
- data/docs/JsonapiCompliable/Util/RelationshipPayload.html +563 -0
- data/docs/JsonapiCompliable/Util/RenderOptions.html +250 -0
- data/docs/JsonapiCompliable/Util/ValidationResponse.html +532 -0
- data/docs/_index.html +527 -0
- data/docs/class_list.html +51 -0
- data/docs/css/common.css +1 -0
- data/docs/css/full_list.css +58 -0
- data/docs/css/style.css +492 -0
- data/docs/file.README.html +99 -0
- data/docs/file_list.html +56 -0
- data/docs/frames.html +17 -0
- data/docs/index.html +99 -0
- data/docs/js/app.js +248 -0
- data/docs/js/full_list.js +216 -0
- data/docs/js/jquery.js +4 -0
- data/docs/method_list.html +1731 -0
- data/docs/top-level-namespace.html +110 -0
- data/gemfiles/rails_5.gemfile +1 -1
- data/lib/jsonapi_compliable/adapters/abstract.rb +267 -4
- data/lib/jsonapi_compliable/adapters/active_record.rb +23 -1
- data/lib/jsonapi_compliable/adapters/null.rb +43 -3
- data/lib/jsonapi_compliable/base.rb +182 -33
- data/lib/jsonapi_compliable/deserializer.rb +90 -21
- data/lib/jsonapi_compliable/extensions/boolean_attribute.rb +12 -0
- data/lib/jsonapi_compliable/extensions/extra_attribute.rb +32 -0
- data/lib/jsonapi_compliable/extensions/temp_id.rb +11 -0
- data/lib/jsonapi_compliable/query.rb +94 -2
- data/lib/jsonapi_compliable/rails.rb +8 -0
- data/lib/jsonapi_compliable/resource.rb +548 -11
- data/lib/jsonapi_compliable/scope.rb +43 -1
- data/lib/jsonapi_compliable/scoping/base.rb +59 -8
- data/lib/jsonapi_compliable/scoping/default_filter.rb +33 -0
- data/lib/jsonapi_compliable/scoping/extra_fields.rb +33 -0
- data/lib/jsonapi_compliable/scoping/filter.rb +29 -2
- data/lib/jsonapi_compliable/scoping/filterable.rb +4 -0
- data/lib/jsonapi_compliable/scoping/paginate.rb +33 -0
- data/lib/jsonapi_compliable/scoping/sort.rb +18 -0
- data/lib/jsonapi_compliable/sideload.rb +229 -1
- data/lib/jsonapi_compliable/stats/dsl.rb +44 -0
- data/lib/jsonapi_compliable/stats/payload.rb +20 -0
- data/lib/jsonapi_compliable/util/field_params.rb +1 -0
- data/lib/jsonapi_compliable/util/hash.rb +18 -0
- data/lib/jsonapi_compliable/util/include_params.rb +22 -0
- data/lib/jsonapi_compliable/util/persistence.rb +13 -3
- data/lib/jsonapi_compliable/util/relationship_payload.rb +2 -0
- data/lib/jsonapi_compliable/util/render_options.rb +2 -0
- data/lib/jsonapi_compliable/util/validation_response.rb +16 -0
- data/lib/jsonapi_compliable/version.rb +1 -1
- metadata +60 -5
- data/gemfiles/rails_4.gemfile.lock +0 -208
- data/gemfiles/rails_5.gemfile.lock +0 -212
- data/lib/jsonapi_compliable/write.rb +0 -93
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
module JsonapiCompliable
|
|
2
2
|
module Extensions
|
|
3
|
+
# Turns ruby ? methods into is_ attributes
|
|
4
|
+
#
|
|
5
|
+
# @example Basic Usage
|
|
6
|
+
# boolean_attribute :active?
|
|
7
|
+
#
|
|
8
|
+
# # equivalent do
|
|
9
|
+
# def is_active
|
|
10
|
+
# @object.active?
|
|
11
|
+
# end
|
|
3
12
|
module BooleanAttribute
|
|
4
13
|
def self.included(klass)
|
|
5
14
|
klass.extend ClassMethods
|
|
6
15
|
end
|
|
7
16
|
|
|
8
17
|
module ClassMethods
|
|
18
|
+
# Register a boolean attribute
|
|
19
|
+
# @param name the corresponding ? method
|
|
20
|
+
# @param [Hash] options Normal .attribute options
|
|
9
21
|
def boolean_attribute(name, options = {}, &blk)
|
|
10
22
|
blk ||= proc { @object.public_send(name) }
|
|
11
23
|
field_name = :"is_#{name.to_s.gsub('?', '')}"
|
|
@@ -2,12 +2,44 @@ require 'jsonapi/serializable/resource/conditional_fields'
|
|
|
2
2
|
|
|
3
3
|
module JsonapiCompliable
|
|
4
4
|
module Extensions
|
|
5
|
+
# Only render a given attribute when the user specifically requests it.
|
|
6
|
+
# Useful for computationally-expensive attributes that are not required
|
|
7
|
+
# on every request.
|
|
8
|
+
#
|
|
9
|
+
# This class handles the serialization, but you may also want to run
|
|
10
|
+
# code during scoping (for instance, to eager load associations referenced
|
|
11
|
+
# by this extra attribute. See (Resource.extra_field).
|
|
12
|
+
#
|
|
13
|
+
# @example Basic Usage
|
|
14
|
+
# # Will only be rendered on user request, ie
|
|
15
|
+
# # /people?extra_fields[people]=net_worth
|
|
16
|
+
# extra_attribute :net_worth
|
|
17
|
+
#
|
|
18
|
+
# @example Eager Loading
|
|
19
|
+
# class PersonResource < ApplicationResource
|
|
20
|
+
# # If the user requests the 'net_worth' attribute, make sure
|
|
21
|
+
# # 'assets' are eager loaded
|
|
22
|
+
# extra_field :net_worth do |scope|
|
|
23
|
+
# scope.includes(:assets)
|
|
24
|
+
# end
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
# class SerializablePerson < JSONAPI::Serializable::Resource
|
|
28
|
+
# # ... code ...
|
|
29
|
+
# extra_attribute :net_worth do
|
|
30
|
+
# @object.assets.sum(&:value)
|
|
31
|
+
# end
|
|
32
|
+
# end
|
|
33
|
+
#
|
|
34
|
+
# @see Resource.extra_field
|
|
5
35
|
module ExtraAttribute
|
|
6
36
|
def self.included(klass)
|
|
7
37
|
klass.extend ClassMethods
|
|
8
38
|
end
|
|
9
39
|
|
|
10
40
|
module ClassMethods
|
|
41
|
+
# @param [Symbol] name the name of the attribute
|
|
42
|
+
# @param [Hash] options the options passed on to vanilla to .attribute
|
|
11
43
|
def extra_attribute(name, options = {}, &blk)
|
|
12
44
|
allow_field = proc {
|
|
13
45
|
if options[:if]
|
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
module JsonapiCompliable
|
|
2
|
+
# If the object we are serializing has the instance variable
|
|
3
|
+
# +@_jsonapi_temp_id+, render +temp-id+ in the {http://jsonapi.org/format/#document-resource-identifier-objects resource identifier}
|
|
4
|
+
#
|
|
5
|
+
# Why? Well, when the request is a nested POST, creating the main entity as
|
|
6
|
+
# well as relationships, we need some way of telling the client, "hey, the
|
|
7
|
+
# object you have in memory, that you just sent to the server, has been
|
|
8
|
+
# persisted and now has id X".
|
|
9
|
+
#
|
|
10
|
+
# +@_jsonapi_temp_id+ is set within this library. You should never have to
|
|
11
|
+
# reference it directly.
|
|
2
12
|
module SerializableTempId
|
|
13
|
+
# Common interface for jsonapi-rb extensions
|
|
3
14
|
def as_jsonapi(*)
|
|
4
15
|
super.tap do |hash|
|
|
5
16
|
if temp_id = @object.instance_variable_get(:'@_jsonapi_temp_id')
|
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
# TODO: refactor - code could be better but it's a one-time thing.
|
|
2
|
-
|
|
3
1
|
module JsonapiCompliable
|
|
2
|
+
# @attr_reader [Hash] params hash of query parameters with symbolized keys
|
|
3
|
+
# @attr_reader [Resource] resource the corresponding Resource object
|
|
4
4
|
class Query
|
|
5
|
+
# TODO: This class could use some refactoring love!
|
|
5
6
|
attr_reader :params, :resource
|
|
6
7
|
|
|
8
|
+
# This is the structure of +Query#to_hash+ used elsewhere in the library
|
|
9
|
+
# @see #to_hash
|
|
10
|
+
# @api private
|
|
11
|
+
# @return [Hash] the default hash
|
|
7
12
|
def self.default_hash
|
|
8
13
|
{
|
|
9
14
|
filter: {},
|
|
@@ -21,10 +26,26 @@ module JsonapiCompliable
|
|
|
21
26
|
@params = params
|
|
22
27
|
end
|
|
23
28
|
|
|
29
|
+
# The relevant include directive
|
|
30
|
+
# @see http://jsonapi-rb.org
|
|
31
|
+
# @return [JSONAPI::IncludeDirective]
|
|
24
32
|
def include_directive
|
|
25
33
|
@include_directive ||= JSONAPI::IncludeDirective.new(params[:include])
|
|
26
34
|
end
|
|
27
35
|
|
|
36
|
+
# The include, directive, as a hash. For instance
|
|
37
|
+
#
|
|
38
|
+
# { posts: { comments: {} } }
|
|
39
|
+
#
|
|
40
|
+
# This will only include relationships that are
|
|
41
|
+
#
|
|
42
|
+
# * Available on the Resource
|
|
43
|
+
# * Whitelisted (when specified)
|
|
44
|
+
#
|
|
45
|
+
# So that users can't simply request your entire object graph.
|
|
46
|
+
#
|
|
47
|
+
# @see Util::IncludeParams
|
|
48
|
+
# @return [Hash] the scrubbed include directive as a hash
|
|
28
49
|
def include_hash
|
|
29
50
|
@include_hash ||= begin
|
|
30
51
|
requested = include_directive.to_hash
|
|
@@ -36,10 +57,66 @@ module JsonapiCompliable
|
|
|
36
57
|
end
|
|
37
58
|
end
|
|
38
59
|
|
|
60
|
+
# All the keys of the #include_hash
|
|
61
|
+
#
|
|
62
|
+
# For example, let's say we had
|
|
63
|
+
#
|
|
64
|
+
# { posts: { comments: {} }
|
|
65
|
+
#
|
|
66
|
+
# +#association_names+ would return
|
|
67
|
+
#
|
|
68
|
+
# [:posts, :comments]
|
|
69
|
+
#
|
|
70
|
+
# @return [Array<Symbol>] all association names, recursive
|
|
39
71
|
def association_names
|
|
40
72
|
@association_names ||= Util::Hash.keys(include_hash)
|
|
41
73
|
end
|
|
42
74
|
|
|
75
|
+
# A flat hash of sanitized query parameters.
|
|
76
|
+
# All relationship names are top-level:
|
|
77
|
+
#
|
|
78
|
+
# {
|
|
79
|
+
# posts: { filter, sort, ... }
|
|
80
|
+
# comments: { filter, sort, ... }
|
|
81
|
+
# }
|
|
82
|
+
#
|
|
83
|
+
# @example sorting
|
|
84
|
+
# # GET /posts?sort=-title
|
|
85
|
+
# { posts: { sort: { title: :desc } } }
|
|
86
|
+
#
|
|
87
|
+
# @example pagination
|
|
88
|
+
# # GET /posts?page[number]=2&page[size]=10
|
|
89
|
+
# { posts: { page: { number: 2, size: 10 } }
|
|
90
|
+
#
|
|
91
|
+
# @example filtering
|
|
92
|
+
# # GET /posts?filter[title]=Foo
|
|
93
|
+
# { posts: { filter: { title: 'Foo' } }
|
|
94
|
+
#
|
|
95
|
+
# @example include
|
|
96
|
+
# # GET /posts?include=comments.author
|
|
97
|
+
# { posts: { include: { comments: { author: {} } } } }
|
|
98
|
+
#
|
|
99
|
+
# @example stats
|
|
100
|
+
# # GET /posts?stats[likes]=count,average
|
|
101
|
+
# { posts: { stats: [:count, :average] } }
|
|
102
|
+
#
|
|
103
|
+
# @example fields
|
|
104
|
+
# # GET /posts?fields=foo,bar
|
|
105
|
+
# { posts: { fields: [:foo, :bar] } }
|
|
106
|
+
#
|
|
107
|
+
# @example extra fields
|
|
108
|
+
# # GET /posts?fields=foo,bar
|
|
109
|
+
# { posts: { extra_fields: [:foo, :bar] } }
|
|
110
|
+
#
|
|
111
|
+
# @example nested parameters
|
|
112
|
+
# # GET /posts?include=comments&sort=comments.created_at&page[comments][size]=3
|
|
113
|
+
# {
|
|
114
|
+
# posts: { ... },
|
|
115
|
+
# comments: { page: { size: 3 }, sort: { created_at: :asc } }
|
|
116
|
+
#
|
|
117
|
+
# @see #default_hash
|
|
118
|
+
# @see Base#query_hash
|
|
119
|
+
# @return [Hash] the normalized query hash
|
|
43
120
|
def to_hash
|
|
44
121
|
hash = { resource.type => self.class.default_hash }
|
|
45
122
|
|
|
@@ -64,6 +141,21 @@ module JsonapiCompliable
|
|
|
64
141
|
hash
|
|
65
142
|
end
|
|
66
143
|
|
|
144
|
+
# Check if the user has requested 0 actual results
|
|
145
|
+
# They may have done this to get, say, the total count
|
|
146
|
+
# without the overhead of fetching actual records.
|
|
147
|
+
#
|
|
148
|
+
# @example Total Count, 0 Results
|
|
149
|
+
# # GET /posts?page[size]=0&stats[total]=count
|
|
150
|
+
# # Response:
|
|
151
|
+
# {
|
|
152
|
+
# data: [],
|
|
153
|
+
# meta: {
|
|
154
|
+
# stats: { total: { count: 100 } }
|
|
155
|
+
# }
|
|
156
|
+
# }
|
|
157
|
+
#
|
|
158
|
+
# @return [Boolean] were 0 results requested?
|
|
67
159
|
def zero_results?
|
|
68
160
|
!@params[:page].nil? &&
|
|
69
161
|
!@params[:page][:size].nil? &&
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
require 'jsonapi/rails'
|
|
2
2
|
|
|
3
3
|
module JsonapiCompliable
|
|
4
|
+
# Rails Integration. Mix this in to ApplicationController.
|
|
5
|
+
#
|
|
6
|
+
# * Mixes in Base
|
|
7
|
+
# * Adds a global around_action (see Base#wrap_context)
|
|
8
|
+
# * Uses Rails' +render+ for rendering
|
|
9
|
+
#
|
|
10
|
+
# @see Base#render_jsonapi
|
|
11
|
+
# @see Base#wrap_context
|
|
4
12
|
module Rails
|
|
5
13
|
def self.included(klass)
|
|
6
14
|
klass.send(:include, Base)
|
|
@@ -1,33 +1,203 @@
|
|
|
1
1
|
module JsonapiCompliable
|
|
2
|
+
# Resources hold configuration: How do you want to process incoming JSONAPI
|
|
3
|
+
# requests?
|
|
4
|
+
#
|
|
5
|
+
# Let's say we start with an empty hash as our scope object:
|
|
6
|
+
#
|
|
7
|
+
# render_jsonapi({})
|
|
8
|
+
#
|
|
9
|
+
# Let's define the behavior of various parameters. Here we'll merge
|
|
10
|
+
# options into our hash when the user filters, sorts, and paginates.
|
|
11
|
+
# Then, we'll pass that hash off to an HTTP Client:
|
|
12
|
+
#
|
|
13
|
+
# class PostResource < ApplicationResource
|
|
14
|
+
# type :posts
|
|
15
|
+
# use_adapter JsonapiCompliable::Adapters::Null
|
|
16
|
+
#
|
|
17
|
+
# # What do do when filter[active] parameter comes in
|
|
18
|
+
# allow_filter :active do |scope, value|
|
|
19
|
+
# scope.merge(active: value)
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
# # What do do when sorting parameters come in
|
|
23
|
+
# sort do |scope, attribute, direction|
|
|
24
|
+
# scope.merge(order: { attribute => direction })
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
# # What do do when pagination parameters come in
|
|
28
|
+
# page do |scope, current_page, per_page|
|
|
29
|
+
# scope.merge(page: current_page, per_page: per_page)
|
|
30
|
+
# end
|
|
31
|
+
#
|
|
32
|
+
# # Resolve the scope by passing the hash to an HTTP Client
|
|
33
|
+
# def resolve(scope)
|
|
34
|
+
# MyHttpClient.get(scope)
|
|
35
|
+
# end
|
|
36
|
+
# end
|
|
37
|
+
#
|
|
38
|
+
# This code can quickly become duplicative - we probably want to reuse
|
|
39
|
+
# this logic for other objects that use the same HTTP client.
|
|
40
|
+
#
|
|
41
|
+
# That's why we also have *Adapters*. Adapters encapsulate common, reusable
|
|
42
|
+
# resource configuration. That's why we don't need to specify the above code
|
|
43
|
+
# when using +ActiveRecord+ - the default logic is already in the adapter.
|
|
44
|
+
#
|
|
45
|
+
# class PostResource < ApplicationResource
|
|
46
|
+
# type :posts
|
|
47
|
+
# use_adapter JsonapiCompliable::Adapters::ActiveRecord
|
|
48
|
+
#
|
|
49
|
+
# allow_filter :title
|
|
50
|
+
# end
|
|
51
|
+
#
|
|
52
|
+
# Of course, we can always override the Resource directly for one-off
|
|
53
|
+
# customizations:
|
|
54
|
+
#
|
|
55
|
+
# class PostResource < ApplicationResource
|
|
56
|
+
# type :posts
|
|
57
|
+
# use_adapter JsonapiCompliable::Adapters::ActiveRecord
|
|
58
|
+
#
|
|
59
|
+
# allow_filter :title_prefix do |scope, value|
|
|
60
|
+
# scope.where(["title LIKE ?", "#{value}%"])
|
|
61
|
+
# end
|
|
62
|
+
# end
|
|
63
|
+
#
|
|
64
|
+
# Resources can also define *Sideloads*. Sideloads define the relationships between resources:
|
|
65
|
+
#
|
|
66
|
+
# allow_sideload :comments, resource: CommentResource do
|
|
67
|
+
# # How to fetch the associated objects
|
|
68
|
+
# # This will be further chained down the line
|
|
69
|
+
# scope do |posts|
|
|
70
|
+
# Comment.where(post_id: posts.map(&:id))
|
|
71
|
+
# end
|
|
72
|
+
#
|
|
73
|
+
# # Now that we've resolved everything, how to assign the objects
|
|
74
|
+
# assign do |posts, comments|
|
|
75
|
+
# posts.each do |post|
|
|
76
|
+
# relevant_comments = comments.select { |c| c.post_id === post.id }
|
|
77
|
+
# post.comments = relevant_comments
|
|
78
|
+
# end
|
|
79
|
+
# end
|
|
80
|
+
# end
|
|
81
|
+
#
|
|
82
|
+
# Once again, we can DRY this up using an Adapter:
|
|
83
|
+
#
|
|
84
|
+
# use_adapter JsonapiCompliable::Adapters::ActiveRecord
|
|
85
|
+
#
|
|
86
|
+
# has_many :comments,
|
|
87
|
+
# scope: -> { Comment.all },
|
|
88
|
+
# resource: CommentResource,
|
|
89
|
+
# foreign_key: :post_id
|
|
90
|
+
#
|
|
91
|
+
# @attr_reader [Hash] context A hash of +object+ and +namespace+ - Example object is a Rails controller, example namespace would be +:index+ or +:show+
|
|
2
92
|
class Resource
|
|
93
|
+
extend Forwardable
|
|
3
94
|
attr_reader :context
|
|
4
95
|
|
|
5
96
|
class << self
|
|
97
|
+
extend Forwardable
|
|
6
98
|
attr_accessor :config
|
|
7
99
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
100
|
+
# @!method allow_sideload
|
|
101
|
+
# @see Sideload#allow_sideload
|
|
102
|
+
def_delegator :sideloading, :allow_sideload
|
|
103
|
+
# @!method has_many
|
|
104
|
+
# @see Adapters::ActiveRecordSideloading#has_many
|
|
105
|
+
def_delegator :sideloading, :has_many
|
|
106
|
+
# @!method has_one
|
|
107
|
+
# @see Adapters::ActiveRecordSideloading#has_one
|
|
108
|
+
def_delegator :sideloading, :has_one
|
|
109
|
+
# @!method belongs_to
|
|
110
|
+
# @see Adapters::ActiveRecordSideloading#belongs_to
|
|
111
|
+
def_delegator :sideloading, :belongs_to
|
|
112
|
+
# @!method has_and_belongs_to_many
|
|
113
|
+
# @see Adapters::ActiveRecordSideloading#has_and_belongs_to_many
|
|
114
|
+
def_delegator :sideloading, :has_and_belongs_to_many
|
|
115
|
+
# @!method polymorphic_belongs_to
|
|
116
|
+
# @see Adapters::ActiveRecordSideloading#polymorphic_belongs_to
|
|
117
|
+
def_delegator :sideloading, :polymorphic_belongs_to
|
|
118
|
+
# @!method polymorphic_has_many
|
|
119
|
+
# @see Adapters::ActiveRecordSideloading#polymorphic_has_many
|
|
120
|
+
def_delegator :sideloading, :polymorphic_has_many
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# @!method sideload
|
|
124
|
+
# @see Sideload#sideload
|
|
125
|
+
def_delegator :sideloading, :sideload
|
|
126
|
+
|
|
127
|
+
# @private
|
|
19
128
|
def self.inherited(klass)
|
|
20
129
|
klass.config = Util::Hash.deep_dup(self.config)
|
|
21
130
|
end
|
|
22
131
|
|
|
132
|
+
# @api private
|
|
23
133
|
def self.sideloading
|
|
24
134
|
@sideloading ||= Sideload.new(:base, resource: self)
|
|
25
135
|
end
|
|
26
136
|
|
|
137
|
+
# Set the sideload whitelist. You may want to omit sideloads for
|
|
138
|
+
# security or performance reasons.
|
|
139
|
+
#
|
|
140
|
+
# Uses JSONAPI::IncludeDirective from {{http://jsonapi-rb.org jsonapi-rb}}
|
|
141
|
+
#
|
|
142
|
+
# @example Whitelisting Relationships
|
|
143
|
+
# # Given the following whitelist
|
|
144
|
+
# class PostResource < ApplicationResource
|
|
145
|
+
# # ... code ...
|
|
146
|
+
# sideload_whitelist([:blog, { comments: :author }])
|
|
147
|
+
# end
|
|
148
|
+
#
|
|
149
|
+
# # A request to sideload 'tags'
|
|
150
|
+
# #
|
|
151
|
+
# # GET /posts?include=tags
|
|
152
|
+
# #
|
|
153
|
+
# # ...will silently fail.
|
|
154
|
+
# #
|
|
155
|
+
# # A request for comments and tags:
|
|
156
|
+
# #
|
|
157
|
+
# # GET /posts?include=tags,comments
|
|
158
|
+
# #
|
|
159
|
+
# # ...will only sideload comments
|
|
160
|
+
#
|
|
161
|
+
# @param [Hash, Array, Symbol] whitelist
|
|
162
|
+
# @see Query#include_hash
|
|
27
163
|
def self.sideload_whitelist(whitelist)
|
|
28
164
|
config[:sideload_whitelist] = JSONAPI::IncludeDirective.new(whitelist).to_hash
|
|
29
165
|
end
|
|
30
166
|
|
|
167
|
+
# Whitelist a filter
|
|
168
|
+
#
|
|
169
|
+
# @example Basic Filtering
|
|
170
|
+
# allow_filter :title
|
|
171
|
+
#
|
|
172
|
+
# # When using ActiveRecord, this code is equivalent
|
|
173
|
+
# allow_filter :title do |scope, value|
|
|
174
|
+
# scope.where(title: value)
|
|
175
|
+
# end
|
|
176
|
+
#
|
|
177
|
+
# @example Custom Filtering
|
|
178
|
+
# # All filters can be customized with a block
|
|
179
|
+
# allow_filter :title_prefix do |scope, value|
|
|
180
|
+
# scope.where('title LIKE ?', "#{value}%")
|
|
181
|
+
# end
|
|
182
|
+
#
|
|
183
|
+
# @example Guarding Filters
|
|
184
|
+
# # Only allow the current user to filter on a property
|
|
185
|
+
# allow_filter :title, if: :admin?
|
|
186
|
+
#
|
|
187
|
+
# def admin?
|
|
188
|
+
# current_user.role == 'admin'
|
|
189
|
+
# end
|
|
190
|
+
#
|
|
191
|
+
# If a filter is not allowed, a +Jsonapi::Errors::BadFilter+ error will be raised.
|
|
192
|
+
#
|
|
193
|
+
# @overload allow_filter(name, options = {})
|
|
194
|
+
# @param [Symbol] name The name of the filter
|
|
195
|
+
# @param [Hash] options
|
|
196
|
+
# @option options [Symbol] :if A method name on the current context - If the method returns false, +BadFilter+ will be raised.
|
|
197
|
+
# @option options [Array<Symbol>] :aliases Allow the user to specify these aliases in the URL, then match to this filter. Mainly used for backwards-compatibility.
|
|
198
|
+
#
|
|
199
|
+
# @yieldparam scope The object being scoped
|
|
200
|
+
# @yieldparam value The sanitized value from the URL
|
|
31
201
|
def self.allow_filter(name, *args, &blk)
|
|
32
202
|
opts = args.extract_options!
|
|
33
203
|
aliases = [name, opts[:aliases]].flatten.compact
|
|
@@ -38,54 +208,210 @@ module JsonapiCompliable
|
|
|
38
208
|
}
|
|
39
209
|
end
|
|
40
210
|
|
|
211
|
+
# Whitelist a statistic.
|
|
212
|
+
#
|
|
213
|
+
# Statistics are requested like
|
|
214
|
+
#
|
|
215
|
+
# GET /posts?stats[total]=count
|
|
216
|
+
#
|
|
217
|
+
# And returned in +meta+:
|
|
218
|
+
#
|
|
219
|
+
# {
|
|
220
|
+
# data: [...],
|
|
221
|
+
# meta: { stats: { total: { count: 100 } } }
|
|
222
|
+
# }
|
|
223
|
+
#
|
|
224
|
+
# Statistics take into account the current scope, *without pagination*.
|
|
225
|
+
#
|
|
226
|
+
# @example Total Count
|
|
227
|
+
# allow_stat total: [:count]
|
|
228
|
+
#
|
|
229
|
+
# @example Average Rating
|
|
230
|
+
# allow_stat rating: [:average]
|
|
231
|
+
#
|
|
232
|
+
# @example Custom Stat
|
|
233
|
+
# allow_stat rating: [:average] do
|
|
234
|
+
# standard_deviation { |scope, attr| ... }
|
|
235
|
+
# end
|
|
236
|
+
#
|
|
237
|
+
# @param [Symbol, Hash] symbol_or_hash The attribute and metric
|
|
238
|
+
# @yieldparam scope The object being scoped
|
|
239
|
+
# @yieldparam [Symbol] attr The name of the metric
|
|
41
240
|
def self.allow_stat(symbol_or_hash, &blk)
|
|
42
241
|
dsl = Stats::DSL.new(config[:adapter], symbol_or_hash)
|
|
43
242
|
dsl.instance_eval(&blk) if blk
|
|
44
243
|
config[:stats][dsl.name] = dsl
|
|
45
244
|
end
|
|
46
245
|
|
|
246
|
+
# When you want a filter to always apply, on every request.
|
|
247
|
+
#
|
|
248
|
+
# @example Only Active Posts
|
|
249
|
+
# default_filter :active do |scope|
|
|
250
|
+
# scope.where(active: true)
|
|
251
|
+
# end
|
|
252
|
+
#
|
|
253
|
+
# Default filters can be overridden *if* there is a corresponding +allow_filter+:
|
|
254
|
+
#
|
|
255
|
+
# @example Overriding Default Filters
|
|
256
|
+
# allow_filter :active
|
|
257
|
+
#
|
|
258
|
+
# default_filter :active do |scope|
|
|
259
|
+
# scope.where(active: true)
|
|
260
|
+
# end
|
|
261
|
+
#
|
|
262
|
+
# # GET /posts?filter[active]=false
|
|
263
|
+
# # Returns only active posts
|
|
264
|
+
#
|
|
265
|
+
# @see .allow_filter
|
|
266
|
+
# @param [Symbol] name The default filter name
|
|
267
|
+
# @yieldparam scope The object being scoped
|
|
47
268
|
def self.default_filter(name, &blk)
|
|
48
269
|
config[:default_filters][name.to_sym] = {
|
|
49
270
|
filter: blk
|
|
50
271
|
}
|
|
51
272
|
end
|
|
52
273
|
|
|
274
|
+
# The Model object associated with this class.
|
|
275
|
+
#
|
|
276
|
+
# This model will be utilized on write requests.
|
|
277
|
+
#
|
|
278
|
+
# Models need not be ActiveRecord ;)
|
|
279
|
+
#
|
|
280
|
+
# @example
|
|
281
|
+
# class PostResource < ApplicationResource
|
|
282
|
+
# # ... code ...
|
|
283
|
+
# model Post
|
|
284
|
+
# end
|
|
285
|
+
#
|
|
286
|
+
# @param [Class] klass The associated Model class
|
|
53
287
|
def self.model(klass)
|
|
54
288
|
config[:model] = klass
|
|
55
289
|
end
|
|
56
290
|
|
|
291
|
+
# Define custom sorting logic
|
|
292
|
+
#
|
|
293
|
+
# @example Sort on alternate table
|
|
294
|
+
# # GET /employees?sort=title
|
|
295
|
+
# sort do |scope, att, dir|
|
|
296
|
+
# if att == :title
|
|
297
|
+
# scope.joins(:current_position).order("title #{dir}")
|
|
298
|
+
# else
|
|
299
|
+
# scope.order(att => dir)
|
|
300
|
+
# end
|
|
301
|
+
# end
|
|
302
|
+
#
|
|
303
|
+
# @yieldparam scope The current object being scoped
|
|
304
|
+
# @yieldparam [Symbol] att The requested sort attribute
|
|
305
|
+
# @yieldparam [Symbol] dir The requested sort direction (:asc/:desc)
|
|
57
306
|
def self.sort(&blk)
|
|
58
307
|
config[:sorting] = blk
|
|
59
308
|
end
|
|
60
309
|
|
|
310
|
+
# Define custom pagination logic
|
|
311
|
+
#
|
|
312
|
+
# @example Use will_paginate instead of Kaminari
|
|
313
|
+
# # GET /employees?page[size]=10&page[number]=2
|
|
314
|
+
# paginate do |scope, current_page, per_page|
|
|
315
|
+
# scope.paginate(page: current_page, per_page: per_page)
|
|
316
|
+
# end
|
|
317
|
+
#
|
|
318
|
+
# @yieldparam scope The current object being scoped
|
|
319
|
+
# @yieldparam [Integer] current_page The page[number] parameter value
|
|
320
|
+
# @yieldparam [Integer] per_page The page[size] parameter value
|
|
61
321
|
def self.paginate(&blk)
|
|
62
322
|
config[:pagination] = blk
|
|
63
323
|
end
|
|
64
324
|
|
|
325
|
+
# Perform special logic when an extra field is requested.
|
|
326
|
+
# Often used to eager load data that will be used to compute the
|
|
327
|
+
# extra field.
|
|
328
|
+
#
|
|
329
|
+
# This is *not* required if you have no custom logic.
|
|
330
|
+
#
|
|
331
|
+
# @example Eager load if extra field is required
|
|
332
|
+
# # GET /employees?extra_fields[employees]=net_worth
|
|
333
|
+
# extra_field(employees: [:net_worth]) do |scope|
|
|
334
|
+
# scope.includes(:assets)
|
|
335
|
+
# end
|
|
336
|
+
#
|
|
337
|
+
# @see Scoping::ExtraFields
|
|
338
|
+
#
|
|
339
|
+
# @param [Symbol] name Name of the extra field
|
|
340
|
+
# @yieldparam scope The current object being scoped
|
|
341
|
+
# @yieldparam [Integer] current_page The page[number] parameter value
|
|
342
|
+
# @yieldparam [Integer] per_page The page[size] parameter value
|
|
65
343
|
def self.extra_field(name, &blk)
|
|
66
344
|
config[:extra_fields][name] = blk
|
|
67
345
|
end
|
|
68
346
|
|
|
347
|
+
# Configure the adapter you want to use.
|
|
348
|
+
#
|
|
349
|
+
# @example ActiveRecord Adapter
|
|
350
|
+
# require 'jsonapi_compliable/adapters/active_record'
|
|
351
|
+
# use_adapter JsonapiCompliable::Adapters::ActiveRecord
|
|
352
|
+
#
|
|
353
|
+
# @param [Class] klass The adapter class
|
|
69
354
|
def self.use_adapter(klass)
|
|
70
355
|
config[:adapter] = klass.new
|
|
71
356
|
end
|
|
72
357
|
|
|
358
|
+
# Override default sort applied when not present in the query parameters.
|
|
359
|
+
#
|
|
360
|
+
# Default: [{ id: :asc }]
|
|
361
|
+
#
|
|
362
|
+
# @example Order by created_at descending by default
|
|
363
|
+
# # GET /employees will order by created_at descending
|
|
364
|
+
# default_sort([{ created_at: :desc }])
|
|
365
|
+
#
|
|
366
|
+
# @param [Array<Hash>] val Array of sorting criteria
|
|
73
367
|
def self.default_sort(val)
|
|
74
368
|
config[:default_sort] = val
|
|
75
369
|
end
|
|
76
370
|
|
|
371
|
+
# The JSONAPI Type. For instance if you queried:
|
|
372
|
+
#
|
|
373
|
+
# GET /employees?fields[positions]=title
|
|
374
|
+
#
|
|
375
|
+
# And/Or got back in the response
|
|
376
|
+
#
|
|
377
|
+
# { id: '1', type: 'positions' }
|
|
378
|
+
#
|
|
379
|
+
# The type would be :positions
|
|
380
|
+
#
|
|
381
|
+
# This should match the +type+ set in your serializer.
|
|
382
|
+
#
|
|
383
|
+
# @example
|
|
384
|
+
# class PostResource < ApplicationResource
|
|
385
|
+
# type :posts
|
|
386
|
+
# end
|
|
387
|
+
#
|
|
388
|
+
# @param [Array<Hash>] value Array of sorting criteria
|
|
77
389
|
def self.type(value = nil)
|
|
78
390
|
config[:type] = value
|
|
79
391
|
end
|
|
80
392
|
|
|
393
|
+
# Set an alternative default page number. Defaults to 1.
|
|
394
|
+
# @param [Integer] val The new default
|
|
81
395
|
def self.default_page_number(val)
|
|
82
396
|
config[:default_page_number] = val
|
|
83
397
|
end
|
|
84
398
|
|
|
399
|
+
# Set an alternate default page size, when not specified in query parameters.
|
|
400
|
+
#
|
|
401
|
+
# @example
|
|
402
|
+
# # GET /employees will only render 10 employees
|
|
403
|
+
# default_page_size 10
|
|
404
|
+
#
|
|
405
|
+
# @param [Integer] val The new default page size.
|
|
85
406
|
def self.default_page_size(val)
|
|
86
407
|
config[:default_page_size] = val
|
|
87
408
|
end
|
|
88
409
|
|
|
410
|
+
# This is where we store all information set via DSL.
|
|
411
|
+
# Useful for introspection.
|
|
412
|
+
# Gets dup'd when inherited.
|
|
413
|
+
#
|
|
414
|
+
# @return [Hash] the current configuration
|
|
89
415
|
def self.config
|
|
90
416
|
@config ||= begin
|
|
91
417
|
{
|
|
@@ -102,6 +428,23 @@ module JsonapiCompliable
|
|
|
102
428
|
end
|
|
103
429
|
end
|
|
104
430
|
|
|
431
|
+
# Run code within a given context.
|
|
432
|
+
# Useful for running code within, say, a Rails controller context
|
|
433
|
+
#
|
|
434
|
+
# When using Rails, controller actions are wrapped this way.
|
|
435
|
+
#
|
|
436
|
+
# @example Sinatra
|
|
437
|
+
# get '/api/posts' do
|
|
438
|
+
# resource.with_context self, :index do
|
|
439
|
+
# scope = jsonapi_scope(Tweet.all)
|
|
440
|
+
# render_jsonapi(scope.resolve, scope: false)
|
|
441
|
+
# end
|
|
442
|
+
# end
|
|
443
|
+
#
|
|
444
|
+
# @see Rails
|
|
445
|
+
# @see Base#wrap_context
|
|
446
|
+
# @param object The context (Rails controller or equivalent)
|
|
447
|
+
# @param namespace One of index/show/etc
|
|
105
448
|
def with_context(object, namespace = nil)
|
|
106
449
|
begin
|
|
107
450
|
prior = context
|
|
@@ -112,32 +455,105 @@ module JsonapiCompliable
|
|
|
112
455
|
end
|
|
113
456
|
end
|
|
114
457
|
|
|
458
|
+
# The current context set by +#with_context+ in the form of
|
|
459
|
+
#
|
|
460
|
+
# { object: context_obj, namespace: :index }
|
|
461
|
+
#
|
|
462
|
+
# @see #with_context
|
|
463
|
+
# @return [Hash] the context hash
|
|
115
464
|
def context
|
|
116
465
|
@context || {}
|
|
117
466
|
end
|
|
118
467
|
|
|
468
|
+
# Build a scope using this Resource configuration
|
|
469
|
+
#
|
|
470
|
+
# Essentially "api private", but can be useful for testing.
|
|
471
|
+
#
|
|
472
|
+
# @see Scope
|
|
473
|
+
# @see Query
|
|
474
|
+
# @param base The base scope we are going to chain
|
|
475
|
+
# @param query The relevant Query object
|
|
476
|
+
# @param opts Opts passed to +Scope.new+
|
|
477
|
+
# @return [Scope] a configured Scope instance
|
|
119
478
|
def build_scope(base, query, opts = {})
|
|
120
479
|
Scope.new(base, self, query, opts)
|
|
121
480
|
end
|
|
122
481
|
|
|
482
|
+
# Create the relevant model.
|
|
483
|
+
# You must configure a model (see .model) to create.
|
|
484
|
+
# If you override, you *must* return the created instance.
|
|
485
|
+
#
|
|
486
|
+
# @example Send e-mail on creation
|
|
487
|
+
# def create(attributes)
|
|
488
|
+
# instance = model.create(attributes)
|
|
489
|
+
# UserMailer.welcome_email(instance).deliver_later
|
|
490
|
+
# instance
|
|
491
|
+
# end
|
|
492
|
+
#
|
|
493
|
+
# @see .model
|
|
494
|
+
# @see Adapters::ActiveRecord#create
|
|
495
|
+
# @param [Hash] create_params The relevant attributes, including id and foreign keys
|
|
496
|
+
# @return [Object] an instance of the just-created model
|
|
123
497
|
def create(create_params)
|
|
124
498
|
adapter.create(model, create_params)
|
|
125
499
|
end
|
|
126
500
|
|
|
501
|
+
# Update the relevant model.
|
|
502
|
+
# You must configure a model (see .model) to update.
|
|
503
|
+
# If you override, you *must* return the updated instance.
|
|
504
|
+
#
|
|
505
|
+
# @example Send e-mail on update
|
|
506
|
+
# def update(attributes)
|
|
507
|
+
# instance = model.update_attributes(attributes)
|
|
508
|
+
# UserMailer.profile_updated_email(instance).deliver_later
|
|
509
|
+
# instance
|
|
510
|
+
# end
|
|
511
|
+
#
|
|
512
|
+
# @see .model
|
|
513
|
+
# @see Adapters::ActiveRecord#update
|
|
514
|
+
# @param [Hash] update_params The relevant attributes, including id and foreign keys
|
|
515
|
+
# @return [Object] an instance of the just-created model
|
|
127
516
|
def update(update_params)
|
|
128
517
|
adapter.update(model, update_params)
|
|
129
518
|
end
|
|
130
519
|
|
|
520
|
+
# Destroy the relevant model.
|
|
521
|
+
# You must configure a model (see .model) to destroy.
|
|
522
|
+
# If you override, you *must* return the destroyed instance.
|
|
523
|
+
#
|
|
524
|
+
# @example Send e-mail on destroy
|
|
525
|
+
# def destroy(attributes)
|
|
526
|
+
# instance = model_class.find(id)
|
|
527
|
+
# instance.destroy
|
|
528
|
+
# UserMailer.goodbye_email(instance).deliver_later
|
|
529
|
+
# instance
|
|
530
|
+
# end
|
|
531
|
+
#
|
|
532
|
+
# @see .model
|
|
533
|
+
# @see Adapters::ActiveRecord#destroy
|
|
534
|
+
# @param [String] id The +id+ of the relevant Model
|
|
535
|
+
# @return [Object] an instance of the just-created model
|
|
131
536
|
def destroy(id)
|
|
132
537
|
adapter.destroy(model, id)
|
|
133
538
|
end
|
|
134
539
|
|
|
540
|
+
# @api private
|
|
135
541
|
def persist_with_relationships(meta, attributes, relationships)
|
|
136
542
|
persistence = JsonapiCompliable::Util::Persistence \
|
|
137
543
|
.new(self, meta, attributes, relationships)
|
|
138
544
|
persistence.run
|
|
139
545
|
end
|
|
140
546
|
|
|
547
|
+
# All possible sideload names, including nested names
|
|
548
|
+
#
|
|
549
|
+
# { comments: { author: {} } }
|
|
550
|
+
#
|
|
551
|
+
# Becomes
|
|
552
|
+
#
|
|
553
|
+
# [:comments, :author]
|
|
554
|
+
#
|
|
555
|
+
# @see Sideload#to_hash
|
|
556
|
+
# @return [Array<Symbol>] the list of association names
|
|
141
557
|
def association_names
|
|
142
558
|
@association_names ||= begin
|
|
143
559
|
if sideloading
|
|
@@ -148,6 +564,31 @@ module JsonapiCompliable
|
|
|
148
564
|
end
|
|
149
565
|
end
|
|
150
566
|
|
|
567
|
+
# An Include Directive Hash of all possible sideloads for the current
|
|
568
|
+
# context namespace, taking into account the sideload whitelist.
|
|
569
|
+
#
|
|
570
|
+
# In other words, say we have this resource:
|
|
571
|
+
#
|
|
572
|
+
# class PostResource < ApplicationResource
|
|
573
|
+
# sideload_whitelist({
|
|
574
|
+
# index: :comments,
|
|
575
|
+
# show: { comments: :author }
|
|
576
|
+
# })
|
|
577
|
+
# end
|
|
578
|
+
#
|
|
579
|
+
# Expected behavior:
|
|
580
|
+
#
|
|
581
|
+
# allowed_sideloads(:index) # => { comments: {} }
|
|
582
|
+
# allowed_sideloads(:show) # => { comments: { author: {} }
|
|
583
|
+
#
|
|
584
|
+
# instance.with_context({}, :index) do
|
|
585
|
+
# instance.allowed_sideloads # => { comments: {} }
|
|
586
|
+
# end
|
|
587
|
+
#
|
|
588
|
+
# @see Util::IncludeParams.scrub
|
|
589
|
+
# @see #with_context
|
|
590
|
+
# @param [Symbol] namespace Can be :index/:show/etc - The current context namespace will be used by default.
|
|
591
|
+
# @return [Hash] the scrubbed include directive
|
|
151
592
|
def allowed_sideloads(namespace = nil)
|
|
152
593
|
return {} unless sideloading
|
|
153
594
|
|
|
@@ -159,72 +600,168 @@ module JsonapiCompliable
|
|
|
159
600
|
sideloads
|
|
160
601
|
end
|
|
161
602
|
|
|
603
|
+
# The relevant proc for the given attribute and calculation.
|
|
604
|
+
#
|
|
605
|
+
# @example Custom Stats
|
|
606
|
+
# # Given this configuration
|
|
607
|
+
# allow_stat :rating do
|
|
608
|
+
# average { |scope, attr| ... }
|
|
609
|
+
# end
|
|
610
|
+
#
|
|
611
|
+
# # We'd call the method like
|
|
612
|
+
# resource.stat(:rating, :average)
|
|
613
|
+
# # Which would return the custom proc
|
|
614
|
+
#
|
|
615
|
+
# Raises +JsonapiCompliable::Errors::StatNotFound+ if not corresponding
|
|
616
|
+
# stat has been configured.
|
|
617
|
+
#
|
|
618
|
+
# @see Errors::StatNotFound
|
|
619
|
+
# @param [String, Symbol] attribute The attribute we're calculating.
|
|
620
|
+
# @param [String, Symbol] calculation The calculation to run
|
|
621
|
+
# @return [Proc] the corresponding callable
|
|
162
622
|
def stat(attribute, calculation)
|
|
163
623
|
stats_dsl = stats[attribute] || stats[attribute.to_sym]
|
|
164
624
|
raise Errors::StatNotFound.new(attribute, calculation) unless stats_dsl
|
|
165
625
|
stats_dsl.calculation(calculation)
|
|
166
626
|
end
|
|
167
627
|
|
|
628
|
+
# Interface to the sideloads for this Resource
|
|
629
|
+
# @api private
|
|
168
630
|
def sideloading
|
|
169
631
|
self.class.sideloading
|
|
170
632
|
end
|
|
171
633
|
|
|
634
|
+
# @see .default_sort
|
|
635
|
+
# @api private
|
|
172
636
|
def default_sort
|
|
173
637
|
self.class.config[:default_sort] || [{ id: :asc }]
|
|
174
638
|
end
|
|
175
639
|
|
|
640
|
+
# @see .default_page_number
|
|
641
|
+
# @api private
|
|
176
642
|
def default_page_number
|
|
177
643
|
self.class.config[:default_page_number] || 1
|
|
178
644
|
end
|
|
179
645
|
|
|
646
|
+
# @see .default_page_size
|
|
647
|
+
# @api private
|
|
180
648
|
def default_page_size
|
|
181
649
|
self.class.config[:default_page_size] || 20
|
|
182
650
|
end
|
|
183
651
|
|
|
652
|
+
# Returns :undefined_jsonapi_type when not configured.
|
|
653
|
+
# @see .type
|
|
654
|
+
# @api private
|
|
184
655
|
def type
|
|
185
656
|
self.class.config[:type] || :undefined_jsonapi_type
|
|
186
657
|
end
|
|
187
658
|
|
|
659
|
+
# @see .allow_filter
|
|
660
|
+
# @api private
|
|
188
661
|
def filters
|
|
189
662
|
self.class.config[:filters]
|
|
190
663
|
end
|
|
191
664
|
|
|
665
|
+
# @see .sort
|
|
666
|
+
# @api private
|
|
192
667
|
def sorting
|
|
193
668
|
self.class.config[:sorting]
|
|
194
669
|
end
|
|
195
670
|
|
|
671
|
+
# @see .allow_stat
|
|
672
|
+
# @api private
|
|
196
673
|
def stats
|
|
197
674
|
self.class.config[:stats]
|
|
198
675
|
end
|
|
199
676
|
|
|
677
|
+
# @see .paginate
|
|
678
|
+
# @api private
|
|
200
679
|
def pagination
|
|
201
680
|
self.class.config[:pagination]
|
|
202
681
|
end
|
|
203
682
|
|
|
683
|
+
# @see .extra_field
|
|
684
|
+
# @api private
|
|
204
685
|
def extra_fields
|
|
205
686
|
self.class.config[:extra_fields]
|
|
206
687
|
end
|
|
207
688
|
|
|
689
|
+
# @see .sideload_whitelist
|
|
690
|
+
# @api private
|
|
208
691
|
def sideload_whitelist
|
|
209
692
|
self.class.config[:sideload_whitelist]
|
|
210
693
|
end
|
|
211
694
|
|
|
695
|
+
# @see .default_filter
|
|
696
|
+
# @api private
|
|
212
697
|
def default_filters
|
|
213
698
|
self.class.config[:default_filters]
|
|
214
699
|
end
|
|
215
700
|
|
|
701
|
+
# @see .model
|
|
702
|
+
# @api private
|
|
216
703
|
def model
|
|
217
704
|
self.class.config[:model]
|
|
218
705
|
end
|
|
219
706
|
|
|
707
|
+
# @see .use_adapter
|
|
708
|
+
# @api private
|
|
220
709
|
def adapter
|
|
221
710
|
self.class.config[:adapter]
|
|
222
711
|
end
|
|
223
712
|
|
|
713
|
+
# How do you want to resolve the scope?
|
|
714
|
+
#
|
|
715
|
+
# For ActiveRecord, when we want to actually fire SQL, it's
|
|
716
|
+
# +#to_a+.
|
|
717
|
+
#
|
|
718
|
+
# @example Custom API Call
|
|
719
|
+
# # Let's build a hash and pass it off to an HTTP client
|
|
720
|
+
# class PostResource < ApplicationResource
|
|
721
|
+
# type :posts
|
|
722
|
+
# use_adapter JsonapiCompliable::Adapters::Null
|
|
723
|
+
#
|
|
724
|
+
# sort do |scope, attribute, direction|
|
|
725
|
+
# scope.merge!(order: { attribute => direction }
|
|
726
|
+
# end
|
|
727
|
+
#
|
|
728
|
+
# page do |scope, current_page, per_page|
|
|
729
|
+
# scope.merge!(page: current_page, per_page: per_page)
|
|
730
|
+
# end
|
|
731
|
+
#
|
|
732
|
+
# def resolve(scope)
|
|
733
|
+
# MyHttpClient.get(scope)
|
|
734
|
+
# end
|
|
735
|
+
# end
|
|
736
|
+
#
|
|
737
|
+
# This method *must* return an array of resolved model objects.
|
|
738
|
+
#
|
|
739
|
+
# By default, delegates to the adapter. You likely want to alter your
|
|
740
|
+
# adapter rather than override this directly.
|
|
741
|
+
#
|
|
742
|
+
# @see Adapters::ActiveRecord#resolve
|
|
743
|
+
# @param scope The scope object we've built up
|
|
744
|
+
# @return [Array] array of resolved model objects
|
|
224
745
|
def resolve(scope)
|
|
225
746
|
adapter.resolve(scope)
|
|
226
747
|
end
|
|
227
748
|
|
|
749
|
+
# How to run write requests within a transaction.
|
|
750
|
+
#
|
|
751
|
+
# @example
|
|
752
|
+
# resource.transaction do
|
|
753
|
+
# # ... save calls ...
|
|
754
|
+
# end
|
|
755
|
+
#
|
|
756
|
+
# Should roll back the transaction, but avoid bubbling up the error,
|
|
757
|
+
# if +JsonapiCompliable::Errors::ValidationError+ is raised within
|
|
758
|
+
# the block.
|
|
759
|
+
#
|
|
760
|
+
# By default, delegates to the adapter. You likely want to alter your
|
|
761
|
+
# adapter rather than override this directly.
|
|
762
|
+
#
|
|
763
|
+
# @see Adapters::ActiveRecord#transaction
|
|
764
|
+
# @return the result of +yield+
|
|
228
765
|
def transaction
|
|
229
766
|
response = nil
|
|
230
767
|
begin
|