hai 0.0.2 → 0.0.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 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