hai 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -5,7 +5,8 @@ require "hai/types/arel/datetime_input_type"
5
5
  module Hai
6
6
  module GraphQL
7
7
  module Types
8
- module Arel; end
8
+ module Arel
9
+ end
9
10
  ALLOWED_TYPES = ::GraphQL::Types.constants - %i[Relay JSON]
10
11
 
11
12
  def self.included(base)
@@ -13,35 +14,109 @@ module Hai
13
14
  end
14
15
 
15
16
  module ClassMethods
16
- def yasashii_type(model)
17
- return if const_defined? "Types::#{model}Type"
17
+ def hai_types(*models)
18
+ base_types = models.map(&method(:define_base_type))
19
+ models.each do |model|
20
+ model.reflections.map(&method(:add_base_type_reflections))
21
+ end
22
+
23
+ models.map(&method(:define_input_object))
24
+ filter_types = models.map(&method(:define_filter_type))
25
+ models.each do |model|
26
+ model.reflections.map(&method(:add_filter_type_reflections))
27
+ end
28
+
29
+ filter_types.each do |filter_type|
30
+ filter_type.send(
31
+ :argument,
32
+ :or,
33
+ "[#{filter_type}]",
34
+ required: false
35
+ )
36
+ end
37
+ end
38
+
39
+ def name_for_base_type(model)
40
+ "Types::#{model}Type"
41
+ end
42
+
43
+ def define_base_type(model)
44
+ return if const_defined? name_for_base_type(model)
18
45
 
19
46
  klass = Class.new(::Types::BaseObject)
20
- model.attribute_types.each do |attr, _type|
21
- klass.send(:field, attr, ::GraphQL::Types::String)
47
+ model.attribute_types.each do |attr, type|
48
+ klass.send(:field, attr, Hai::GraphQL::TYPE_CAST[type.class])
49
+ rescue ArgumentError => e
50
+ binding.pry
22
51
  end
23
52
 
24
53
  ::Types.const_set "#{model}Type", klass
54
+ end
25
55
 
26
- model.reflections.each do |name, ref|
27
- yasashii_type(ref.klass)
28
- if name == ref.plural_name
29
- klass.send(:field, name, "[Types::#{ref.klass}Type]")
30
- else
31
- klass.send(:field, name, "Types::#{ref.klass}Type")
32
- end
56
+ def add_base_type_reflections(name, ref)
57
+ if name == ref.plural_name
58
+ name_for_base_type(ref.active_record).constantize.send(
59
+ :field,
60
+ name,
61
+ "[#{name_for_base_type(ref.klass)}]"
62
+ )
63
+ else
64
+ name_for_base_type(ref.active_record).constantize.send(
65
+ :field,
66
+ name,
67
+ name_for_base_type(ref.klass)
68
+ )
33
69
  end
70
+ rescue NoMethodError => e
71
+ binding.pry
72
+ end
34
73
 
74
+ def define_input_object(model)
35
75
  # input objects
36
76
  klass = Class.new(::Types::BaseInputObject)
37
77
  klass.description("Attributes for creating or updating a #{model}.")
38
78
  model.attribute_types.each do |attr, type|
39
79
  next if %w[id created_at updated_at].include?(attr)
40
80
 
41
- klass.argument(attr, Hai::GraphQL::TYPE_CAST[type.class], required: false)
81
+ klass.argument(
82
+ attr,
83
+ Hai::GraphQL::TYPE_CAST[type.class],
84
+ required: false
85
+ )
42
86
  end
43
87
  ::Types.const_set "#{model}Attributes", klass
44
88
  end
89
+
90
+ def name_for_filter_type(model)
91
+ "#{model}FilterInputType"
92
+ end
93
+
94
+ def define_filter_type(model)
95
+ # Class Filter
96
+ filter_klass = Class.new(::GraphQL::Schema::InputObject)
97
+ model.attribute_types.each do |attr, type|
98
+ filter_klass.send(
99
+ :argument,
100
+ attr,
101
+ AREL_TYPE_CAST[type.class] ||
102
+ AREL_TYPE_CAST[type.class.superclass],
103
+ required: false
104
+ )
105
+ end
106
+
107
+ Object.const_set name_for_filter_type(model), filter_klass
108
+ end
109
+
110
+ def add_filter_type_reflections(name, ref)
111
+ name_for_filter_type(ref.active_record).constantize.send(
112
+ :argument,
113
+ name,
114
+ name_for_filter_type(ref.klass),
115
+ required: false
116
+ )
117
+ rescue NoMethodError, RuntimeError => e
118
+ binding.pry
119
+ end
45
120
  end
