apique 1.0.0 → 1.1.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c185ec939c0e1327ab39f7a28e5c0d35c3638eb6
4
- data.tar.gz: 446dbe26222728a725f071ff1682a40090f0178a
3
+ metadata.gz: 5ee5c5d499ded1ebb2f79cdc449d84223d09c9b6
4
+ data.tar.gz: d0d2b475eb89a5aec8f5f571fb471548dd03f299
5
5
  SHA512:
6
- metadata.gz: f522d9cedae7c11e5b1dbe3cf275435bb32141398c9648963352da0cb94daafff6d97918a3dbc99d9c4819f1e000d3dfc24948a332ce24d23944f6316f69ee20
7
- data.tar.gz: 0194de1b634a6045f269945e9851c106ecde80358632bcb3f038bfb680547c02cf3ca7ec8b81098e727335429d52ead576350d9bf2bec511b3a4e9c81f439f84
6
+ metadata.gz: 27dc8bfbfbf4dd6571865895d3da2fc6815b7db7544726e5f706d6515e01a77f9e7f9842ccdb19cd66ca4868b307d7d273ca487901abf1d37974730cc6c46d47
7
+ data.tar.gz: b1fe4d5183fb1ee8fcd599c8f107bd171217848c97b12418ce7b087f6acbf114af7399222fd87105719371257a575b5480ed33fb6924865a6b5b331f52d5b125
@@ -3,12 +3,12 @@ module Apique::Basics
3
3
  extend ActiveSupport::Concern
4
4
 
5
5
  included do
6
- protect_from_forgery with: :null_session
7
-
8
6
  before_action :authorize_access
9
7
 
10
- rescue_from CanCan::AccessDenied do |e|
11
- render json: {error: e.message}, status: :forbidden
8
+ if defined? CanCan
9
+ rescue_from CanCan::AccessDenied do |e|
10
+ render json: {error: e.message}, status: :forbidden
11
+ end
12
12
  end
13
13
 
14
14
  rescue_from NotImplementedError do |e|
@@ -39,13 +39,13 @@ module Apique::Basics
39
39
  # The plural name of the ORM class.
40
40
  # @return [String]
41
41
  def collection_name
42
- @collection_name ||= params[:model]
42
+ @collection_name ||= params[:model] || controller_name.underscore.tr('/', '~')
43
43
  end
44
44
 
45
45
  # The ORM class of the resource.
46
46
  # @return [Class]
47
47
  def resource_class
48
- @resource_class ||= collection_name.singularize.classify.constantize
48
+ @resource_class ||= collection_name.singularize.tr('~', '/').classify.constantize
49
49
  end
50
50
 
51
51
  end
@@ -15,6 +15,9 @@ module Apique::Filterable
15
15
  params_types['q'] = [String, Hash]
16
16
  end
17
17
 
18
+ UUID_RE = /^[a-f\d]{8}(-[a-f\d]{4}){3}-[a-f\d]{12}$/
19
+ BOOL_RE = /^(true|false|t|f)$/
20
+ ISO8601_RE = /^\d{4}(-\d\d){2}T\d\d(:\d\d){2}(\.\d+)?Z([+-]\d\d:\d\d)?$/
18
21
 
19
22
  private
20
23
 
@@ -58,29 +61,55 @@ module Apique::Filterable
58
61
  # A call to a scope. A method must be defined as a scope to work on another ORM relation.
59
62
  collection = collection.public_send "search_by_#{k}", v
60
63
  else
