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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7ca5c32df5a0a64e436b786d26cbe51074d18dc4
4
- data.tar.gz: c9ddad6eb9c3586ec3c7f9568696c7776c508834
3
+ metadata.gz: 736b8601bf09408a0fa6ed8f2a0c148a79f7c285
4
+ data.tar.gz: 9b0adbc770aea3e609f15804db1b33ddba1dee13
5
5
  SHA512:
6
- metadata.gz: 29bbb74fac76a93090f94e08cf38c0021bbd531d31979dbb4a1ba720ebd366df02b7f3b4fa405c1a5a743db8fe85c71cfb608b24610f1d5378d31a769441b24c
7
- data.tar.gz: 022b06dc74a994896b1c6478a716a540f04218071f7cd5baeab44fcb1d2c67a38aef385842ae8aeeb65226db02c70941f33fe8676a6c2d96185a89a2e262d21c
6
+ metadata.gz: 467e32b7154d37819d6bc4598872e66f3a138120ea2781253238ce4b6fc426b416184c79e5ce58a6d9b596b5754cc87bf7ffb2fc02ab2867bdc6eb00edbe8939
7
+ data.tar.gz: c8ec00841469671ef261283f3dd6f556c095742be04d31f1bfa9d633ee5066cd009a790560bc65a61845b6d2db366cff5aa2da7b0226e0a4a2ff7146677c2b78
@@ -10,11 +10,3 @@ install: bundle install --retry=3 --jobs=3
10
10
  gemfile:
11
11
  - gemfiles/rails_4.gemfile
12
12
  - gemfiles/rails_5.gemfile
13
-
14
- deploy:
15
- provider: rubygems
16
- api_key: $RUBYGEMS_API_KEY
17
- gem: graphiti-rb
18
- on:
19
- tags: true
20
- repo: jsonapi-suite/jsonapi_compliable
data/README.md CHANGED
@@ -1,75 +1,5 @@
1
1
  ### Graphiti
2
2
 