46
121
  end
47
122
  end
@@ -0,0 +1,34 @@
1
+ module Hai
2
+ module GraphQL
3
+ class UpdateMutations
4
+ class << self
5
+ def add(mutation_type, model)
6
+ define_resolver(model)
7
+ add_field(mutation_type, model)
8
+ end
9
+
10
+ def define_resolver(model)
11
+ klass = Class.new(Hai::GraphQL::Types::BaseCreate)
12
+ klass.send(:graphql_name, "Update#{model}")
13
+ klass.description("Mutation to Update #{model}.")
14
+
15
+ klass.argument(:attributes, "Types::#{model}Attributes")
16
+ klass.argument(:id, ::GraphQL::Types::ID)
17
+
18
+ klass.field(:result, ::Types.const_get("#{model}Type"))
19
+
20
+ klass.define_method(:resolve) do |id:, attributes:|
21
+ Hai::Update.new(model, context).execute(id: id, attributes: attributes.to_h)
22
+ end
23
+
24
+ Hai::GraphQL::Types.const_set("Update#{model}", klass)
25
+ end
26
+
27
+ def add_field(mutation_type, model)
28
+ mutation_type.field("update_#{model.name.downcase}",
29
+ mutation: Hai::GraphQL::Types.const_get("Update#{model}"))
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
data/lib/hai/graphql.rb CHANGED
@@ -1,34 +1,47 @@
1
1
  require "hai/graphql/read_queries"
2
2
  require "hai/graphql/list_queries"
3
3
  require "hai/graphql/create_mutations"
4
+ require "hai/graphql/update_mutations"
5
+ require "hai/graphql/delete_mutations"
4
6
  require "hai/types/arel/int_input_type"
7
+ require "hai/types/arel/float_input_type"
5
8
  require "hai/types/arel/string_input_type"
6
9
  require "hai/types/arel/datetime_input_type"
10
+ require "hai/types/arel/boolean_input_type"
7
11
 
8
12
  module Hai
9
13
  module GraphQL
10
14
  TYPE_CAST = {
11
15
  ActiveModel::Type::Integer => ::GraphQL::Types::Int,
16
+ ActiveModel::Type::Float => ::GraphQL::Types::Float,
12
17
  ActiveModel::Type::String => ::GraphQL::Types::String,
13
- ActiveRecord::AttributeMethods::TimeZoneConversion::TimeZoneConverter => ::GraphQL::Types::ISO8601DateTime
18
+ ActiveModel::Type::Boolean => ::GraphQL::Types::Boolean,
19
+ ActiveRecord::AttributeMethods::TimeZoneConversion::TimeZoneConverter =>
20
+ ::GraphQL::Types::ISO8601DateTime
14
21
  }.freeze
15
22
  AREL_TYPE_CAST = {
16
23
  ActiveModel::Type::Integer => Hai::GraphQL::Types::Arel::IntInputType,
24
+ ActiveModel::Type::Float => Hai::GraphQL::Types::Arel::FloatInputType,
17
25
  ActiveModel::Type::String => Hai::GraphQL::Types::Arel::StringInputType,
18
- ActiveRecord::AttributeMethods::TimeZoneConversion::TimeZoneConverter => Hai::GraphQL::Types::Arel::DateTimeInputType
26
+ ActiveModel::Type::Boolean => Hai::GraphQL::Types::Arel::BooleanInputType,
27
+ ActiveRecord::AttributeMethods::TimeZoneConversion::TimeZoneConverter =>
28
+ Hai::GraphQL::Types::Arel::DateTimeInputType
19
29
  }.freeze
30
+
20
31
  def self.included(base)
21
32
  base.extend(ClassMethods)
22
33
  end
23
34
 
24
35
  module ClassMethods
25
- def yasashii_query(model)
36
+ def hai_query(model)
26
37
  Hai::GraphQL::ReadQueries.add(self, model)
27
38
  Hai::GraphQL::ListQueries.add(self, model)
28
39
  end
29
40
 