61
- is_text = [ActiveRecord::Type::String, ActiveRecord::Type::Text].include? resource_class.columns.find {|i| i.name == k}.cast_type
62
-
63
- if k =~ /_or_/
64
- fields = k.split('_or_')
65
-
66
- if is_text
67
- collection = collection.where [
68
- fields.map {|i| "#{i} ILIKE ?"} * ' OR ',
69
- *(["%#{v}%"] * fields.size)
70
- ]
71
- else
72
- collection = collection.where [
73
- fields.map {|i| "#{i} = ?"} * ' OR ',
74
- *([v] * fields.size)
75
- ]
76
- end
77
- else
78
- if is_text
79
- collection = collection.where ["#{k} ILIKE ?", "%#{v}%"]
80
- else
81
- collection = collection.where k => v
64
+ conditions = k.split('_or_').map do |k|
65
+ case get_cast_type(k)
66
+ when :text
67
+ if v.is_a? Array
68
+ ["#{k} IN (?)", v.select(&:present?)]
69
+ else
70
+ ["#{k} ILIKE ?", "%#{v}%"]
71
+ end
72
+ when :integer
73
+ if v.is_a? Hash
74
+ [:from, :to].each {|x| v[x] &&= v[x].to_i}
75
+ from_to_statement(k, v)
76
+ elsif enum_map = resource_class.defined_enums[k.to_s]
77
+ if v.is_a? Array
78
+ ["#{k} IN (?)", v.map {|vi| enum_map[vi]}]
79
+ else
80
+ ["#{k} = ?", enum_map[v]]
81
+ end
82
+ else
83
+ if v.is_a? Array
84
+ ["#{k} IN (?)", v.map(&:to_i)]
85
+ else
86
+ ["#{k} = ?", v.to_i]
87
+ end
88
+ end
89
+ when :uuid
90
+ if v.is_a? Array
91
+ ["#{k} IN (?)", v.select {|vi| v =~ UUID_RE}]
92
+ else
93
+ if v =~ UUID_RE
94
+ ["#{k} = ?", v]
95
+ end
96
+ end
97
+ when :boolean
98
+ if v =~ BOOL_RE
99
+ ["#{k} = ?", v]
100
+ end
101
+ when :datetime
102
+ [:from, :to].each {|x| v.delete x if v[x] !~ ISO8601_RE}
103
+ from_to_statement(k, v)
82
104
  end
105
+ end.compact
106
+
107
+ if conditions.blank?
108
+ collection = collection.none
109
+ break
83
110
  end
111
+
112
+ collection = collection.where conditions.map(&:first)*' OR ', *conditions.map(&:last)
84
113
  end
85
114
  end
86
115
 
@@ -88,4 +117,39 @@ module Apique::Filterable
88
117
  end
89
118
  end
90
119
 
120
+ if ActiveRecord::VERSION::MAJOR >= 5
121
+ def get_cast_type_class(field)
122
+ resource_class.columns.find {|i| i.name == field}.instance_variable_get(:@cast_type).class
123
+ end
124
+ else
125
+ def get_cast_type_class(field)
126
+ resource_class.columns.find {|i| i.name == field}.cast_type
127
+ end
128
+ end
129
+
130
+ def get_cast_type(field)
131
+ type = get_cast_type_class(field)
132
+ if [ActiveModel::Type::String, ActiveModel::Type::Text].find {|k| type <= k}
133
+ :text
134
+ elsif type <= ActiveModel::Type::Integer
135
+ :integer
136
+ elsif type <= ActiveModel::Type::Boolean
137
+ :boolean
138
+ elsif type <= ActiveModel::Type::DateTime
139
+ :datetime
140
+ elsif type <= ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Uuid
141
+ :uuid
142
+ end
143
+ end
144
+
145
+ def from_to_statement(k, v)
146
+ if v[:from].present? and v[:to].present?
147
+ ["#{k} BETWEEN ? AND ?", v[:from], v[:to]]
148
+ elsif v[:from].present?
149
+ ["#{k} >= ?", v[:from]]
150
+ elsif v[:to].present?
151
+ ["#{k} <= ?", v[:to]]
152
+ end
153
+ end
154
+
91
155
  end
@@ -7,9 +7,17 @@ module Apique::Listable
7
7
  # [Hash: param_key_string => {:desc, :example}]
8
8
  class_attribute :params_usage
9
9
  self.params_usage = {}
10
+ params_usage['f'] = {
11
+ desc: "f [String] serialize the collection in a specific format (optional)"
12
+ }
13
+ params_usage['j'] = {
14
+ desc: "j [Array<String>] join a set of relations (optional)"
15
+ }
10
16
  # [Hash: param_key_string => param_types_array]
11
17
  class_attribute :params_types
12
18
  self.params_types = {}
19
+ params_types['f'] = [String]
20
+ params_types['j'] = [Array]
13
21
 
14
22
  helper_method :subject_collection
15
23
 
@@ -23,7 +31,7 @@ module Apique::Listable
23
31
 
24
32
  # Exception will raise if a request query would contain an incorrect of malformed parameter.
25
33
  # This will provide the front-end with a description of the correct API usage.
26
- class Apique::MalformedParameters < Exception
34
+ class ::Apique::MalformedParameters < Exception
27
35
 
28
36
  def initialize(request, param, params_usage, *args)
29
37
  message = "Bad request query: malformed parameter #{param}"
@@ -48,6 +56,7 @@ module Apique::Listable
48
56
 
49
57
  # GET /api/{plural_resource_name}
50
58
  def index
