hai 0.0.2 → 0.0.3

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