graphiti 1.0.alpha.1 → 1.0.alpha.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +0 -8
  3. data/README.md +2 -72
  4. data/bin/console +1 -1
  5. data/exe/graphiti +5 -0
  6. data/graphiti.gemspec +2 -2
  7. data/lib/generators/jsonapi/resource_generator.rb +1 -1
  8. data/lib/generators/jsonapi/templates/application_resource.rb.erb +0 -2
  9. data/lib/graphiti.rb +17 -1
  10. data/lib/graphiti/adapters/abstract.rb +32 -0
  11. data/lib/graphiti/adapters/active_record/base.rb +8 -0
  12. data/lib/graphiti/adapters/active_record/many_to_many_sideload.rb +2 -9
  13. data/lib/graphiti/cli.rb +45 -0
  14. data/lib/graphiti/configuration.rb +12 -0
  15. data/lib/graphiti/context.rb +1 -0
  16. data/lib/graphiti/errors.rb +105 -3
  17. data/lib/graphiti/filter_operators.rb +13 -3
  18. data/lib/graphiti/query.rb +8 -0
  19. data/lib/graphiti/rails.rb +1 -1
  20. data/lib/graphiti/railtie.rb +21 -2
  21. data/lib/graphiti/renderer.rb +2 -1
  22. data/lib/graphiti/resource.rb +11 -2
  23. data/lib/graphiti/resource/configuration.rb +1 -0
  24. data/lib/graphiti/resource/dsl.rb +4 -3
  25. data/lib/graphiti/resource/interface.rb +15 -0
  26. data/lib/graphiti/resource/links.rb +92 -0
  27. data/lib/graphiti/resource/polymorphism.rb +1 -0
  28. data/lib/graphiti/resource/sideloading.rb +13 -5
  29. data/lib/graphiti/resource_proxy.rb +1 -1
  30. data/lib/graphiti/schema.rb +169 -0
  31. data/lib/graphiti/schema_diff.rb +174 -0
  32. data/lib/graphiti/scoping/filter.rb +1 -1
  33. data/lib/graphiti/sideload.rb +47 -18
  34. data/lib/graphiti/sideload/belongs_to.rb +5 -1
  35. data/lib/graphiti/sideload/has_many.rb +5 -1
  36. data/lib/graphiti/sideload/many_to_many.rb +6 -2
  37. data/lib/graphiti/sideload/polymorphic_belongs_to.rb +3 -1
  38. data/lib/graphiti/types.rb +39 -17
  39. data/lib/graphiti/util/class.rb +22 -0
  40. data/lib/graphiti/util/hash.rb +16 -0
  41. data/lib/graphiti/util/hooks.rb +2 -2
  42. data/lib/graphiti/util/link.rb +48 -0
  43. data/lib/graphiti/util/serializer_relationships.rb +94 -0
  44. data/lib/graphiti/version.rb +1 -1
  45. metadata +16 -7