3
- [![Build Status](https://travis-ci.org/jsonapi-suite/jsonapi_compliable.svg?branch=master)](https://travis-ci.org/jsonapi-suite/jsonapi_compliable)
3
+ ![Build Status](https://travis-ci.org/graphiti-api/graphiti.svg?branch=master)
4
4
 
5
- [JSONAPI Suite Website](https://jsonapi-suite.github.io/jsonapi_suite)
6
-
7
- [Documentation](https://jsonapi-suite.github.io/jsonapi_compliable)
8
-
9
- Supported Rails versions: >= 4.1
10
-
11
- ### Upgrading to 0.11.x
12
-
13
- Due to a backwards-incompatibility introduced in the underlying
14
- [jsonapi-rb](http://jsonapi-rb.org) gem, specifying custom serializers
15
- now works slightly differently.
16
-
17
- Before:
18
-
19
- ```ruby
20
- # app/serializers/serializable_post.rb
21
-
22
- has_many :comments, class: SerializableSpecialComment
23
- ```
24
-
25
- and/or
26
-
27
- ```ruby
28
- render_jsonapi(post, class: SerializableSpecialPost)
29
- ```
30
-
31
- This is now all handled at the controller level:
32
-
33
- ```ruby
34
- render_jsonapi(post, class: {
35
- Post: SerializableSpecialPost,
36
- Comment: SerializableSpecialComment
37
- })
38
- ```
39
-
40
- ### Upgrading to 0.10
41
-
42
- `sideload_whitelist` has been moved from the resource to the controller:
43
-
44
- ```diff
45
- class PostsController < ApplicationController
46
- jsonapi resource: PostResource do
47
- - sideload_whitelist({ index: [:foo] })
48
- - end
49
- + sideload_whitelist({ index: [:foo] })
50
- end
51
-
52
- # NEW
53
- ```
54
-
55
- ### Running tests
56
-
57
- We support Rails >= 4.1. To do so, we use the [appraisal](https://github.com/thoughtbot/appraisal) gem. So, run:
58
-
59
- ```bash
60
- $ bin/appraisal rails-4 bin/rspec
61
- $ bin/appraisal rails-5 bin/rspec
62
- ```
63
-
64
- Or run tests for all versions:
65
-
66
- ```bash
67
- $ bin/appraisal bin/rspec
68
- ```
69
-
70
- ### Generating the Documentation
71
-
72
- ```bash
73
- $ yard doc
74
- $ yard server
75
- ```
5
+ A stylish alternative to GraphQL.
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require "bundler/setup"
4
- require "graphiti-rb"
4
+ require "graphiti"
5
5
 
6
6
  # You can add fixtures and/or initialization code here to make experimenting
7
7
  # with your gem easier. You can also use a different console, if you like.
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'graphiti/cli'
4
+
5
+ Graphiti::CLI.start(ARGV)
@@ -20,7 +20,7 @@ Gem::Specification.new do |spec|
20
20
  # Pinning this version until backwards-incompatibility is addressed
21
21
  spec.add_dependency 'jsonapi-serializable', '~> 0.3.0'
22
22
  spec.add_dependency 'dry-types', '~> 0.13'
23
- spec.add_dependency 'jsonapi_errorable', '~> 0.9'
23
+ spec.add_dependency 'graphiti_errors', '~> 1.0.alpha.2'
24
24
  spec.add_dependency 'concurrent-ruby', '~> 1.0'
25
25
 
26
26
  spec.add_development_dependency "activerecord", ['>= 4.1', '< 6']
@@ -30,5 +30,5 @@ Gem::Specification.new do |spec|
30
30
  spec.add_development_dependency "sqlite3"
31
31
  spec.add_development_dependency "database_cleaner"
32
32
  spec.add_development_dependency "activemodel", ['>= 4.1', '< 6']
33
- spec.add_development_dependency "jsonapi_spec_helpers", '>= 1.0.alpha.1'
33
+ spec.add_development_dependency "graphiti_spec_helpers", '>= 1.0.alpha.1'
34
34
  end
@@ -1,4 +1,4 @@
1
- module Graphiti
1
+ module Jsonapi
2
2
  class ResourceGenerator < ::Rails::Generators::NamedBase
3
3
  source_root File.expand_path('../templates', __FILE__)
4
4
 
@@ -2,13 +2,11 @@
2
2
  # ApplicationResource is similar to ApplicationRecord - a base class that
3
3
  # holds configuration/methods for subclasses.
4
4
  # All Resources should inherit from ApplicationResource.
5
- # Resource documentation: https://jsonapi-suite.github.io/jsonapi_compliable/JsonapiCompliable/Resource.html
6
5
  <%- end -%>
7
6
  class ApplicationResource < Graphiti::Resource
8
7
  <%- unless omit_comments? -%>
9
8
  # Use the ActiveRecord Adapter for all subclasses.
10
9
  # Subclasses can still override this default.
11
- # More on adapters: https://jsonapi-suite.github.io/jsonapi_compliable/JsonapiCompliable/Adapters/Abstract.html
12
10
  <%- end -%>
13
11
  self.abstract_class = true
14
12
  self.adapter = Graphiti::Adapters::ActiveRecord::Base.new
@@ -7,7 +7,7 @@ require 'active_support/concern'
7
7
  require 'active_support/time'
8
8
 
9
9
  require 'dry-types'
10
- require 'jsonapi_errorable'
10
+ require 'graphiti_errors'
11
11
 
12
12
  require 'jsonapi/serializable'
13
13
 
@@ -17,8 +17,11 @@ require "graphiti/configuration"
17
17
  require "graphiti/context"
18
18
  require "graphiti/errors"
19
19
  require "graphiti/types"
20
+ require "graphiti/schema"
21
+ require "graphiti/schema_diff"
20
22
  require "graphiti/adapters/abstract"
21
23
  require "graphiti/resource/sideloading"
24
+ require "graphiti/resource/links"
22
25
  require "graphiti/resource/configuration"
23
26
  require "graphiti/resource/dsl"
24
27
  require "graphiti/resource/interface"
@@ -56,6 +59,9 @@ require "graphiti/util/sideload"
56
59
  require "graphiti/util/hooks"
57
60
  require "graphiti/util/attribute_check"
58
61
  require "graphiti/util/serializer_attributes"
62
+ require "graphiti/util/serializer_relationships"
63
+ require "graphiti/util/class"
64
+ require "graphiti/util/link"
59
65
 
60
66
  require 'graphiti/adapters/null'
61
67
 
@@ -116,6 +122,16 @@ module Graphiti
116
122
  def self.configure
117
123
  yield config
118
124
  end
125
+
126
+ def self.resources
127
+ @resources ||= []
128
+ end
129
+
130
+ def self.check!
131
+ resources.each do |resource|
132
+ resource.sideloads.values.each(&:check!)
133
+ end
134
+ end
119
135
  end
120
136
 
121
137
  require "graphiti/runner"
@@ -76,6 +76,30 @@ module Graphiti
76
76
  # @see Adapters::ActiveRecordSideloading
77
77
  # @see Adapters::Null
78
78
  class Abstract
79
+ def default_operators
80
+ {
81
+ string: [
82
+ :eq,
83
+ :not_eq,
84
+ :eql,
85
+ :not_eql,
86
+ :prefix,
87
+ :not_prefix,
88
+ :suffix,
89
+ :not_suffix,
90
+ :match,
91
+ :not_match
92
+ ],
93
+ integer_id: numerical_operators,
94
+ integer: numerical_operators,
95
+ big_decimal: numerical_operators,
96
+ float: numerical_operators,
97
+ boolean: [:eq],
98
+ date: numerical_operators,
99
+ datetime: numerical_operators
100
+ }
101
+ end
102
+
79
103
  def filter_string_eq(scope, attribute, value)
80
104
  raise Errors::AdapterNotImplemented.new(self, attribute, :filter_string_eq)
81
105
  end
@@ -378,6 +402,10 @@ module Graphiti
378
402
  scope
379
403
  end
380
404
 
405
+ def belongs_to_many_filter(sideload, scope, value)
406
+ raise 'You must implement #belongs_to_many_filter in an adapter subclass'
407
+ end
408
+
381
409
  def associate_all(parent, children, association_name, association_type)
382
410
  if activerecord_associate?(parent, children[0], association_name)
383
411
  activerecord_adapter.associate_all parent,
@@ -500,6 +528,10 @@ module Graphiti
500
528
 
501
529
  private
502
530
 
531
+ def numerical_operators
532
+ [:eq, :not_eq, :gt, :gte, :lt, :lte]
533
+ end
534
+
503
535
  def activerecord_adapter
504
536
  @activerecord_adapter ||=
505
537
  ::Graphiti::Adapters::ActiveRecord::Base.new
@@ -189,6 +189,14 @@ module Graphiti
189
189
  }
190
190
  end
191
191
 
192
+ def belongs_to_many_filter(sideload, scope, value)
193
+ scope
194
+ .includes(sideload.through_relationship_name)
195
+ .where(sideload.through_table_name => {
196
+ sideload.true_foreign_key => value
197
+ })
198
+ end
199
+
192
200
  def associate_all(parent, children, association_name, association_type)
193
201
  association = parent.association(association_name)
194
202
  association.loaded!
@@ -2,20 +2,13 @@ module Graphiti
2
2
  module Adapters
3
3
  module ActiveRecord
4
4
  class ManyToManySideload < Sideload::ManyToMany
5
- def default_base_scope
6
- resource_class.model.all
7
- end
8
-
9
5
  def through_table_name
10
6
  @through_table_name ||= parent_resource_class.model
11
7
  .reflections[through.to_s].klass.table_name
12
8
  end
13
9
 
14
- def scope(parent_ids)
15
- base_scope
16
- .includes(through)
17
- .where(through_table_name => { true_foreign_key => parent_ids })
18
- .distinct
10
+ def through_relationship_name
11
+ foreign_key.keys.first
19
12
  end
20
13
 
21
14
  def infer_foreign_key
@@ -0,0 +1,45 @@
1
+ require 'thor'
2
+ require 'net/http'
3
+ require 'graphiti'
4
+
5
+ Thor::Base.shell = Thor::Shell::Color
6
+
7
+ module Graphiti
8
+ class CLI < Thor
9
+ desc 'schema_check OLD_SCHEMA NEW_SCHEMA', 'Diff 2 schemas for backwards incompatibilities. Pass file path or URL. If your app relies on JSON Web Tokens, you can set GRAPHITI_TOKEN for authentication'
10
+ def schema_check(old, new)
11
+ old = schema_for(old)
12
+ new = schema_for(new)
13
+
14
+ errors = Graphiti::SchemaDiff.new(old, new).compare
15
+ if errors.any?
16
+ say(set_color("Backwards incompatibilties found!\n", :red, :bold))
17
+ errors.each { |e| say(set_color(e, :yellow)) }
18
+ exit(1)
19
+ else
20
+ say(set_color("No incompatibilities found!", :green))
21
+ exit(0)
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def schema_for(input)
28
+ if input.starts_with?('http')
29
+ JSON.parse(fetch_remote_schema(input))
30
+ else
31
+ JSON.parse(File.read(input))
32
+ end
33
+ end
34
+
35
+ def fetch_remote_schema(path)
36
+ uri = URI(path)
37
+ http = Net::HTTP.new(uri.host, uri.port)
38
+ http.use_ssl = true if uri.scheme == 'https'
39
+ req = Net::HTTP::Get.new(uri)
40
+ req['Authorization'] = "Token token=\"#{ENV['GRAPHITI_TOKEN']}\""
41
+ res = http.request(req)
42
+ res.body
43
+ end
44
+ end
45
+ end
@@ -9,6 +9,9 @@ module Graphiti
9
9
  attr_accessor :concurrency
10
10
 
11
11
  attr_accessor :respond_to
12
+ attr_accessor :context_for_endpoint
13
+ attr_accessor :schema_path
14
+ attr_accessor :links_on_demand
12
15
 
13
16
  # Set defaults
14
17
  # @api private
@@ -16,6 +19,15 @@ module Graphiti
16
19
  @raise_on_missing_sideload = true
17
20
  @concurrency = false
18
21
  @respond_to = [:json, :jsonapi, :xml]
22
+ @links_on_demand = false
23
+
24
+ if defined?(::Rails)
25
+ @schema_path = "#{::Rails.root}/public/schema.json"
26
+ end
27
+ end
28
+
29
+ def schema_path
30
+ @schema_path ||= raise('No schema_path defined! Set Graphiti.config.schema_path to save your schema.')
19
31
  end
20
32
  end
21
33
  end
@@ -10,6 +10,7 @@ module Graphiti
10
10
 
11
11
  included do
12
12
  class_attribute :sideload_whitelist
13
+ self.sideload_whitelist = {}
13
14
  class << self;prepend Overrides;end
14
15
  end
15
16
  end
@@ -16,6 +16,40 @@ The adapter #{@adapter.class} does not implement method '#{@method}', which was
16
16
  end
17
17
  end
18
18
 
19
+ class InvalidLink < Base
20
+ def initialize(resource_class, sideload)
21
+ @resource_class = resource_class
22
+ @sideload = sideload
23
+ end
24
+
25
+ def message
26
+ <<-MSG
27
+ #{@resource_class.name}: Cannot link to sideload #{@sideload.name.inspect}!
28
+
29
+ Make sure the endpoint "#{@sideload.resource.endpoint[:full_path]}" exists, or customize the endpoint for #{@sideload.resource.class.name}.
30
+
31
+ If you do not wish to generate a link, pass link: false or set self.relationship_links_by_default = false.
32
+ MSG
33
+ end
34
+ end
35
+
36
+ class Unlinkable < Base
37
+ def initialize(resource_class, sideload)
38
+ @resource_class = resource_class
39
+ @sideload = sideload
40
+ end
41
+
42
+ def message
43
+ <<-MSG
44
+ #{@resource_class.name}: Tried to link sideload #{@sideload.name.inspect}, but cannot generate links!
45
+
46
+ Graphiti.config.context_for_endpoint must be set to enable link generation:
47
+
48
+ Graphiti.config.context_for_endpoint = ->(path, action) { ... }
49
+ MSG
50
+ end
51
+ end
52
+
19
53
  class AttributeError < Base
20
54
  attr_reader :resource,
21
55
  :name,
@@ -75,6 +109,58 @@ The adapter #{@adapter.class} does not implement method '#{@method}', which was
75
109
  end
76
110
  end
77
111
 
112
+ class InvalidEndpoint < Base
113
+ def initialize(resource_class, path, action)
114
+ @resource_class = resource_class
115
+ @path = path
116
+ @action = action
117
+ end
118
+
119
+ def message
120
+ <<-MSG
121
+ #{@resource_class.name} cannot be called directly from endpoint #{@path}##{@action}!
122
+
123
+ Either set a primary endpoint for this resource:
124
+
125
+ endpoint '/my/url', [:index, :show, :create]
126
+
127
+ Or whitelist a secondary endpoint:
128
+
129
+ secondary_endoint '/my_url', [:index, :update]
130
+
131
+ The current endpoints allowed for this resource are: #{@resource_class.endpoints.inspect}
132
+ MSG
133
+ end
134
+ end
135
+
136
+ class InvalidType < Base
137
+ def initialize(key, value)
138
+ @key = key
139
+ @value = value
140
+ end
141
+
142
+ def message
143
+ "Type must be a Hash with keys #{Types::REQUIRED_KEYS.inspect}"
144
+ end
145
+ end
146
+
147
+ class ResourceEndpointConflict < Base
148
+ def initialize(path, action, resource_a, resource_b)
149
+ @path = path
150
+ @action = action
151
+ @resource_a = resource_a
152
+ @resource_b = resource_b
153
+ end
154
+
155
+ def message
156
+ <<-MSG
157
+ Both '#{@resource_a}' and '#{@resource_b}' are associated to endpoint #{@path}##{@action}!
158
+
159
+ Only one resource can be associated to a given url/verb combination.
160
+ MSG
161
+ end
162
+ end
163
+
78
164
  class PolymorphicChildNotFound < Base
79
165
  def initialize(resource_class, model)
80
166
  @resource_class = resource_class
@@ -209,15 +295,31 @@ end
209
295
  end
210
296
  end
211
297
 
298
+ class MissingSideloadFilter < Base
299
+ def initialize(resource_class, sideload, filter)
300
+ @resource_class = resource_class
301
+ @sideload = sideload
302
+ @filter = filter
303
+ end
304
+
305
+ def message
306
+ <<-MSG
307
+ #{@resource_class.name}: sideload #{@sideload.name.inspect} is associated with resource #{@sideload.resource.class.name}, but it does not have corresponding filter.
308
+
309
+ Expecting filter #{@filter.inspect} on #{@sideload.resource.class.name}.
310
+ MSG
311
+ end
312
+ end
313
+
212
314
  class ResourceNotFound < Base
213
- def initialize(resource, sideload_name)
214
- @resource = resource
315
+ def initialize(resource_class, sideload_name)
316
+ @resource_class = resource_class
215
317
  @sideload_name = sideload_name
216
318
  end
217
319
 
218
320
  def message
219
321
  <<-MSG
220
- Could not find resource class for sideload '#{@sideload_name}' on Resource '#{@resource.class.name}'!
322
+ Could not find resource class for sideload '#{@sideload_name}' on Resource '#{@resource_class.name}'!
221
323
 
222
324
  If this follows a non-standard naming convention, use the :resource option to pass it directly:
223
325