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.
- checksums.yaml +4 -4
- data/.travis.yml +0 -8
- data/README.md +2 -72
- data/bin/console +1 -1
- data/exe/graphiti +5 -0
- data/graphiti.gemspec +2 -2
- data/lib/generators/jsonapi/resource_generator.rb +1 -1
- data/lib/generators/jsonapi/templates/application_resource.rb.erb +0 -2
- data/lib/graphiti.rb +17 -1
- data/lib/graphiti/adapters/abstract.rb +32 -0
- data/lib/graphiti/adapters/active_record/base.rb +8 -0
- data/lib/graphiti/adapters/active_record/many_to_many_sideload.rb +2 -9
- data/lib/graphiti/cli.rb +45 -0
- data/lib/graphiti/configuration.rb +12 -0
- data/lib/graphiti/context.rb +1 -0
- data/lib/graphiti/errors.rb +105 -3
- data/lib/graphiti/filter_operators.rb +13 -3
- data/lib/graphiti/query.rb +8 -0
- data/lib/graphiti/rails.rb +1 -1
- data/lib/graphiti/railtie.rb +21 -2
- data/lib/graphiti/renderer.rb +2 -1
- data/lib/graphiti/resource.rb +11 -2
- data/lib/graphiti/resource/configuration.rb +1 -0
- data/lib/graphiti/resource/dsl.rb +4 -3
- data/lib/graphiti/resource/interface.rb +15 -0
- data/lib/graphiti/resource/links.rb +92 -0
- data/lib/graphiti/resource/polymorphism.rb +1 -0
- data/lib/graphiti/resource/sideloading.rb +13 -5
- data/lib/graphiti/resource_proxy.rb +1 -1
- data/lib/graphiti/schema.rb +169 -0
- data/lib/graphiti/schema_diff.rb +174 -0
- data/lib/graphiti/scoping/filter.rb +1 -1
- data/lib/graphiti/sideload.rb +47 -18
- data/lib/graphiti/sideload/belongs_to.rb +5 -1
- data/lib/graphiti/sideload/has_many.rb +5 -1
- data/lib/graphiti/sideload/many_to_many.rb +6 -2
- data/lib/graphiti/sideload/polymorphic_belongs_to.rb +3 -1
- data/lib/graphiti/types.rb +39 -17
- data/lib/graphiti/util/class.rb +22 -0
- data/lib/graphiti/util/hash.rb +16 -0
- data/lib/graphiti/util/hooks.rb +2 -2
- data/lib/graphiti/util/link.rb +48 -0
- data/lib/graphiti/util/serializer_relationships.rb +94 -0
- data/lib/graphiti/version.rb +1 -1
- metadata +16 -7
@@ -3,8 +3,18 @@ module Graphiti
|
|
3
3
|
class Catchall
|
4
4
|
attr_reader :procs
|
5
5
|
|
6
|
-
def initialize
|
6
|
+
def initialize(resource, type_name, opts)
|
7
7
|
@procs = {}
|
8
|
+
defaults = resource.adapter.default_operators[type_name] || []
|
9
|
+
if opts[:only]
|
10
|
+
defaults = defaults.select { |op| opts[:only].include?(op) }
|
11
|
+
end
|
12
|
+
if opts[:except]
|
13
|
+
defaults = defaults.reject { |op| opts[:except].include?(op) }
|
14
|
+
end
|
15
|
+
defaults.each do |op|
|
16
|
+
@procs[op] = nil
|
17
|
+
end
|
8
18
|
end
|
9
19
|
|
10
20
|
def method_missing(name, *args, &blk)
|
@@ -16,8 +26,8 @@ module Graphiti
|
|
16
26
|
end
|
17
27
|
end
|
18
28
|
|
19
|
-
def self.build(&blk)
|
20
|
-
c = Catchall.new
|
29
|
+
def self.build(resource, type_name, opts = {}, &blk)
|
30
|
+
c = Catchall.new(resource, type_name, opts)
|
21
31
|
c.instance_eval(&blk) if blk
|
22
32
|
c.to_hash
|
23
33
|
end
|
data/lib/graphiti/query.rb
CHANGED
@@ -21,6 +21,14 @@ module Graphiti
|
|
21
21
|
not association?
|
22
22
|
end
|
23
23
|
|
24
|
+
def links?
|
25
|
+
if Graphiti.config.links_on_demand
|
26
|
+
[true, 'true'].include?(@params[:links])
|
27
|
+
else
|
28
|
+
true
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
24
32
|
def to_hash
|
25
33
|
{}.tap do |hash|
|
26
34
|
hash[:filter] = filters unless filters.empty?
|
data/lib/graphiti/rails.rb
CHANGED
data/lib/graphiti/railtie.rb
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
module Graphiti
|
2
2
|
class Railtie < ::Rails::Railtie
|
3
|
-
|
4
3
|
initializer "graphiti.require_activerecord_adapter" do
|
5
4
|
config.after_initialize do |app|
|
6
5
|
ActiveSupport.on_load(:active_record) do
|
@@ -16,6 +15,7 @@ module Graphiti
|
|
16
15
|
register_parameter_parser
|
17
16
|
register_renderers
|
18
17
|
establish_concurrency
|
18
|
+
configure_endpoint_lookup
|
19
19
|
end
|
20
20
|
|
21
21
|
# from jsonapi-rails
|
@@ -55,7 +55,7 @@ module Graphiti
|
|
55
55
|
::ActionController::Renderers.add(:jsonapi_errors) do |proxy, options|
|
56
56
|
self.content_type ||= Mime[:jsonapi]
|
57
57
|
|
58
|
-
validation =
|
58
|
+
validation = GraphitiErrors::Serializers::Validation.new \
|
59
59
|
proxy.data, proxy.payload.relationships
|
60
60
|
|
61
61
|
render \
|
@@ -70,5 +70,24 @@ module Graphiti
|
|
70
70
|
Graphiti.config.concurrency = !::Rails.env.test? &&
|
71
71
|
::Rails.application.config.cache_classes
|
72
72
|
end
|
73
|
+
|
74
|
+
def configure_endpoint_lookup
|
75
|
+
Graphiti.config.context_for_endpoint = ->(path, action) {
|
76
|
+
method = :GET
|
77
|
+
case action
|
78
|
+
when :show then path = "#{path}/1"
|
79
|
+
when :create then method = :POST
|
80
|
+
when :update
|
81
|
+
path = "#{path}/1"
|
82
|
+
method = :PUT
|
83
|
+
when :destroy
|
84
|
+
path = "#{path}/1"
|
85
|
+
method = :DELETE
|
86
|
+
end
|
87
|
+
|
88
|
+
route = ::Rails.application.routes.recognize_path(path, method: method) rescue nil
|
89
|
+
"#{route[:controller]}_controller".classify.safe_constantize if route
|
90
|
+
}
|
91
|
+
end
|
73
92
|
end
|
74
93
|
end
|
data/lib/graphiti/renderer.rb
CHANGED
@@ -33,6 +33,7 @@ module Graphiti
|
|
33
33
|
options[:fields] = proxy.fields
|
34
34
|
options[:expose] ||= {}
|
35
35
|
options[:expose][:extra_fields] = proxy.extra_fields
|
36
|
+
options[:expose][:proxy] = proxy
|
36
37
|
options[:include] = proxy.include_hash
|
37
38
|
options[:meta] ||= {}
|
38
39
|
options[:meta].merge!(stats: proxy.stats) unless proxy.stats.empty?
|
@@ -45,7 +46,7 @@ module Graphiti
|
|
45
46
|
def notify
|
46
47
|
if defined?(ActiveSupport::Notifications)
|
47
48
|
opts = [
|
48
|
-
'render.
|
49
|
+
'render.graphiti',
|
49
50
|
records: records,
|
50
51
|
options: options
|
51
52
|
]
|
data/lib/graphiti/resource.rb
CHANGED
@@ -4,6 +4,7 @@ module Graphiti
|
|
4
4
|
include Interface
|
5
5
|
include Configuration
|
6
6
|
include Sideloading
|
7
|
+
include Links
|
7
8
|
|
8
9
|
attr_reader :context
|
9
10
|
|
@@ -21,14 +22,22 @@ module Graphiti
|
|
21
22
|
end
|
22
23
|
end
|
23
24
|
|
24
|
-
def context
|
25
|
+
def self.context
|
25
26
|
Graphiti.context[:object]
|
26
27
|
end
|
27
28
|
|
28
|
-
def
|
29
|
+
def context
|
30
|
+
self.class.context
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.context_namespace
|
29
34
|
Graphiti.context[:namespace]
|
30
35
|
end
|
31
36
|
|
37
|
+
def context_namespace
|
38
|
+
self.class.context_namespace
|
39
|
+
end
|
40
|
+
|
32
41
|
def build_scope(base, query, opts = {})
|
33
42
|
Scope.new(base, self, query, opts)
|
34
43
|
end
|
@@ -9,11 +9,12 @@ module Graphiti
|
|
9
9
|
|
10
10
|
if att = get_attr(name, :filterable, raise_error: :only_unsupported)
|
11
11
|
aliases = [name, opts[:aliases]].flatten.compact
|
12
|
-
operators = FilterOperators.build(&blk)
|
12
|
+
operators = FilterOperators.build(self, att[:type], opts, &blk)
|
13
13
|
config[:filters][name.to_sym] = {
|
14
14
|
aliases: aliases,
|
15
|
-
type: att[:type]
|
16
|
-
|
15
|
+
type: att[:type],
|
16
|
+
operators: operators.to_hash
|
17
|
+
}
|
17
18
|
else
|
18
19
|
if type = args[0]
|
19
20
|
attribute name, type, only: [:filterable]
|
@@ -5,15 +5,18 @@ module Graphiti
|
|
5
5
|
|
6
6
|
class_methods do
|
7
7
|
def all(params = {}, base_scope = nil)
|
8
|
+
validate!
|
8
9
|
_all(params, {}, base_scope)
|
9
10
|
end
|
10
11
|
|
12
|
+
# @api private
|
11
13
|
def _all(params, opts, base_scope)
|
12
14
|
runner = Runner.new(self, params)
|
13
15
|
runner.proxy(base_scope, opts)
|
14
16
|
end
|
15
17
|
|
16
18
|
def find(params, base_scope = nil)
|
19
|
+
validate!
|
17
20
|
id = params[:data].try(:[], :id) || params.delete(:id)
|
18
21
|
params[:filter] ||= {}
|
19
22
|
params[:filter].merge!(id: id)
|
@@ -23,9 +26,21 @@ module Graphiti
|
|
23
26
|
end
|
24
27
|
|
25
28
|
def build(params, base_scope = nil)
|
29
|
+
validate!
|
26
30
|
runner = Runner.new(self, params)
|
27
31
|
runner.proxy(base_scope, single: true, raise_on_missing: true)
|
28
32
|
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def validate!
|
37
|
+
if context && context.respond_to?(:request)
|
38
|
+
path = context.request.env['PATH_INFO']
|
39
|
+
unless allow_request?(path, context_namespace)
|
40
|
+
raise Errors::InvalidEndpoint.new(self, path, context_namespace)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
29
44
|
end
|
30
45
|
end
|
31
46
|
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
module Graphiti
|
2
|
+
module Links
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
DEFAULT_ACTIONS = [:index, :show, :create, :update, :destroy].freeze
|
6
|
+
|
7
|
+
module Overrides
|
8
|
+
def endpoint
|
9
|
+
if endpoint = super
|
10
|
+
endpoint
|
11
|
+
else
|
12
|
+
self.endpoint = infer_endpoint
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
included do
|
18
|
+
class_attribute :endpoint,
|
19
|
+
:base_url,
|
20
|
+
:endpoint_namespace,
|
21
|
+
:secondary_endpoints,
|
22
|
+
:autolink
|
23
|
+
self.secondary_endpoints = []
|
24
|
+
self.autolink = true
|
25
|
+
|
26
|
+
class << self
|
27
|
+
prepend Overrides
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class_methods do
|
32
|
+
def infer_endpoint
|
33
|
+
return unless name
|
34
|
+
|
35
|
+
path = "/#{name.gsub('Resource', '').pluralize.underscore}".to_sym
|
36
|
+
{
|
37
|
+
path: path,
|
38
|
+
full_path: full_path_for(path),
|
39
|
+
url: url_for(path),
|
40
|
+
actions: DEFAULT_ACTIONS.dup
|
41
|
+
}
|
42
|
+
end
|
43
|
+
|
44
|
+
def primary_endpoint(path, actions = DEFAULT_ACTIONS.dup)
|
45
|
+
path = path.to_sym
|
46
|
+
self.endpoint = {
|
47
|
+
path: path,
|
48
|
+
full_path: full_path_for(path),
|
49
|
+
url: url_for(path),
|
50
|
+
actions: actions
|
51
|
+
}
|
52
|
+
end
|
53
|
+
|
54
|
+
# NB: avoid << b/c class_attribute
|
55
|
+
def secondary_endpoint(path, actions = DEFAULT_ACTIONS.dup)
|
56
|
+
path = path.to_sym
|
57
|
+
self.secondary_endpoints += [{
|
58
|
+
path: path,
|
59
|
+
full_path: full_path_for(path),
|
60
|
+
url: url_for(path),
|
61
|
+
actions: actions
|
62
|
+
}]
|
63
|
+
end
|
64
|
+
|
65
|
+
def endpoints
|
66
|
+
([endpoint] + secondary_endpoints).compact
|
67
|
+
end
|
68
|
+
|
69
|
+
def allow_request?(path, action)
|
70
|
+
endpoints.any? do |e|
|
71
|
+
if [:update, :show, :destroy].include?(context_namespace)
|
72
|
+
path = path.split('/')
|
73
|
+
path.pop
|
74
|
+
path = path.join('/')
|
75
|
+
end
|
76
|
+
|
77
|
+
e[:full_path].to_s == path && e[:actions].include?(context_namespace)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def full_path_for(path)
|
84
|
+
[endpoint_namespace, path].join('').to_sym
|
85
|
+
end
|
86
|
+
|
87
|
+
def url_for(path)
|
88
|
+
[base_url, full_path_for(path)].join('').to_sym
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -23,11 +23,7 @@ module Graphiti
|
|
23
23
|
end
|
24
24
|
|
25
25
|
def apply_sideloads_to_serializer
|
26
|
-
config[:sideloads].
|
27
|
-
if serializer.relationship_blocks[name].nil? && sideload.readable?
|
28
|
-
serializer.relationship(name)
|
29
|
-
end
|
30
|
-
end
|
26
|
+
Util::SerializerRelationships.new(self, config[:sideloads]).apply
|
31
27
|
end
|
32
28
|
|
33
29
|
def has_many(name, opts = {}, &blk)
|
@@ -61,6 +57,18 @@ module Graphiti
|
|
61
57
|
allow_sideload(name, opts, &blk)
|
62
58
|
end
|
63
59
|
|
60
|
+
def belongs_to_many(name, resource: nil, as:, &blk)
|
61
|
+
resource = resource ||= Util::Class.infer_resource_class(self, name)
|
62
|
+
sideload = resource.sideload(as)
|
63
|
+
|
64
|
+
_adapter = adapter
|
65
|
+
filter sideload.true_foreign_key, resource.attributes[:id][:type] do
|
66
|
+
eq do |scope, value|
|
67
|
+
_adapter.belongs_to_many_filter(sideload, scope, value)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
64
72
|
def sideload(name)
|
65
73
|
sideloads[name]
|
66
74
|
end
|
@@ -2,7 +2,7 @@ module Graphiti
|
|
2
2
|
class ResourceProxy
|
3
3
|
include Enumerable
|
4
4
|
|
5
|
-
attr_reader :resource, :query, :scope
|
5
|
+
attr_reader :resource, :query, :scope, :payload
|
6
6
|
|
7
7
|
def initialize(resource, scope, query, payload: nil, single: false, raise_on_missing: false)
|
8
8
|
@resource = resource
|
@@ -0,0 +1,169 @@
|
|
1
|
+
module Graphiti
|
2
|
+
class Schema
|
3
|
+
attr_reader :resources
|
4
|
+
|
5
|
+
def self.generate(resources = nil)
|
6
|
+
::Rails.application.eager_load! if defined?(::Rails)
|
7
|
+
resources ||= Graphiti.resources.reject(&:abstract_class?)
|
8
|
+
new(resources).generate
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.generate!(resources = nil)
|
12
|
+
schema = generate(resources)
|
13
|
+
|
14
|
+
if ENV['FORCE_SCHEMA'] != 'true' && File.exists?(Graphiti.config.schema_path)
|
15
|
+
old = JSON.parse(File.read(Graphiti.config.schema_path))
|
16
|
+
errors = Graphiti::SchemaDiff.new(old, schema).compare
|
17
|
+
return errors if errors.any?
|
18
|
+
end
|
19
|
+
File.write(Graphiti.config.schema_path, JSON.pretty_generate(schema))
|
20
|
+
[]
|
21
|
+
end
|
22
|
+
|
23
|
+
def initialize(resources)
|
24
|
+
@resources = resources
|
25
|
+
end
|
26
|
+
|
27
|
+
def generate
|
28
|
+
{
|
29
|
+
resources: generate_resources,
|
30
|
+
endpoints: generate_endpoints,
|
31
|
+
types: generate_types
|
32
|
+
}
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def generate_types
|
38
|
+
{}.tap do |types|
|
39
|
+
Graphiti::Types.map.each_pair do |name, config|
|
40
|
+
types[name] = config.slice(:kind, :description)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def generate_endpoints
|
46
|
+
{}.tap do |endpoints|
|
47
|
+
@resources.each do |r|
|
48
|
+
r.endpoints.each do |e|
|
49
|
+
actions = {}
|
50
|
+
e[:actions].each do |a|
|
51
|
+
next unless ctx = context_for(e[:full_path], a)
|
52
|
+
|
53
|
+
existing = endpoints[e[:full_path]]
|
54
|
+
if existing && config = existing[:actions][a]
|
55
|
+
raise Errors::ResourceEndpointConflict.new \
|
56
|
+
e[:full_path], a, r.name, config[:resource]
|
57
|
+
end
|
58
|
+
|
59
|
+
actions[a] = { resource: r.name }
|
60
|
+
if whitelist = ctx.sideload_whitelist
|
61
|
+
if whitelist[a]
|
62
|
+
actions[a].merge!(sideload_whitelist: whitelist[a])
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
unless actions.empty?
|
68
|
+
endpoints[e[:full_path]] ||= { actions: {} }
|
69
|
+
endpoints[e[:full_path]][:actions].merge!(actions)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def context_for(path, action)
|
77
|
+
Graphiti.config.context_for_endpoint.call(path.to_s, action)
|
78
|
+
end
|
79
|
+
|
80
|
+
def generate_resources
|
81
|
+
@resources.map do |r|
|
82
|
+
config = {
|
83
|
+
name: r.name,
|
84
|
+
type: r.type.to_s,
|
85
|
+
attributes: attributes(r),
|
86
|
+
extra_attributes: extra_attributes(r),
|
87
|
+
filters: filters(r),
|
88
|
+
relationships: relationships(r)
|
89
|
+
}
|
90
|
+
|
91
|
+
if r.polymorphic?
|
92
|
+
config.merge!(polymorphic: true, children: r.children.map(&:name))
|
93
|
+
end
|
94
|
+
|
95
|
+
config
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def attributes(resource)
|
100
|
+
{}.tap do |attrs|
|
101
|
+
resource.attributes.each_pair do |name, config|
|
102
|
+
if config.values_at(:readable, :writable, :sortable).any?
|
103
|
+
attrs[name] = {
|
104
|
+
type: config[:type].to_s,
|
105
|
+
readable: flag(config[:readable]),
|
106
|
+
writable: flag(config[:writable]),
|
107
|
+
sortable: flag(config[:sortable])
|
108
|
+
}
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def extra_attributes(resource)
|
115
|
+
{}.tap do |attrs|
|
116
|
+
resource.extra_attributes.each_pair do |name, config|
|
117
|
+
attrs[name] = {
|
118
|
+
type: config[:type].to_s,
|
119
|
+
readable: flag(config[:readable])
|
120
|
+
}
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def flag(value)value
|
126
|
+
if value.is_a?(Symbol)
|
127
|
+
'guarded'
|
128
|
+
else
|
129
|
+
!!value
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def filters(resource)
|
134
|
+
{}.tap do |f|
|
135
|
+
resource.filters.each_pair do |name, config|
|
136
|
+
config = {
|
137
|
+
type: config[:type].to_s,
|
138
|
+
operators: config[:operators].keys.map(&:to_s)
|
139
|
+
}
|
140
|
+
attr = resource.attributes[name]
|
141
|
+
if attr[:filterable].is_a?(Symbol)
|
142
|
+
if attr[:filterable] == :required
|
143
|
+
config[:required] = true
|
144
|
+
else
|
145
|
+
config[:guard] = true
|
146
|
+
end
|
147
|
+
end
|
148
|
+
f[name] = config
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def relationships(resource)
|
154
|
+
{}.tap do |r|
|
155
|
+
resource.sideloads.each_pair do |name, config|
|
156
|
+
schema = { type: config.type.to_s }
|
157
|
+
if config.type == :polymorphic_belongs_to
|
158
|
+
schema[:resources] = config.children.values
|
159
|
+
.map(&:resource).map(&:class).map(&:name)
|
160
|
+
else
|
161
|
+
schema[:resource] = config.resource.class.name
|
162
|
+
end
|
163
|
+
|
164
|
+
r[name] = schema
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|