@@ -0,0 +1,174 @@
1
+ module Graphiti
2
+ class SchemaDiff
3
+ def initialize(old, new)
4
+ @old = old.deep_symbolize_keys
5
+ @new = new.deep_symbolize_keys
6
+ @errors = []
7
+ end
8
+
9
+ def compare
10
+ compare_each if @old != @new
11
+ @errors
12
+ end
13
+
14
+ private
15
+
16
+ def compare_each
17
+ compare_resources
18
+ compare_endpoints
19
+ compare_types
20
+ end
21
+
22
+ def compare_resources
23
+ @old[:resources].each_with_index do |r, index|
24
+ new_resource = @new[:resources].find { |n| n[:name] == r[:name] }
25
+ compare_resource(r, new_resource) do
26
+ compare_attributes(r, new_resource)
27
+ compare_extra_attributes(r, new_resource)
28
+ compare_filters(r, new_resource)
29
+ compare_relationships(r, new_resource)
30
+ end
31
+ end
32
+ end
33
+
34
+ def compare_resource(old_resource, new_resource)
35
+ unless new_resource
36
+ @errors << "#{old_resource[:name]} was removed."
37
+ return
38
+ end
39
+
40
+ if old_resource[:type] != new_resource[:type]
41
+ @errors << "#{old_resource[:name]} changed type from #{old_resource[:type].inspect} to #{new_resource[:type].inspect}."
42
+ end
43
+ yield
44
+ end
45
+
46
+ def compare_attributes(old_resource, new_resource)
47
+ old_resource[:attributes].each_pair do |name, old_att|
48
+ unless new_att = new_resource[:attributes][name]
49
+ @errors << "#{old_resource[:name]}: attribute #{name.inspect} was removed."
50
+ next
51
+ end
52
+
53
+ compare_attribute(old_resource[:name], name, old_att, new_att)
54
+ end
55
+ end
56
+
57
+ def compare_relationships(old_resource, new_resource)
58
+ old_resource[:relationships].each_pair do |name, old_rel|
59
+ unless new_rel = new_resource[:relationships][name]
60
+ @errors << "#{old_resource[:name]}: relationship #{name.inspect} was removed."
61
+ next
62
+ end
63
+
64
+ if new_rel[:resource] != old_rel[:resource]
65
+ @errors << "#{old_resource[:name]}: relationship #{name.inspect} changed resource from #{old_rel[:resource]} to #{new_rel[:resource]}."
66
+ end
67
+
68
+ if new_rel[:type] != old_rel[:type]
69
+ @errors << "#{old_resource[:name]}: relationship #{name.inspect} changed type from #{old_rel[:type].inspect} to #{new_rel[:type].inspect}."
70
+ end
71
+ end
72
+ end
73
+
74
+ def compare_extra_attributes(old_resource, new_resource)
75
+ old_resource[:extra_attributes].each_pair do |name, old_att|
76
+ unless new_att = new_resource[:extra_attributes][name]
77
+ @errors << "#{old_resource[:name]}: extra attribute #{name.inspect} was removed."
78
+ next
79
+ end
80
+
81
+ compare_attribute(old_resource[:name], name, old_att, new_att, extra: true)
82
+ end
83
+ end
84
+
85
+ def compare_filters(old_resource, new_resource)
86
+ old_resource[:filters].each_pair do |name, old_filter|
87
+ unless new_filter = new_resource[:filters][name]
88
+ @errors << "#{old_resource[:name]}: filter #{name.inspect} was removed."
89
+ next
90
+ end
91
+
92
+ if new_filter[:type] != old_filter[:type]
93
+ @errors << "#{old_resource[:name]}: filter #{name.inspect} changed type from #{old_filter[:type].inspect} to #{new_filter[:type].inspect}."
94
+ next
95
+ end
96
+
97
+ if (diff = old_filter[:operators] - new_filter[:operators]).length > 0
98
+ diff.each do |op|
99
+ @errors << "#{old_resource[:name]}: filter #{name.inspect} removed operator #{op.inspect}."
100
+ end
101
+ end
102
+
103
+ if new_filter[:required] && !old_filter[:required]
104
+ @errors << "#{old_resource[:name]}: filter #{name.inspect} went from optional to required."
105
+ end
106
+
107
+ if new_filter[:guard] && !old_filter[:guard]
108
+ @errors << "#{old_resource[:name]}: filter #{name.inspect} went from unguarded to guarded."
109
+ end
110
+ end
111
+ end
112
+
113
+ def compare_endpoints
114
+ @old[:endpoints].each_pair do |path, old_endpoint|
115
+ unless new_endpoint = @new[:endpoints][path]
116
+ @errors << "Endpoint \"#{path}\" was removed."
117
+ next
118
+ end
119
+
120
+ old_endpoint[:actions].each_pair do |name, old_action|
121
+ unless new_action = new_endpoint[:actions][name]
122
+ @errors << "Endpoint \"#{path}\" removed action #{name.inspect}."
123
+ next
124
+ end
125
+
126
+ if new_action[:sideload_whitelist] && !old_action[:sideload_whitelist]
127
+ @errors << "Endpoint \"#{path}\" added sideload whitelist."
128
+ end
129
+
130
+ if new_action[:sideload_whitelist]
131
+ if new_action[:sideload_whitelist] != old_action[:sideload_whitelist]
132
+ removal = Util::Hash.include_removed? \
133
+ new_action[:sideload_whitelist], old_action[:sideload_whitelist]
134
+ if removal
135
+ @errors << "Endpoint \"#{path}\" had incompatible sideload whitelist. Was #{old_action[:sideload_whitelist].inspect}, now #{new_action[:sideload_whitelist].inspect}."
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
142
+
143
+ def compare_types
144
+ @old[:types].each_pair do |name, old_type|
145
+ unless new_type = @new[:types][name]
146
+ @errors << "Type #{name.inspect} was removed."
147
+ next
148
+ end
149
+
150
+ if new_type[:kind] != old_type[:kind]
151
+ @errors << "Type #{name.inspect} changed kind from #{old_type[:kind].inspect} to #{new_type[:kind].inspect}."
152
+ end
153
+ end
154
+ end
155
+
156
+ def compare_attribute(resource_name, att_name, old_att, new_att, extra: false)
157
+ prefix = extra ? "extra attribute" : "attribute"
158
+
159
+ if old_att[:type] != new_att[:type]
160
+ @errors << "#{resource_name}: #{prefix} #{att_name.inspect} changed type from #{old_att[:type].inspect} to #{new_att[:type].inspect}."
161
+ end
162
+
163
+ [:readable, :writable, :sortable].each do |flag|
164
+ if [true, 'guarded'].include?(old_att[flag]) && new_att[flag] == false
165
+ @errors << "#{resource_name}: #{prefix} #{att_name.inspect} changed flag #{flag.inspect} from #{old_att[flag].inspect} to #{new_att[flag].inspect}."
166
+ end
167
+
168
+ if new_att[flag] == 'guarded' && old_att[flag] == true
169
+ @errors << "#{resource_name}: #{prefix} #{att_name.inspect} changed flag #{flag.inspect} from #{old_att[flag].inspect} to #{new_att[flag].inspect}."
170
+ end
171
+ end
172
+ end
173
+ end
174
+ end
@@ -44,7 +44,7 @@ module Graphiti
44
44
  def filter_scope(filter, operator, value)
