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