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

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.
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