45
45
  operator = operator.to_s.gsub('!', 'not_').to_sym
46
46
 
47
- if custom_scope = filter.values.first[operator]
47
+ if custom_scope = filter.values[0][:operators][operator]
48
48
  custom_scope.call(@scope, value, resource.context)
49
49
  else
50
50
  filter_via_adapter(filter, operator, value)
@@ -9,13 +9,15 @@ module Graphiti
9
9
  :foreign_key,
10
10
  :primary_key,
11
11
  :parent,
12
- :group_name
12
+ :group_name,
13
+ :link
13
14
 
14
15
  class_attribute :scope_proc,
15
16
  :assign_proc,
16
17
  :assign_each_proc,
17
18
  :params_proc,
18
- :pre_load_proc
19
+ :pre_load_proc,
20
+ :link_proc
19
21
 
20
22
  def initialize(name, opts)
21
23
  @name = name
@@ -28,6 +30,7 @@ module Graphiti
28
30
  @readable = opts[:readable]
29
31
  @writable = opts[:writable]
30
32
  @as = opts[:as]
33
+ @link = opts[:link]
31
34
 
32
35
  # polymorphic-specific
33
36
  @group_name = opts[:group_name]
@@ -36,6 +39,8 @@ module Graphiti
36
39
  if polymorphic_child?
37
40
  parent.resource.polymorphic << resource_class
38
41
  end
42
+
43
+ check! if defined?(::Rails)
39
44
  end
40
45
 
41
46
  def self.scope(&blk)
@@ -58,6 +63,30 @@ module Graphiti
58
63
  self.pre_load_proc = blk
59
64
  end
60
65
 
66
+ def self.link(&blk)
67
+ self.link_proc = blk
68
+ end
69
+
70
+ def check!
71
+ case type
72
+ when :has_many, :has_one
73
+ unless resource.filters[foreign_key]
74
+ raise Errors::MissingSideloadFilter.new parent_resource_class,
75
+ self, foreign_key
76
+ end
77
+ when :belongs_to
78
+ unless resource.filters[primary_key]
79
+ raise Errors::MissingSideloadFilter.new parent_resource_class,
80
+ self, primary_key
81
+ end
82
+ when :many_to_many
83
+ unless resource.filters[true_foreign_key]
84
+ raise Errors::MissingSideloadFilter.new parent_resource_class,
85
+ self, true_foreign_key
86
+ end
87
+ end
88
+ end
89
+
61
90
  def readable?
62
91
  !!@readable
63
92
  end
@@ -66,6 +95,16 @@ module Graphiti
66
95
  !!@writable
67
96
  end
68
97
 
98
+ def link?
99
+ return true if link_proc
100
+
101
+ if @link.nil?
102
+ !!@parent_resource_class.autolink
103
+ else
104
+ !!@link
105
+ end
106
+ end
107
+
69
108
  def polymorphic_parent?
70
109
  resource.polymorphic?
71
110
  end
@@ -116,7 +155,7 @@ module Graphiti
116
155
 
117
156
  def load(parents, query)
118
157
  params = load_params(parents, query)
119
- params_proc.call(params, parents, query) if params_proc
158
+ params_proc.call(params, parents) if params_proc
120
159
  opts = load_options(parents, query)
121
160
  proxy = resource.class._all(params, opts, base_scope)
