hai 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2055a088c784b6ffa1ef52d9ce4bdcfd41c024742bb070f4776c67ae125e4cdb
4
- data.tar.gz: 81d7c516af333b982b74511b466232cd922e69d892d6577316f8cd5f9842be0c
3
+ metadata.gz: dd1e9a5e790765b1b2030e4384048a2ad08f420e2078b5d883e2bffd2d8dc8eb
4
+ data.tar.gz: 4da9e653d8c708d2e01135aa8cbf70234ec73100a0c158cb3dbd1583a47add28
5
5
  SHA512:
6
- metadata.gz: 244f6fa084d19a44b84d7530708928ba219f9204e3102b59c19c8917368193f02bb7099eb1238f8b870f44839b8d48bb4f0147df134ff3050be9db45133754dd
7
- data.tar.gz: f1e925b0e23c14d019e58a302dffa2bee6b56313757af5540419e370aef8edd0fc08c269d693de10173f1631421a32d787fc43c85bd3f2320fcaab2d2b69ba74
6
+ metadata.gz: dda8bcf71ee7a008b7fac3c6ad0f83b25e3d4fb3c50078b7cae6e1d9d896fa1d3d88b18eacced38fe7ce2e7e6c1efd55c02dc6187934376ab03e7f28f7ed7af4
7
+ data.tar.gz: f125a58684eeba263730397623dd1f3fa58b1168423960c88cb5a0d4f815f6312c18d470d558c8cb8f9a992d542b60637c2086dd4487b0b8737ea48db0c19015
data/Gemfile CHANGED
@@ -10,6 +10,6 @@ gem "rake", "~> 13.0"
10
10
  gem "minitest", "~> 5.0"
11
11
 
12
12
  gem "factory_bot"
13
- gem "graphql"
14
13
  gem "pry"
15
14
  gem "rubocop", "~> 1.21"
15
+ gem "ruby-lsp"
data/Gemfile.lock CHANGED
@@ -1,8 +1,9 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- hai (0.0.1)
4
+ hai (0.0.2)
5
5
  activerecord (~> 7.0)
6
+ graphql (~> 2.0)
6
7
  pg (~> 1.3.5)
7
8
 
8
9
  GEM
@@ -26,12 +27,14 @@ GEM
26
27
  graphql (2.0.9)
27
28
  i18n (1.10.0)
28
29
  concurrent-ruby (~> 1.0)
30
+ language_server-protocol (3.16.0.3)
29
31
  method_source (1.0.0)
30
32
  minitest (5.16.0)
31
33
  parallel (1.21.0)
32
34
  parser (3.1.1.0)
33
35
  ast (~> 2.4.1)
34
36
  pg (1.3.5)
37
+ prettier_print (0.1.0)
35
38
  pry (0.14.1)
36
39
  coderay (~> 1.1)
37
40
  method_source (~> 1.0)
@@ -50,23 +53,32 @@ GEM
50
53
  unicode-display_width (>= 1.4.0, < 3.0)
51
54
  rubocop-ast (1.16.0)
52
55
  parser (>= 3.1.1.0)
56
+ ruby-lsp (0.1.0)
57
+ language_server-protocol
58
+ rubocop (>= 1.0)
59
+ sorbet-runtime
60
+ syntax_tree (>= 2.4)
53
61
  ruby-progressbar (1.11.0)
62
+ sorbet-runtime (0.5.10139)
63
+ syntax_tree (2.8.0)
64
+ prettier_print
54
65
  tzinfo (2.0.4)
55
66
  concurrent-ruby (~> 1.0)
56
67
  unicode-display_width (2.1.0)
57
68
 
58
69
  PLATFORMS
59
70
  x86_64-darwin-21
71
+ x86_64-linux
60
72
 
61
73
  DEPENDENCIES
62
74
  activesupport (~> 7.0)
63
75
  factory_bot
64
- graphql
65
76
  hai!
66
77
  minitest (~> 5.0)
67
78
  pry
68
79
  rake (~> 13.0)
69
80
  rubocop (~> 1.21)
81
+ ruby-lsp
70
82
 
71
83
  BUNDLED WITH
72
84
  2.3.5