59
+ join_relations!
51
60
  if respond_to? :filter_collection!, true
52
61
  filter_collection!
53
62
  end
@@ -57,18 +66,24 @@ module Apique::Listable
57
66
  if respond_to? :paginate_collection!, true
58
67
  paginate_collection!
59
68
  end
60
- render json: subject_collection
69
+ render json: subject_collection, apique_format: params[:f] || 'default'
61
70
  end
62
71
 
63
72
 
64
73
  private
65
74
 
66
75
  ### Basic getters-setters ###
76
+
77
+ # The name of ivar to cache the filtered collection.
78
+ # @return [String]
79
+ def collection_variable_name
80
+ @collection_variable_name ||= "@#{collection_name.tr('~', '_')}"
81
+ end
67
82
 
68
83
  # Returns the collection from the created instance variable.
69
84
  # @return [DB collection proxy]
70
85
  def get_collection
71
- instance_variable_get("@#{collection_name}") || set_collection
86
+ instance_variable_get(collection_variable_name) || set_collection
72
87
  end
73
88
 
74
89
  # Used as a helper to create DRYed json builders in inheritable controllers.
@@ -77,13 +92,20 @@ module Apique::Listable
77
92
  # Puts the collection into an instance variable.
78
93
  # In most cases, the collection exactly allowed for the user by CanCan rules should be an entry point.
79
94
  # @return [DB collection proxy]
80
- def set_collection(collection = resource_class.accessible_by(current_ability))
81
- instance_variable_set("@#{collection_name}", collection)
95
+ def set_collection(collection = defined?(CanCan) ? resource_class.accessible_by(current_ability) : resource_class.all)
96
+ instance_variable_set(collection_variable_name, collection)
82
97
  end
83
98
 
84
99
 
85
100
  ### Whitelists ###
86
101
 
102
+ # @virtual
103
+ # Pattern method for relations a user can join. Add whitelisting logic in a descendant using `super`.
104
+ # @return [ActionController::Parameters]
105
+ def join_params
106
+ params[:j].presence || ActionController::Parameters.new
107
+ end
108
+
87
109
  # Check consistency of a query parameters for a list. Use it as a before filter if needed.
88
110
  # @raise [MalformedParameters] if the request query is malformed.
89
111
  def validate_query
@@ -100,4 +122,19 @@ module Apique::Listable
100
122
  end
101
123
  end
102
124
 
125
+
126
+ ### Collection filters ###
127
+
128
+ # Join (eager load) to the current collection the references specified by params[:j]
129
+ # @return [DB collection proxy]
130
+ def join_relations!
131
+ collection = get_collection
132
+
133
+ if join_params.present?
134
+ collection = collection.includes(*join_params)
135
+ end
136
+
137
+ set_collection collection
138
+ end
139
+
103
140
  end
@@ -31,11 +31,17 @@ module Apique::Pickable
31
31
  def resource_name
32
32
  @resource_name ||= collection_name.singularize
33
33
  end
34
+
35
+ # The name of ivar to cache the picked resource.
36
+ # @return [String]
37
+ def resource_variable_name
38
+ @resource_variable_name ||= "@#{resource_name.tr('~', '_')}"
39
+ end
34
40
 
35
41
  # Returns the resource from the created instance variable.
36
42
  # @return [Object]
37
43
  def get_resource
38
- instance_variable_get("@#{resource_name}") || set_resource
44
+ instance_variable_get(resource_variable_name) || set_resource
39
45
  end
40
46
 
41
47
  # Used as a helper to create DRYed json builders in inheritable controllers.
@@ -50,7 +56,7 @@ module Apique::Pickable
50
56
  raise RecordNotFound, "could not find a record of type #{resource_class} with id = #{params[:id]}"
51
57
  end
52
58
 
53
- instance_variable_set("@#{resource_name}", resource)
59
+ instance_variable_set(resource_variable_name, resource)
54
60
  end
55
61
 
56
62
  end
@@ -41,7 +41,7 @@ module Apique::Sortable
41
41
  end
42
42
  end
43
43
  if collection.order_values.blank?
44
- collection = collection.order "id desc"
44
+ collection = collection.order "#{resource_class.primary_key} desc"
45
45
  end
46
46
 
47
47
  set_collection collection
@@ -1,3 +1,3 @@
1
1
  module Apique
2
- VERSION = "1.0.0"
2
+ VERSION = "1.1.2"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: apique
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sergey Baev
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-04-23 00:00:00.000000000 Z
11
+ date: 2017-06-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler