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