122
161
  pre_load_proc.call(proxy) if pre_load_proc
@@ -157,8 +196,7 @@ module Graphiti
157
196
  end
158
197
 
159
198
  def resolve(parents, query)
160
- # legacy / custom / many-to-many
161
- if self.class.scope_proc || type == :many_to_many
199
+ if self.class.scope_proc
162
200
  sideload_scope = fire_scope(parents)
163
201
  sideload_scope = Scope.new sideload_scope,
164
202
  resource,
@@ -261,21 +299,12 @@ module Graphiti
261
299
  end
262
300
  end
263
301
 
264
- def namespace_for(klass)
265
- namespace = klass.name
266
- split = namespace.split('::')
267
- split[0,split.length-1].join('::')
302
+ def infer_resource_class
303
+ Util::Class.infer_resource_class(parent_resource.class, name)
268
304
  end
269
305
 
270
- def infer_resource_class
271
- namespace = namespace_for(parent_resource.class)
272
- inferred_name = "#{name.to_s.singularize.classify}Resource"
273
- klass = "#{namespace}::#{inferred_name}".safe_constantize
274
- klass ||= inferred_name.safe_constantize
275
- unless klass
276
- raise Errors::ResourceNotFound.new(parent_resource, name)
277
- end
278
- klass
306
+ def namespace_for(klass)
307
+ Util::Class.namespace_for(klass)
279
308
  end
280
309
  end
281
310
  end
@@ -6,10 +6,14 @@ class Graphiti::Sideload::BelongsTo < Graphiti::Sideload
6
6
  def load_params(parents, query)
7
7
  query.to_hash.tap do |hash|
8
8
  hash[:filter] ||= {}
9
- hash[:filter][primary_key] = ids_for_parents(parents)
9
+ hash[:filter].merge!(base_filter(parents))
10
10
  end
11
11
  end
12
12
 
13
+ def base_filter(parents)
14
+ { primary_key => ids_for_parents(parents).join(',') }
15
+ end
16
+
13
17
  def assign_each(parent, children)
14
18
  children.find { |c| c.send(primary_key) == parent.send(foreign_key) }
15
19
  end
@@ -6,10 +6,14 @@ class Graphiti::Sideload::HasMany < Graphiti::Sideload
6
6
  def load_params(parents, query)
7
7
  query.to_hash.tap do |hash|
8
8
  hash[:filter] ||= {}
9
- hash[:filter][foreign_key] = ids_for_parents(parents)
9
+ hash[:filter].merge!(base_filter(parents))
10
10
  end
11
11
  end
12
12
 
13
+ def base_filter(parents)
14
+ { foreign_key => ids_for_parents(parents).join(',') }
15
+ end
16
+
13
17
  def assign_each(parent, children)
14
18
  children.select { |c| c.send(foreign_key) == parent.send(primary_key) }
15
19
  end
@@ -1,4 +1,4 @@
1
- class Graphiti::Sideload::ManyToMany < Graphiti::Sideload
1
+ class Graphiti::Sideload::ManyToMany < Graphiti::Sideload::HasMany
2
2
  def type
3
3
  :many_to_many
4
4
  end
@@ -11,8 +11,12 @@ class Graphiti::Sideload::ManyToMany < Graphiti::Sideload
11
11
  foreign_key.values.first
12
12
  end
13
13
 
14
+ def base_filter(parents)
15
+ { true_foreign_key => ids_for_parents(parents).join(',') }
16
+ end
17
+
14
18
  def infer_foreign_key
15
- raise 'You must explicitly pass :foreign_key for many-to-many relaitonships, or override in subclass to return a hash.'
19
+ raise 'You must explicitly pass :foreign_key for many-to-many relationships, or override in subclass to return a hash.'
16
20
  end
17
21
 
18
22
  def assign_each(parent, children)
@@ -23,12 +23,14 @@ class Graphiti::Sideload::PolymorphicBelongsTo < Graphiti::Sideload::BelongsTo
23
23
  def on(name, &blk)
24
24
  group = Group.new(name)
25
25
  @groups << group
26
- group.belongs_to(name.to_s.underscore.to_sym)
27
26
  group
28
27
  end
29
28
 
30
29
  def apply(sideload, resource_class)
31
30
  @groups.each do |group|
31
+ if group.calls.empty?
32
+ group.belongs_to(group.name.to_s.underscore.to_sym)
33
+ end
32
34
  group.calls.each do |call|
33
35
  args = call[1]
34
36
  opts = args.extract_options!
@@ -72,6 +72,8 @@ module Graphiti
72
72
  Dry::Types['params.hash'][input].deep_symbolize_keys