data/README.md CHANGED
@@ -1,8 +1,9 @@
1
1
  # Hai
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/hai`. To experiment with that code, run `bin/console` for an interactive prompt.
3
+ The easist way to create a CRUD GraphQL or Rest api with ruby.
4
+ Heavily inspired by [Ash Elixir](https://www.ash-elixir.org/)
4
5
 
5
- TODO: Delete this and the text above, and describe your gem
6
+ Feedback is welcome and appreciated.
6
7
 
7
8
  ## Installation
8
9
 
@@ -16,24 +17,199 @@ And then execute:
16
17
 
17
18
  $ bundle install
18
19
 
19
- Or install it yourself as:
20
+ ## Usage
20
21
 
21
- $ gem install hai
22
+ Hai is a resource based api and those resources are ActiveRecord models. Keeping with this first principle, let's see how it can be used in your Ruby application.
22
23
 
23
- ## Usage
24
+ ## Action Modifications
25
+
26
+ If you want to modify any of the actions, you can add a Actions module to the
27
+ model that you want to modify.
28
+
29
+ ```ruby
30
+ class Post < ApplicationRecord
31
+ belongs_to :user
32
+
33
+ module Actions
34
+ def self.read(query, context)
35
+ query.where(user_id: context[:user].id)
36
+ end
37
+
38
+ def self.list(query, context)
39
+ query.where(user_id: context[:user].id)
40
+ end
41
+
42
+ def self.create(post, context)
43
+ post.user = context[:user]
44
+ end
45
+
46
+ def self.update(post, context)
47
+ post.last_updated_by = context[:user]
48
+ end
49
+ end
50
+ end
51
+ ```
52
+ ## Policies
53
+ Policies are handled in the same manner of Action Modifications. We will use the `Policies` module in the model to handle things like authorization.
54
+
55
+ ```ruby
56
+ class Post < ApplicationRecord
57
+ belongs_to :user
58
+
59
+ module Policies
60
+ def self.read(context)
61
+ context[:user].can?(:read, context[:model])
62
+ end
63
+
64
+ def self.list(query, context)
65
+ context[:user].can?(:list, context[:model])
66
+ end
67
+
68
+ # NOTE: create does a create or update
69
+ def self.create(post, context)
70
+ if post.persisted?
71
+ post.user_id == context[:user].id
72
+ else
73
+ context[:user].can?(:create, context[:model])
74
+ end
75
+ end
76
+
77
+ def self.update(post, context)
78
+ post.user_id == context[:user].id
79
+ end
80
+
81
+ def self.delete(post, context)
82
+ post.user_id == context[:user].id
83
+ end
84
+ end
85
+ end
86
+ ```
87
+
88
+ ## Graphql
89
+
90
+ Hai Graphql depends on `graphql-ruby` so if you don't have that installed and
91
+ boostrapped, head over to [ their repo and do that now ](https://github.com/rmosolgo/graphql-ruby#installation).
92
+
93
+ First, we have to load the Hai Graphql Types with the following snippet of code in your GraphQL::Schema file. Currently, order of operations matters so this needs to be called before the mutation and query class methods.
94
+
95
+ ```ruby
96
+ class MyAppSchema < GraphQL::Schema
97
+ include Hai::GraphQL::Types
98
+ hai_types(User, Post) # comma list of the models you want to expose
99
+
100
+ mutation(Types::MutationType)
101
+ query(Types::QueryType)
102
+ # ...
103
+ end
104
+ ```
105
+
106
+ Now, if we want to add read operations (`readUser` and `listUsers`) complete with filtering, pagination, & sorting, we just have to declare it in the `Types::QueryType` file like so:
107
+
108
+ ```ruby
109
+ module Types
110
+ class QueryType < Types::BaseObject
111
+ # Add `node(id: ID!) and `nodes(ids: [ID!]!)`
112
+ include GraphQL::Types::Relay::HasNodeField
113
+ include GraphQL::Types::Relay::HasNodesField
114
+
115
+ include Hai::GraphQL
116
+ hai_query(User)
117
+ end
118
+ end
119
+ ```
120
+
121
+ Lastly, if you want to add mutations (`createUser`, `updateUser`, & `deleteUser`), you simply declare which models you'd like to expose in the `Types::MutationType` file.
122
+
123
+ ```ruby
124
+ module Types
125
+ class MutationType < Types::BaseObject
126
+ include Hai::GraphQL
127
+ hai_mutation(User)
128
+ end
129
+ end
130
+ ```
131
+
132
+ ## Rest
133
+
134
+ This is even easier than adding Hai Graphql. Hai Rest is a dynamic engine that can be mounted with any namespace. You just have to mount it in your routes file like this:
135
+
136
+ ```ruby
137
+ Rails.application.routes.draw do
138
+ mount Hai::Rest::Engine => "/rest"
139
+ end
140
+ ```
141
+
142
+ Example queries for rest.
143
+ #### List all users
144
+
145
+ Simple use case
146
+
147
+ `GET <base_url>/rest/users`
148
+
149
+ You can also filter:
150
+
151
+ `GET <base_url>/rest/users?filter[name][eq]=bob`
152
+
153
+ Sort
154
+
155
+ `GET <base_url>/rest/users?sort[field]=name&sort[direction]=desc`
156
+
157
+ Paginate
158
+
159
+ `GET <base_url>/rest/users?limit=10&offset=20`
160
+
161
+ Or all things combined
162
+
163
+ `GET <base_url>/rest/users?filter[name][eq]=bob&sort[field]=name&sort[direction]=desc&limit=10&offset=20`
164
+
165
+ #### Read a specific user
166
+
167
+ `GET <base_url>/rest/users/1`
168
+
169
+ #### Create a user
170
+
171
+ `POST <base_url>/rest/users`
172
+
173
+ ```JSON
174
+ {
175
+ "user": {
176
+ "name": "bob"
177
+ }
178
+ }
179
+ ```
180
+
181
+ #### Update a user
182
+ `PUT <base_url>/rest/users/1`
183
+
184
+ ```JSON
185
+ {
186
+ "user": {
187
+ "name": "bob"
188
+ }
189
+ }
190
+ ```
24
191
 
25
- TODO: Write usage instructions here
192
+ #### Delete a user
193
+ `DELETE <base_url>/rest/users/1`
26
194
 
27
195
  ## Development
28
196
 
29
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
197
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run
198
+ `rake test` to run the tests. You can also run `bin/console` for an interactive
199
+ prompt that will allow you to experiment.
30
200
 
31
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
201
+ To install this gem onto your local machine, run `bundle exec rake install`. To
202
+ release a new version, update the version number in `version.rb`, and then run
203
+ `bundle exec rake release`, which will create a git tag for the version, push
204
+ git commits and the created tag, and push the `.gem` file to
205
+ [rubygems.org](https://rubygems.org).
32
206
 
33
207
  ## Contributing
34
208
 
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/hai.
209
+ Bug reports and pull requests are welcome on GitHub at
210
+ https://github.com/[USERNAME]/hai.
36
211
 
37
212
  ## License
38
213
 
39
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
214
+ The gem is available as open source under the terms of the [MIT
215
+ License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,65 @@
1
+ module Hai
2
+ class RestController < ::ApplicationController
3
+ skip_forgery_protection
4
+ def index
5
+ render json:
6
+ Hai::Read
7
+ .new(model_class, context)
8
+ .list(
9
+ # TODO: this is a security risk, thanks co-pilot
10
+ # potenntial use attributes types plus AREL_TYPE_CAST
11
+ filter: params[:filter]&.to_unsafe_h,
12
+ limit: params[:limit],
13
+ offset: params[:offset],
14
+ sort: params[:sort]&.to_unsafe_h
15
+ )
16
+ .to_json
17
+ end
18
+
19
+ def show
20
+ render json:
21
+ Hai::Read
22
+ .new(model_class, context)
23
+ .read(id: { eq: params[:id] })
24
+ .to_json
25
+ end
26
+
27
+ def create
28
+ render json:
29
+ Hai::Create
30
+ .new(model_class, context)
31
+ .execute(**params[params[:model].singularize].permit!)
32
+ .to_json
33
+ end
34
+
35
+ def update
36
+ render json:
37
+ Hai::Update
38
+ .new(model_class, context)
39
+ .execute(
40
+ id: params[:id],
41
+ attributes: params[params[:model]].permit!
42
+ )
43
+ .to_json
44
+ end
45
+
46
+ def destroy
47
+ render json:
48
+ Hai::Delete
49
+ .new(model_class, context)
50
+ .execute(id: params[:id])
51
+ .to_json
52
+ end
53
+
54
+ private
55
+
56
+ def model_class
57
+ params[:model].classify.constantize
58
+ end
59
+
60
+ def read_params
61
+ params.permit! || {}
62
+ # params.permit(:limit, :offset, sort: { :field , :order},filter: {} )
63
+ end
64
+ end
65
+ end
data/config/routes.rb ADDED
@@ -0,0 +1,7 @@
1
+ Hai::Rest::Engine.routes.draw do
2
+ get "/:model", to: "rest#index"
3
+ get "/:model/:id", to: "rest#show"
4
+ post "/:model", to: "rest#create"
5
+ put "/:model/:id", to: "rest#update"
6
+ delete "/:model/:id", to: "rest#destroy"
7
+ end
@@ -0,0 +1,26 @@
1
+ module Hai
2
+ module ActionMods
3
+ def self.included(base)
4
+ base.extend(ClassMethods)
5
+ end
6
+
7
+ def run_action_modification(action, context)
8
+ return unless (action_mod = self.class.action_mods[action])
9
+
10
+ action_mod.call(self, context)
11
+ end
12
+
13
+ module ClassMethods
14
+ def action_mods
15
+ @action_mods ||= {}
16
+ end
17
+
18
+ # TODO: validate CRUD actions
19
+ def action(action, &block)
20
+ action_mods[action] = lambda do |instance, context|
21
+ block.call(instance, context)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
data/lib/hai/create.rb CHANGED
@@ -1,13 +1,51 @@
1
1
  module Hai
2
2
  class Create
3
- attr_accessor :model
3
+ attr_accessor :model, :context
4
4
 
5
- def initialize(model)
5
+ def initialize(model, context)
6
6
  @model = model
7
+ @context = context
8
+ @context[:model] = model
7
9
  end
8
10
 
9
11
  def execute(**attrs)
10
- model.create(**attrs)
12
+ id = attrs.delete(:id)
13
+ instance = id ? model.find(id) : model.new
14
+
15
+ return unauthorized_error unless check_policy(instance)
16
+
17
+ instance.assign_attributes(**attrs)
18
+
19
+ run_action_modification(instance)
20
+
21
+ if instance.save
22
+ { errors: [], result: instance }
23
+ else
24
+ { errors: instance.errors.map(&:full_message), result: nil }
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def unauthorized_error
31
+ { errors: ["UnauthorizedError"], result: nil }
32
+ end
33
+
34
+ def check_policy(instance)
35
+ if model.const_defined?("Policies") &&
36
+ model::Policies.respond_to?(:create)
37
+ model::Policies.create(instance, context)
38
+ else
39
+ true
40
+ end
41
+ end
42
+
43
+ def run_action_modification(instance)
44
+ if model.const_defined?("Actions") && model::Actions.respond_to?(:create)
45
+ model::Actions.create(instance, context)
46
+ else
47
+ instance
48
+ end
11
49
  end
12
50
  end
13
51
  end
data/lib/hai/delete.rb ADDED
@@ -0,0 +1,17 @@
1
+ module Hai
2
+ class Delete
3
+ attr_reader :model, :context
4
+
5
+ def initialize(model, context)
6
+ @model = model
7
+ @context = context
8
+ end
9
+
10
+ def execute(id:)
11
+ record = model.find(id)
12
+ raise UnauthorizedError if record.respond_to?(:check_hai_policy) && record.check_hai_policy(:delete, context)
13
+
14
+ record.destroy
15
+ end
16
+ end
17
+ end
@@ -1,23 +1,42 @@
1
+ require "hai/types/base_create"
2
+
1
3
  module Hai
2
4
  module GraphQL
3
5
  class CreateMutations
4
6
  class << self
5
7
  def add(mutation_type, model)
8
+ define_resolver(model)
6
9
  add_field(mutation_type, model)
7
- define_create_method(mutation_type, model)
8
10
  end
9
11
 
10
- def add_field(mutation_type, model)
11
- mutation_type.field("create_#{model.name.downcase}", "Types::#{model}Type".constantize) do
12
- mutation_type.description("Create a #{model}.")
13
- argument(:attributes, "Types::#{model}Attributes")
12
+ def define_resolver(model)
13
+ klass = Class.new(Hai::GraphQL::Types::BaseCreate)
14
+ klass.send(:graphql_name, "Create#{model}")
15
+ klass.description("Attributes for creating or updating a #{model}.")
16
+ model.attribute_types.each do |attr, type|
17
+ next if %w[id created_at updated_at].include?(attr)
18
+
19
+ klass.argument(
20
+ attr,
21
+ Hai::GraphQL::TYPE_CAST[type.class] ||
22
+ Hai::GraphQL::TYPE_CAST[type.class.superclass],
23
+ required: false
24
+ )
14
25
  end
15
- end
16
26
 
17
- def define_create_method(mutation_type, model)
18
- mutation_type.define_method("create_#{model.name.downcase}") do |attributes:|
19
- Hai::Create.new(model).execute(**attributes.to_h)
27
+ klass.field(:result, ::Types.const_get("#{model}Type"))
28
+
29
+ klass.define_method(:resolve) do |args|
30
+ Hai::Create.new(model, context).execute(**args)
20
31
  end
32
+ Hai::GraphQL::Types.const_set("Create#{model}", klass)
33
+ end
34
+
35
+ def add_field(mutation_type, model)
36
+ mutation_type.field(
37
+ "create_#{model.name.downcase}",
38
+ mutation: Hai::GraphQL::Types.const_get("Create#{model}")
39
+ )
21
40
  end
22
41
  end
23
42
  end
@@ -0,0 +1,25 @@
1
+ module Hai
2
+ module GraphQL
3
+ class DeleteMutations
4
+ class << self
5
+ def add(mutation_type, model)
6
+ add_field(mutation_type, model)
7
+ define_create_method(mutation_type, model)
8
+ end
9
+
10
+ def add_field(mutation_type, model)
11
+ mutation_type.field("delete_#{model.name.downcase}", "Types::#{model}Type".constantize) do
12
+ mutation_type.description("Delete a #{model}.")
13
+ argument(:id, ::GraphQL::Types::ID)
14
+ end
15
+ end
16
+
17
+ def define_create_method(mutation_type, model)
18
+ mutation_type.define_method("delete_#{model.name.downcase}") do |id:|
19
+ Hai::Delete.new(model, context).execute(id: id)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -1,33 +1,40 @@
1
+ require "hai/types/sort_input_type"
2
+
1
3
  module Hai
2
4
  module GraphQL
3
5
  class ListQueries
4
6
  class << self
5
7
  def add(query_type, model)
6
- define_filter_type(model)
7
8
  add_field(query_type, model)
8
9
  define_list_method(query_type, model)
9
10
  end
10
11
 
11
- def define_filter_type(model)
12
- filter_klass = Class.new(::GraphQL::Schema::InputObject)
13
- model.attribute_types.each do |attr, type|
14
- filter_klass.send(:argument, attr, AREL_TYPE_CAST[type.class], required: false)
15
- end
16
- Object.const_set "#{model}FilterInputType", filter_klass
17
- end
18
-
19
12
  def add_field(query_type, model)
20
- query_type.field "list_#{model.name.downcase}", ["Types::#{model}Type".constantize] do
13
+ query_type.field "list_#{model.name.pluralize.downcase}",
14
+ ["Types::#{model}Type".constantize] do
21
15
  query_type.description "List of #{model}."
22
- argument :filter, "#{model}FilterInputType".constantize, required: false
16
+ argument :filter,
17
+ "#{model}FilterInputType".constantize,
18
+ required: false
23
19
  argument :limit, ::GraphQL::Types::Int, required: false
24
20
  argument :offset, ::GraphQL::Types::Int, required: false
21
+ argument :sort, Types::SortInputType, required: false
22
+
23
+ (model.try(:arguments) || []).each do |name, type, kwargs|
24
+ argument(name, type, **kwargs)
25
+ end
25
26
  end
26
27
  end
27
28
 
28
29
  def define_list_method(query_type, model)
29
- query_type.define_method("list_#{model.name.downcase}") do |**args|
30
- Hai::Read.new(model).list(args.transform_values { |v| v.is_a?(Integer) ? v : v.to_h })
30
+ query_type.define_method(
31
+ "list_#{model.name.pluralize.downcase}"
32
+ ) do |**args|
33
+ Hai::Read.new(model, context).list(
34
+ **args.transform_values do |v|
35
+ [Integer, String].include?(v.class) ? v : v.to_h
36
+ end
37
+ )
31
38
  end
32
39
  end
33
40
  end
@@ -8,17 +8,25 @@ module Hai
8
8
  end
9
9
 
10
10
  def add_field(query_type, model)
11
- query_type.field("read_#{model.name.downcase}", "Types::#{model}Type".constantize) do
11
+ query_type.field(
12
+ "read_#{model.name.downcase}",
13
+ "Types::#{model}Type".constantize
14
+ ) do
12
15
  query_type.description("List a single #{model}.")
13
16
  model.attribute_types.each do |attr, type|
14
- argument(attr, Hai::GraphQL::AREL_TYPE_CAST[type.class], required: false)
17
+ argument(
18
+ attr,
19
+ Hai::GraphQL::AREL_TYPE_CAST[type.class] ||
20
+ Hai::GraphQL::Types::Arel::IntInputType,
21
+ required: false
22
+ )
15
23
  end
16
24
  end
17
25
  end
18
26
 
19
27
  def define_read_method(query_type, model)
20
28
  query_type.define_method("read_#{model.name.downcase}") do |**args|
21
- Hai::Read.new(model).read(args.transform_values(&:to_h))
29
+ Hai::Read.new(model, context).read(args.transform_values(&:to_h))
22
30
  end
23
31
  end
24
32
  end