30
- def yasashii_mutation(model)
41
+ def hai_mutation(model)
31
42
  Hai::GraphQL::CreateMutations.add(self, model)
43
+ Hai::GraphQL::UpdateMutations.add(self, model)
44
+ Hai::GraphQL::DeleteMutations.add(self, model)
32
45
  end
33
46
  end
34
47
  end
@@ -0,0 +1,26 @@
1
+ module Hai
2
+ module Policies
3
+ def self.included(base)
4
+ base.extend(ClassMethods)
5
+ end
6
+
7
+ def check_hai_policy(action, context)
8
+ return true unless (policy = self.class.policies[action])
9
+
10
+ policy.call(self, context)
11
+ end
12
+
13
+ module ClassMethods
14
+ def policies
15
+ @policies ||= {}
16
+ end
17
+
18
+ # TODO: validate CRUD actions
19
+ def policy(action, &block)
20
+ policies[action] = lambda do |instance, context|
21
+ block.call(instance, context)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,13 @@
1
+ require "hai"
2
+ require "rails"
3
+
4
+ module Hai
5
+ class Railtie < Rails::Railtie
6
+ railtie_name :hai
7
+
8
+ rake_tasks do
9
+ path = File.expand_path(__dir__)
10
+ Dir.glob("#{path}/tasks/**/*.rake").each { |f| load f }
11
+ end
12
+ end
13
+ end
data/lib/hai/read.rb CHANGED
@@ -1,42 +1,61 @@
1
1
  module Hai
2
2
  class Read
3
3
  attr_accessor :model
4
- attr_reader :table
4
+ attr_reader :table, :context
5
5
 
6
- def initialize(model)
6
+ def initialize(model, context)
7
7
  @model = model
8
+ @context = context
9
+ @context[:model] = model
8
10
  @table = model.arel_table
9
11
  end
10
12
 
11
13
  # return [] or ActiveRecord::Relationship
12
- def list(query_hash)
13
- limit = query_hash.delete(:limit)
14
- offset = query_hash.delete(:offset)
15
- filter = query_hash.delete(:filter)
16
-
17
- reflection_queries = build_reflection_queries(filter) if filter
18
- query = filter.present? ? model.where(build_query(filter)) : model.all
19
- if filter
20
- reflection_queries.each do |ref, q|
21
- query = query.joins(ref).merge(q)
22
- end
23
- end
14
+ def list(filter: nil, limit: nil, offset: nil, sort: nil, **extra)
15
+ check_list_policy(context)
24
16
 
17
+ context[:arguments] = extra
18
+ query = build_filter(filter)
19
+ query = query.order({ sort.fetch(:field) => sort.fetch(:order) }) if sort
25
20
  query = query.limit(limit) if limit
26
21
  query = query.offset(offset) if offset
27
- query
22
+ run_action_modification(query)
28
23
  end
29
24
 
30
25
  # return nil or model
31
26
  def read(query_hash)
32
- model.find_by(build_query(query_hash))
27
+ build_filter(query_hash).first.tap do |record|
28
+ if record.respond_to?(:check_hai_policy) &&
29
+ !record.check_hai_policy(:read, context)
30
+ raise UnauthorizedError
31
+ end
32
+ end
33
33
  end
34
34
 
35
35
  private
36
36
 
37
- # TODO: prolly can remove this
38
- def select_manager
39
- @select_manager ||= Arel::SelectManager.new(table)
37
+ def check_read_policy
38
+ if model.const_defined?("Policies") && model::Policies.respond_to?(:read)
39
+ model::Policies.read(context)
40
+ else
41
+ true
42
+ end
43
+ end
44
+
45
+ def check_list_policy
46
+ if model.const_defined?("Policies") && model::Policies.respond_to?(:list)
47
+ model::Policies.list(context)
48
+ else
49
+ true
50
+ end
51
+ end
52
+
53
+ def run_action_modification(query)
54
+ if model.const_defined?("Actions") && model::Actions.respond_to?(:list)
55
+ model::Actions.list(query, context)
56
+ else
57
+ query
58
+ end
40
59
  end
41
60
 
42
61
  def reflections
@@ -44,21 +63,53 @@ module Hai
44
63
  end
45
64
 
46
65
  def query_reflections(query_hash)
