hai 0.0.2 → 0.0.4

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: f748b48e465a88f0d1ff069c711d9ea21e0ac855e6a261bb29cf0a8e52e75585
4
+ data.tar.gz: ad117a23c477fec0ec5494b7bda4d1143a4698fbd7796cffebbb0995d25571db
5
5
  SHA512:
6
- metadata.gz: 244f6fa084d19a44b84d7530708928ba219f9204e3102b59c19c8917368193f02bb7099eb1238f8b870f44839b8d48bb4f0147df134ff3050be9db45133754dd
7
- data.tar.gz: f1e925b0e23c14d019e58a302dffa2bee6b56313757af5540419e370aef8edd0fc08c269d693de10173f1631421a32d787fc43c85bd3f2320fcaab2d2b69ba74
6
+ metadata.gz: bfa8f4be0c3bbeb2719113b19a25a8b5846ff0013daa93f8f9c965463c2a26776ef1044b2628c7e39d0d77be069089be729a1351a5d697a2d71f8b1b2c91df1a
7
+ data.tar.gz: 0a07c759c76f2fc8dd347be3f3e5b1760a7eacead3dd199abae296bf0aa608e172916d0a419f4021732f33d7db3f8c5de9cb196dcd14aacd56f7277e723a510e
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,211 @@ 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
+ <details>
25
+ <summary>Action Modifications </summary>
26
+
27
+ If you want to modify any of the actions, you can add a Actions module to the
28
+ model that you want to modify.
29
+
30
+ ```ruby
31
+ class Post < ApplicationRecord
32
+ belongs_to :user
33
+
34
+ module Actions
35
+ def self.read(query, context)
36
+ query.where(user_id: context[:user].id)
37
+ end
38
+
39
+ def self.list(query, context)
40
+ query.where(user_id: context[:user].id)
41
+ end
42
+
43
+ def self.create(post, context)
44
+ post.user = context[:user]
45
+ end
46
+
47
+ def self.update(post, context)
48
+ post.last_updated_by = context[:user]
49
+ end
50
+ end
51
+ end
52
+ ```
53
+ </details>
54
+
55
+ <details>
56
+ <summary> Policies</summary>
57
+ Policies are handled in the same manner of Action Modifications. We will use the `Policies` module in the model to handle things like authorization.
58
+
59
+ ```ruby
60
+ class Post < ApplicationRecord
61
+ belongs_to :user
62
+
63
+ module Policies
64
+ def self.read(context)
65
+ context[:user].can?(:read, context[:model])
66
+ end
67
+
68
+ def self.list(query, context)
69
+ context[:user].can?(:list, context[:model])
70
+ end
71
+
72
+ # NOTE: create does a create or update
73
+ def self.create(post, context)
74
+ if post.persisted?
75
+ post.user_id == context[:user].id
76
+ else
77
+ context[:user].can?(:create, context[:model])
78
+ end
79
+ end
80
+
81
+ def self.update(post, context)
82
+ post.user_id == context[:user].id
83
+ end
84
+
85
+ def self.delete(post, context)
86
+ post.user_id == context[:user].id
87
+ end
88
+ end
89
+ end
90
+ ```
91
+ </details>
92
+
93
+ <details>
94
+ <summary>Graphql</summary>
95
+
96
+ Hai Graphql depends on `graphql-ruby` so if you don't have that installed and
97
+ boostrapped, head over to [ their repo and do that now ](https://github.com/rmosolgo/graphql-ruby#installation).
98
+
99
+ 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.
100
+
101
+ ```ruby
102
+ class MyAppSchema < GraphQL::Schema
103
+ include Hai::GraphQL::Types
104
+ hai_types(User, Post) # comma list of the models you want to expose
105
+
106
+ mutation(Types::MutationType)
107
+ query(Types::QueryType)
108
+ # ...
109
+ end
110
+ ```
111
+
112
+ 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:
113
+
114
+ ```ruby
115
+ module Types
116
+ class QueryType < Types::BaseObject
117
+ # Add `node(id: ID!) and `nodes(ids: [ID!]!)`
118
+ include GraphQL::Types::Relay::HasNodeField
119
+ include GraphQL::Types::Relay::HasNodesField
120
+
121
+ include Hai::GraphQL
122
+ hai_query(User)
123
+ end
124
+ end
125
+ ```
126
+
127
+ 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.
128
+
129
+ ```ruby
130
+ module Types
131
+ class MutationType < Types::BaseObject
132
+ include Hai::GraphQL
133
+ hai_mutation(User)
134
+ end
135
+ end
136
+ ```
137
+ </details>
138
+
139
+ <details>
140
+
141
+ <summary>Rest</summary>
142
+
143
+ 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:
144
+
145
+ ```ruby
146
+ Rails.application.routes.draw do
147
+ mount Hai::Rest::Engine => "/rest"
148
+ end
149
+ ```
150
+
151
+ Example queries for rest.
152
+ #### List all users
153
+
154
+ Simple use case
155
+
156
+ `GET <base_url>/rest/users`
157
+
158
+ You can also filter:
159
+
160
+ `GET <base_url>/rest/users?filter[name][eq]=bob`
161
+
162
+ Sort
163
+
164
+ `GET <base_url>/rest/users?sort[field]=name&sort[direction]=desc`
165
+
166
+ Paginate
167
+
168
+ `GET <base_url>/rest/users?limit=10&offset=20`
169
+
170
+ Or all things combined
171
+
172
+ `GET <base_url>/rest/users?filter[name][eq]=bob&sort[field]=name&sort[direction]=desc&limit=10&offset=20`
173
+
174
+ #### Read a specific user
175
+
176
+ `GET <base_url>/rest/users/1`
177
+
178
+ #### Create a user
179
+
180
+ `POST <base_url>/rest/users`
181
+
182
+ ```JSON
183
+ {
184
+ "user": {
185
+ "name": "bob"
186
+ }
187
+ }
188
+ ```
189
+
190
+ #### Update a user
191
+ `PUT <base_url>/rest/users/1`
192
+
193
+ ```JSON
194
+ {
195
+ "user": {
196
+ "name": "bob"
197
+ }
198
+ }
199
+ ```
200
+
201
+ #### Delete a user
202
+ `DELETE <base_url>/rest/users/1`
203
+
204
+ </details>
24
205
 
25
- TODO: Write usage instructions here
26
206
 
27
207
  ## Development
28
208
 
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.
209
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run
210
+ `rake test` to run the tests. You can also run `bin/console` for an interactive
211
+ prompt that will allow you to experiment.
30
212
 
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).
213
+ To install this gem onto your local machine, run `bundle exec rake install`. To
214
+ release a new version, update the version number in `version.rb`, and then run
215
+ `bundle exec rake release`, which will create a git tag for the version, push
216
+ git commits and the created tag, and push the `.gem` file to
217
+ [rubygems.org](https://rubygems.org).
32
218
 
33
219
  ## Contributing
34
220
 
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/hai.
221
+ Bug reports and pull requests are welcome on GitHub at
222
+ https://github.com/[USERNAME]/hai.
36
223
 
37
224
  ## License
38
225
 
39
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
226
+ The gem is available as open source under the terms of the [MIT
227
+ License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,69 @@
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 context
57
+ try(:super) || {}
58
+ end
59
+
60
+ def model_class
61
+ params[:model].classify.constantize
62
+ end
63
+
64
+ def read_params
65
+ params.permit! || {}
66
+ # params.permit(:limit, :offset, sort: { :field , :order},filter: {} )
67
+ end
68
+ end
69
+ 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