73
73
  end
74
74
 
75
+ REQUIRED_KEYS = [:params, :read, :write, :kind, :description]
76
+
75
77
  def self.map
76
78
  @map ||= begin
77
79
  hash = {
@@ -79,52 +81,72 @@ module Graphiti
79
81
  canonical_name: :integer,
80
82
  params: Dry::Types['coercible.integer'],
81
83
  read: Dry::Types['coercible.string'],
82
- write: Dry::Types['coercible.integer']
84
+ write: Dry::Types['coercible.integer'],
85
+ kind: 'scalar',
86
+ description: 'Base Type. Query/persist as integer, render as string.'
83
87
  },
84
88
  string: {
85
89
  params: Dry::Types['coercible.string'],
86
90
  read: Dry::Types['coercible.string'],
87
- write: Dry::Types['coercible.string']
91
+ write: Dry::Types['coercible.string'],
92
+ kind: 'scalar',
93
+ description: 'Base Type.'
88
94
  },
89
95
  integer: {
90
96
  params: PresentInteger,
91
97
  read: Integer,
92
- write: Integer
98
+ write: Integer,
99
+ kind: 'scalar',
100
+ description: 'Base Type.'
93
101
  },
94
102
  big_decimal: {
95
103
  params: ParamDecimal,
96
104
  read: Dry::Types['json.decimal'],
97
- write: Dry::Types['json.decimal']
105
+ write: Dry::Types['json.decimal'],
106
+ kind: 'scalar',
107
+ description: 'Base Type.'
98
108
  },
99
109
  float: {
100
110
  params: Dry::Types['coercible.float'],
101
111
  read: Float,
102
- write: Float
112
+ write: Float,
113
+ kind: 'scalar',
114
+ description: 'Base Type.'
103
115
  },
104
116
  boolean: {
105
117
  params: PresentBool,
106
118
  read: Bool,
107
- write: Bool
119
+ write: Bool,
120
+ kind: 'scalar',
121
+ description: 'Base Type.'
108
122
  },
109
123
  date: {
110
124
  params: PresentDate,
111
125
  read: Date,
112
- write: Date
126
+ write: Date,
127
+ kind: 'scalar',
128
+ description: 'Base Type.'
113
129
  },
114
130
  datetime: {
115
131
  params: PresentParamsDateTime,
116
132
  read: ReadDateTime,
117
- write: WriteDateTime
133
+ write: WriteDateTime,
134
+ kind: 'scalar',
135
+ description: 'Base Type.'
118
136
  },
119
137
  hash: {
120
138
  params: PresentParamsHash,
121
139
  read: Dry::Types['strict.hash'],
122
- write: Dry::Types['strict.hash']
140
+ write: Dry::Types['strict.hash'],
141
+ kind: 'record',
142
+ description: 'Base Type.'
123
143
  },
124
144
  array: {
125
145
  params: Dry::Types['strict.array'],
126
146
  read: Dry::Types['strict.array'],
127
- write: Dry::Types['strict.array']
147
+ write: Dry::Types['strict.array'],
148
+ kind: 'array',
149
+ description: 'Base Type.'
128
150
  }
129
151
  }
130
152
 
@@ -134,12 +156,16 @@ module Graphiti
134
156
 
135
157
  arrays = {}
136
158
  hash.each_pair do |name, map|
159
+ next if [:boolean, :hash, :array].include?(name)
160
+
137
161
  arrays[:"array_of_#{name.to_s.pluralize}"] = {
138
162
  canonical_name: name,
139
163
  params: Dry::Types['strict.array'].of(map[:params]),
140
164
  read: Dry::Types['strict.array'].of(map[:read]),
141
165
  test: Dry::Types['strict.array'].of(map[:test]),
142
- write: Dry::Types['strict.array'].of(map[:write])
166
+ write: Dry::Types['strict.array'].of(map[:write]),
167
+ kind: 'array',
168
+ description: 'Base Type.'
143
169
  }
144
170
  end
145
171
  hash.merge!(arrays)
@@ -153,12 +179,8 @@ module Graphiti
153
179
  end
154
180
 
155
181
  def self.[]=(key, value)
156
- unless value.is_a?(Hash)
157
- value = {
158
- read: value,
159
- params: value,
160
- test: value
161
- }
182
+ unless value.is_a?(Hash) && (REQUIRED_KEYS - value.keys).length.zero?
183
+ raise Errors::InvalidType.new(key, value)
162
184
  end
163
185
  map[key.to_sym] = value
164
186
  end