47
- reflections.each_with_object({}) do |(ref, _info), acc|
48
- acc[ref] = query_hash.delete(ref)
49
- end.compact
66
+ reflections
67
+ .each_with_object({}) do |(ref, _info), acc|
68
+ acc[ref] = query_hash.delete(ref)
69
+ end
70
+ .compact
50
71
  end
51
72
 
52
73
  def build_reflection_queries(query_hash)
53
- reflections.each_with_object({}) do |(ref, info), acc|
54
- q_hash = query_hash.delete(ref)
55
- acc[ref] = info.klass.where(where_clause(info.klass.arel_table, q_hash)) if q_hash
56
- end.compact
74
+ reflections
75
+ .each_with_object({}) do |(ref, info), acc|
76
+ q_hash = query_hash.delete(ref)
77
+ acc[ref] = info.klass.where(
78
+ where_clause(info.klass.arel_table, q_hash)
79
+ ) if q_hash
80
+ end
81
+ .compact
57
82
  end
58
83
 
59
- def build_query(query_hash)
60
- or_branch = query_hash.delete(:or)
61
- query = where_clause(model.arel_table, query_hash)
84
+ def build_joins(filter_hash)
85
+ reflections.map do |ref, _|
86
+ if filter_hash
87
+ .keys
88
+ .concat((filter_hash[:or] || []).flat_map(&:keys).uniq)
89
+ .include?(ref)
90
+ ref
91
+ end
92
+ end
93
+ end
94
+
95
+ def build_filter(filter_hash)
96
+ return model.all unless filter_hash.present?
97
+
98
+ joins = build_joins(filter_hash)
99
+ reflection_queries = build_reflection_queries(filter_hash)
100
+ or_branch = filter_hash.delete(:or)
101
+ # build_reflection_queries mutates the filter_hash
102
+ query =
103
+ (
104
+ if filter_hash.present?
105
+ model.where(where_clause(model.arel_table, filter_hash))
106
+ else
107
+ model.all
108
+ end
109
+ )
110
+
111
+ joins.compact.each { |ref| query = query.left_joins(ref) }
112
+ reflection_queries.each { |_ref, q| query = query.merge(q) }
62
113
 
63
114
  return query unless or_branch
64
115
 
@@ -66,7 +117,8 @@ module Hai
66
117
  end
67
118
 
68
119
  def add_sub_query(query, or_branch)
69
- query.or(build_query(or_branch))
120
+ or_branch.each { |q| query = query.or(build_filter(q)) }
121
+ query
70
122
  end
71
123
 
72
124
  def where_clause(table, query_hash)
@@ -79,19 +131,5 @@ module Hai
79
131
  end
80
132
  end
81
133
  end
82
-
83
- def limit; end
84
-
85
- def offset; end
86
-
87
- def having; end
88
-
89
- def group; end
90
-
91
- def order; end
92
-
93
- def select; end
94
-
95
- def distinct; end
96
134
  end
97
135
  end
@@ -0,0 +1,8 @@
1
+ namespace :hai do
2
+ namespace :graphql do
3
+ desc "Generates filter types for a given model"
4
+ task :filter_type do
5
+ puts "Lets freaking go!"
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,2 @@
1
+ namespace :hai do
2
+ end
@@ -0,0 +1,11 @@
1
+ module Hai
2
+ module GraphQL
3
+ module Types
4
+ module Arel
5
+ class BooleanInputType < ::GraphQL::Schema::InputObject
6
+ argument :eq, ::GraphQL::Types::Boolean
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,10 @@
1
+ module Hai
2
+ module GraphQL
3
+ module Types
4
+ module Arel
5
+ class FloatInputType < IntInputType
6
+ end
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,13 @@
1
+
2
+ module Hai
3
+ module GraphQL
4
+ module Types
5
+ module Arel
6
+ class SortInputType < ::GraphQL::Schema::InputObject
7
+ argument :field, ::GraphQL::Types::String # TODO: maybe add "types" dynamically?
8
+ argument :order, ::GraphQL::Types::String # TODO: change to enum DESC||ASC
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,14 @@
1
+ require_relative "./mutation_error_type"
2
+
3
+ module Hai
4
+ module GraphQL
5
+ module Types
6
+ # TODO: make this base class configurable?
7
+ class BaseCreate < ::GraphQL::Schema::RelayClassicMutation
8
+ null true
9
+
10
+ field :errors, [String], null: false
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,11 @@
1
+ module Hai
2
+ module GraphQL
3
+ module Types
4
+ class MutationError < ::GraphQL::Schema::Object
5
+ field :message, String
6
+ field :code, Integer
7
+ field :fields, [String], null: false
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,10 @@
1
+ module Hai
2
+ module GraphQL
3
+ module Types
4
+ class SortInputType < ::GraphQL::Schema::InputObject
5
+ argument :field, ::GraphQL::Types::String # TODO: maybe add "types" dynamically?
6
+ argument :order, ::GraphQL::Types::String # TODO: change to enum DESC||ASC
7
+ end
8
+ end
9
+ end
10
+ end
data/lib/hai/update.rb CHANGED
@@ -1,15 +1,35 @@
1
1
  module Hai
2
- class Create
3
- attr_accessor :model
4
- attr_reader :table
2
+ class Update
3
+ attr_reader :model, :context
5
4
 
6
- def initialize(model)
5
+ def initialize(model, context)
7
6
  @model = model
7
+ @context = context
8
8
  end
9
9
 
10
10
  def execute(id:, attributes:)
11
11
  record = model.find(id)
12
- record.update(**attributes)
12
+ return unauthorized_error unless check_policy(record)
13
+
14
+ if record.update(**attributes)
15
+ { errors: [], result: record }
16
+ else
17
+ { errors: record.errors.map(&:full_message), result: nil }
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def unauthorized_error
24
+ { errors: ["UnauthorizedError"], result: nil }
25
+ end
26
+
27
+ def check_policy(instance)
28
+ if model.const_defined?("Policies") && model::Policies.respond_to?(:update)
29
+ model::Policies.update(instance, context)
30
+ else
31
+ true
32
+ end
13
33
  end
14
34
  end
15
35
  end
data/lib/hai/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hai
4
- VERSION = "0.0.2"
4
+ VERSION = "0.0.3"
5
5
  end
data/lib/hai.rb CHANGED
@@ -1,12 +1,28 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_record"
4
+ # TODO: remove dependency on Graphql
5
+ require "graphql"
6
+ require_relative "hai/graphql"
7
+ require_relative "hai/graphql/types"
8
+
3
9
  require_relative "hai/version"
10
+
4
11
  require_relative "hai/read"
5
12
  require_relative "hai/create"
6
- require_relative "hai/graphql"
7
- require_relative "hai/graphql/types"
13
+ require_relative "hai/update"
14
+ require_relative "hai/delete"
15
+
16
+ require_relative "hai/policies"
17
+ require_relative "hai/action_mods"
18
+ require "hai/railtie" if defined?(Rails)
8
19
 
9
20
  module Hai
10
- class Error < StandardError; end
11
- # Your code goes here...
21
+ class Error < StandardError
22
+ end
23
+ class Rest
24
+ class Engine < ::Rails::Engine
25
+ isolate_namespace Hai
26
+ end
27
+ end
12
28
  end
data/todo.md CHANGED
@@ -12,11 +12,11 @@
12
12
  - [/] create
13
13
  - [/] attributes
14
14
  - [] required or not
15
- - [] update
16
- - [] id + attributes
17
- - [] destroy
18
- - [] id
19
- - [] authorization [current_user]
15
+ - [/] update
16
+ - [/] id + attributes
17
+ - [/] destroy
18
+ - [/] id
19
+ - [/] authorization [current_user]
20
20
 
21
21
  ------------------------------------------
22
22
 
@@ -29,13 +29,20 @@
29
29
  - [/] read
30
30
  - [/] list
31
31
  - [/] relationships
32
+ - [] ActiveRecord::RecordNotFound ?
32
33
  - [/] create
33
- - [ ] callback for current_user/context
34
- - [] update
35
- - [ ] callback for current_user/context
36
- - [] destroy
34
+ - [/] callback for current_user/context
35
+ - [] Errors
36
+ - [/] update
37
+ - [/] callback for policy
38
+ - [] Errors
39
+ - [/] destroy
40
+ - [] Errors
41
+ - [] errors
42
+ - [] subscriptions
37
43
  ------
38
- - [/] gemify
44
+ - [x] gemify
45
+ - [] automate release
39
46
 
40
47
  ## Post gem
41
48
  - [] Multiple or queries