api_maker 0.0.1 → 0.0.2
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 +4 -4
- data/app/api_maker/api_helpers/api_maker_helpers.rb +5 -0
- data/app/api_maker/services/can_can/load_abilities.rb +30 -0
- data/app/api_maker/services/devise/sign_in.rb +64 -0
- data/app/api_maker/services/devise/sign_out.rb +9 -0
- data/app/api_maker/services/models/find_or_create_by.rb +18 -0
- data/app/channels/api_maker/subscriptions_channel.rb +33 -2
- data/app/controllers/api_maker/base_controller.rb +7 -3
- data/app/controllers/api_maker/commands_controller.rb +26 -4
- data/app/controllers/api_maker/session_statuses_controller.rb +1 -1
- data/app/services/api_maker/abilities_loader.rb +104 -0
- data/app/services/api_maker/application_service.rb +2 -1
- data/app/services/api_maker/base_command.rb +248 -0
- data/app/services/api_maker/collection_command_service.rb +29 -15
- data/app/services/api_maker/collection_loader.rb +124 -0
- data/app/services/api_maker/command_failed_error.rb +3 -0
- data/app/services/api_maker/command_response.rb +17 -6
- data/app/services/api_maker/command_service.rb +3 -3
- data/app/services/api_maker/create_command.rb +11 -26
- data/app/services/api_maker/create_command_service.rb +3 -3
- data/app/services/api_maker/database_type.rb +9 -0
- data/app/services/api_maker/deep_merge_params.rb +26 -0
- data/app/services/api_maker/deserializer.rb +35 -0
- data/app/services/api_maker/destroy_command.rb +15 -21
- data/app/services/api_maker/destroy_command_service.rb +3 -3
- data/app/services/api_maker/generate_react_native_api_service.rb +3 -19
- data/app/services/api_maker/include_helpers.rb +17 -0
- data/app/services/api_maker/index_command.rb +8 -88
- data/app/services/api_maker/index_command_service.rb +5 -5
- data/app/services/api_maker/js_method_namer_service.rb +1 -1
- data/app/services/api_maker/locals_from_controller.rb +14 -0
- data/app/services/api_maker/member_command_service.rb +15 -13
- data/app/services/api_maker/model_classes_java_script_generator_service.rb +37 -0
- data/app/services/api_maker/model_content_generator_service.rb +17 -21
- data/app/services/api_maker/models/save.rb +29 -0
- data/app/services/api_maker/models_finder_service.rb +6 -2
- data/app/services/api_maker/models_generator_service.rb +6 -43
- data/app/services/api_maker/move_components_to_routes.rb +50 -0
- data/app/services/api_maker/primary_id_for_model.rb +6 -0
- data/app/services/api_maker/reset_indexed_db_service.rb +36 -0
- data/app/services/api_maker/routes_file_reloader.rb +20 -0
- data/app/services/api_maker/select_columns_on_collection.rb +78 -0
- data/app/services/api_maker/select_parser.rb +32 -0
- data/app/services/api_maker/service_command.rb +27 -0
- data/app/services/api_maker/service_command_service.rb +14 -0
- data/app/services/api_maker/simple_model_errors.rb +52 -0
- data/app/services/api_maker/update_command.rb +8 -24
- data/app/services/api_maker/update_command_service.rb +3 -3
- data/app/services/api_maker/valid_command.rb +4 -13
- data/app/services/api_maker/valid_command_service.rb +3 -3
- data/app/services/api_maker/validation_errors_generator_service.rb +146 -0
- data/app/views/api_maker/_data.html.erb +17 -11
- data/config/routes.rb +0 -2
- data/lib/api_maker/ability.rb +22 -7
- data/lib/api_maker/ability_loader.rb +9 -6
- data/lib/api_maker/base_collection_instance.rb +15 -0
- data/lib/api_maker/base_resource.rb +135 -9
- data/lib/api_maker/base_service.rb +14 -0
- data/lib/api_maker/collection_serializer.rb +95 -34
- data/lib/api_maker/command_spec_helper.rb +41 -11
- data/lib/api_maker/configuration.rb +31 -4
- data/lib/api_maker/expect_to_able_to_helper.rb +31 -0
- data/lib/api_maker/individual_command.rb +24 -9
- data/lib/api_maker/javascript/model-template.js.erb +39 -25
- data/lib/api_maker/javascript/models.js.erb +6 -0
- data/lib/api_maker/loader.rb +1 -1
- data/lib/api_maker/memory_storage.rb +1 -1
- data/lib/api_maker/model_extensions.rb +34 -18
- data/lib/api_maker/permitted_params_argument.rb +5 -1
- data/lib/api_maker/preloader.rb +71 -32
- data/lib/api_maker/preloader_base.rb +108 -0
- data/lib/api_maker/preloader_belongs_to.rb +34 -33
- data/lib/api_maker/preloader_has_many.rb +45 -39
- data/lib/api_maker/preloader_has_one.rb +30 -47
- data/lib/api_maker/railtie.rb +3 -11
- data/lib/api_maker/relationship_preloader.rb +42 -0
- data/lib/api_maker/resource_routing.rb +18 -4
- data/lib/api_maker/result_parser.rb +34 -20
- data/lib/api_maker/serializer.rb +53 -22
- data/lib/api_maker/spec_helper/browser_logs.rb +14 -0
- data/lib/api_maker/spec_helper/execute_collection_command.rb +46 -0
- data/lib/api_maker/spec_helper/execute_member_command.rb +52 -0
- data/lib/api_maker/spec_helper/expect_no_browser_errors.rb +18 -0
- data/lib/api_maker/spec_helper/wait_for_expect.rb +20 -0
- data/lib/api_maker/spec_helper/wait_for_flash_message.rb +21 -0
- data/lib/api_maker/spec_helper.rb +112 -48
- data/lib/api_maker/version.rb +1 -1
- data/lib/api_maker.rb +7 -3
- metadata +108 -89
- data/README.md +0 -476
- data/app/controllers/api_maker/devise_controller.rb +0 -60
- data/lib/api_maker/base_command.rb +0 -81
- data/lib/api_maker/javascript/api.js +0 -92
- data/lib/api_maker/javascript/base-model.js +0 -543
- data/lib/api_maker/javascript/bootstrap/attribute-row.jsx +0 -16
- data/lib/api_maker/javascript/bootstrap/attribute-rows.jsx +0 -47
- data/lib/api_maker/javascript/bootstrap/card.jsx +0 -79
- data/lib/api_maker/javascript/bootstrap/checkbox.jsx +0 -127
- data/lib/api_maker/javascript/bootstrap/checkboxes.jsx +0 -105
- data/lib/api_maker/javascript/bootstrap/live-table.jsx +0 -168
- data/lib/api_maker/javascript/bootstrap/money-input.jsx +0 -136
- data/lib/api_maker/javascript/bootstrap/radio-buttons.jsx +0 -80
- data/lib/api_maker/javascript/bootstrap/select.jsx +0 -168
- data/lib/api_maker/javascript/bootstrap/string-input.jsx +0 -203
- data/lib/api_maker/javascript/cable-connection-pool.js +0 -169
- data/lib/api_maker/javascript/cable-subscription-pool.js +0 -111
- data/lib/api_maker/javascript/cable-subscription.js +0 -33
- data/lib/api_maker/javascript/collection.js +0 -186
- data/lib/api_maker/javascript/commands-pool.js +0 -123
- data/lib/api_maker/javascript/custom-error.js +0 -14
- data/lib/api_maker/javascript/deserializer.js +0 -35
- data/lib/api_maker/javascript/devise.js.erb +0 -113
- data/lib/api_maker/javascript/error-logger.js +0 -119
- data/lib/api_maker/javascript/event-connection.jsx +0 -24
- data/lib/api_maker/javascript/event-created.jsx +0 -26
- data/lib/api_maker/javascript/event-destroyed.jsx +0 -26
- data/lib/api_maker/javascript/event-emitter-listener.jsx +0 -32
- data/lib/api_maker/javascript/event-listener.jsx +0 -41
- data/lib/api_maker/javascript/event-updated.jsx +0 -26
- data/lib/api_maker/javascript/form-data-to-object.js +0 -70
- data/lib/api_maker/javascript/included.js +0 -39
- data/lib/api_maker/javascript/key-value-store.js +0 -47
- data/lib/api_maker/javascript/logger.js +0 -23
- data/lib/api_maker/javascript/model-name.js +0 -21
- data/lib/api_maker/javascript/models-response-reader.js +0 -43
- data/lib/api_maker/javascript/paginate.jsx +0 -128
- data/lib/api_maker/javascript/params.js +0 -68
- data/lib/api_maker/javascript/resource-route.jsx +0 -75
- data/lib/api_maker/javascript/resource-routes.jsx +0 -36
- data/lib/api_maker/javascript/result.js +0 -25
- data/lib/api_maker/javascript/session-status-updater.js +0 -113
- data/lib/api_maker/javascript/sort-link.jsx +0 -88
- data/lib/api_maker/javascript/updated-attribute.jsx +0 -60
- data/lib/api_maker/preloader_through.rb +0 -101
- data/lib/api_maker/relationship_includer.rb +0 -42
data/README.md
DELETED
|
@@ -1,476 +0,0 @@
|
|
|
1
|
-
# ApiMaker
|
|
2
|
-
|
|
3
|
-
Generates Rails API endpoints and JavaScript API files for Webpack and more by inspecting your models and serializers.
|
|
4
|
-
|
|
5
|
-
## Installation
|
|
6
|
-
Add this line to your application's Gemfile:
|
|
7
|
-
|
|
8
|
-
```ruby
|
|
9
|
-
gem "api_maker"
|
|
10
|
-
```
|
|
11
|
-
|
|
12
|
-
ApiMaker requires [Webpacker](https://github.com/rails/webpacker), so make sure you have that set up as well. It also uses an extension called [qs](https://www.npmjs.com/package/qs), that you should add to your packages, but that is probally already there by default.
|
|
13
|
-
|
|
14
|
-
ApiMaker makes use of [CanCanCan](https://github.com/CanCanCommunity/cancancan) to keep track of what models a given user should have access to. Each resource defines its own abilities under `app/api_maker/resources/user_resource` like this:
|
|
15
|
-
```ruby
|
|
16
|
-
class Resources::UserResource < Resources::ApplicationResource
|
|
17
|
-
def abilities
|
|
18
|
-
can :update, User if current_user&.admin?
|
|
19
|
-
can :update, User, id: current_user&.id if current_user.present?
|
|
20
|
-
can :read, User
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
Add an `api_maker_args` method to your application controller. This controls what arguments will be passed to the CanCan ability and the serializers:
|
|
26
|
-
```ruby
|
|
27
|
-
class ApplicationController
|
|
28
|
-
private
|
|
29
|
-
|
|
30
|
-
def api_maker_args
|
|
31
|
-
@api_maker_args ||= {current_user: current_user}
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
Insert this mount into `config/routes.rb`:
|
|
37
|
-
```ruby
|
|
38
|
-
Rails.application.routes.draw do
|
|
39
|
-
mount ApiMaker::Engine => "/api_maker"
|
|
40
|
-
end
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
ApiMaker will only create models, endpoints and serializers for ActiveRecord models that are defined as resources. So be sure to add resources under `app/api_maker/resources` for your models first. You can add some helper methods if you want to use in your resources like `current_user` and `signed_in_as_admin?`.
|
|
44
|
-
```ruby
|
|
45
|
-
class Resources::ApplicationResource < ApiMaker::BaseResource
|
|
46
|
-
def current_user
|
|
47
|
-
args&.dig(:current_user)
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def signed_in_as_admin?
|
|
51
|
-
current_user&.role == "admin"
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
```ruby
|
|
57
|
-
class Resources::UserResources < Resources::ApplicationResource
|
|
58
|
-
attributes :id, :email, :custom_attribute
|
|
59
|
-
attributes :calculated_attribute, selected_by_default: false
|
|
60
|
-
attributes :secret_attribute, if: :signed_in_as_admin?
|
|
61
|
-
collection_commands :count_users
|
|
62
|
-
member_commands :calculate_age
|
|
63
|
-
relationships :account, :tasks
|
|
64
|
-
|
|
65
|
-
def custom_attribute
|
|
66
|
-
"Hello world! Current user is: #{args.fetch(:current_user).email}"
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
You should also create an application command here: `app/api_maker/commands/application_command` with content like this:
|
|
72
|
-
```ruby
|
|
73
|
-
class Commands::ApplicationCommand < ApiMaker::BaseCommand
|
|
74
|
-
end
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
Add this to your application model:
|
|
78
|
-
```ruby
|
|
79
|
-
class ApplicationRecord < ActiveRecord::Base
|
|
80
|
-
include ApiMaker::ModelExtensions
|
|
81
|
-
end
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
ApiMaker uses that to keep track of what attributes, relationships and commands you want exposed through the API.
|
|
85
|
-
|
|
86
|
-
Its now time to generate everything like this:
|
|
87
|
-
```bash
|
|
88
|
-
rake api_maker:generate_models
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
If you want to be able to create and update models, then you should go into each resource and create a params method to define, which attributes can be written on each model like this:
|
|
92
|
-
```ruby
|
|
93
|
-
class Resources::TaskResource < ApiMaker::ModelController
|
|
94
|
-
def permitted_params(arg)
|
|
95
|
-
arg.params.require(:project).permit(:name)
|
|
96
|
-
end
|
|
97
|
-
end
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
### I18n
|
|
101
|
-
|
|
102
|
-
In order to use the built in text support, you need to add `i18n-js` to your project.
|
|
103
|
-
|
|
104
|
-
Start by adding to your Gemfile:
|
|
105
|
-
```ruby
|
|
106
|
-
gem "i18n-js"
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
Then add `config/i18n-js.yml`:
|
|
110
|
-
```yml
|
|
111
|
-
translations:
|
|
112
|
-
- file: "app/assets/javascripts/i18n/translations.js"
|
|
113
|
-
only: ["*.activerecord.attributes.*", "*.activerecord.models.*", "*.date.*", "*.js.*", "*.number.currency.*", "*.time.*"]
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
Then add this to `app/assets/javascript/application.js.erb`:
|
|
117
|
-
```js
|
|
118
|
-
//= require i18n
|
|
119
|
-
//= require i18n/translations
|
|
120
|
-
|
|
121
|
-
var locale = document.querySelector("html").getAttribute("lang")
|
|
122
|
-
I18n.locale = locale
|
|
123
|
-
|
|
124
|
-
<% if Rails.env.development? || Rails.env.test? %>
|
|
125
|
-
I18n.missingTranslation = function(key) {
|
|
126
|
-
console.error(`No translation for: ${key}`)
|
|
127
|
-
return `translation missing: ${key}`
|
|
128
|
-
}
|
|
129
|
-
<% end %>
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
Add this to the `<html>`-tag:
|
|
133
|
-
```html
|
|
134
|
-
<html lang="<%= I18n.locale %>">
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
Add this to `config/application.rb` to ease development:
|
|
138
|
-
```ruby
|
|
139
|
-
config.middleware.use I18n::JS::Middleware
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
### ActionCable
|
|
143
|
-
|
|
144
|
-
Your `connection.rb` should look something like this:
|
|
145
|
-
```rb
|
|
146
|
-
class ApplicationCable::Connection < ActionCable::Connection::Base
|
|
147
|
-
identified_by :current_user
|
|
148
|
-
|
|
149
|
-
def connect
|
|
150
|
-
self.current_user = find_verified_user
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
private
|
|
154
|
-
|
|
155
|
-
def find_verified_user
|
|
156
|
-
verified_user = User.find_by(id: cookies.signed["user.id"])
|
|
157
|
-
|
|
158
|
-
if verified_user && cookies.signed["user.expires_at"] > Time.zone.now
|
|
159
|
-
verified_user
|
|
160
|
-
else
|
|
161
|
-
reject_unauthorized_connection
|
|
162
|
-
end
|
|
163
|
-
end
|
|
164
|
-
end
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
Your `channel.rb` should look something like this:
|
|
168
|
-
```rb
|
|
169
|
-
class ApplicationCable::Channel < ActionCable::Channel::Base
|
|
170
|
-
private # rubocop:disable Layout/IndentationWidth
|
|
171
|
-
|
|
172
|
-
def current_ability
|
|
173
|
-
@current_ability ||= ApiMakerAbility.for_user(current_user)
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
def current_user
|
|
177
|
-
@current_user ||= env["warden"].user
|
|
178
|
-
end
|
|
179
|
-
end
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
## Usage
|
|
183
|
-
|
|
184
|
-
### Creating a new model from JavaScript
|
|
185
|
-
|
|
186
|
-
```js
|
|
187
|
-
import Task from "api-maker/models/task"
|
|
188
|
-
|
|
189
|
-
var task = new Task()
|
|
190
|
-
task.assignAttributes({name: "New task"})
|
|
191
|
-
task.create().then(status => {
|
|
192
|
-
if (status.success) {
|
|
193
|
-
console.log(`Task was created with ID: ${task.id()}`)
|
|
194
|
-
} else {
|
|
195
|
-
console.log("Task wasnt created")
|
|
196
|
-
}
|
|
197
|
-
})
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
### Finding an existing model
|
|
201
|
-
|
|
202
|
-
```js
|
|
203
|
-
Task.find(5).then(task => {
|
|
204
|
-
console.log(`Task found: ${task.name()}`)
|
|
205
|
-
})
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
### Updating a model
|
|
209
|
-
|
|
210
|
-
```js
|
|
211
|
-
task.assignAttributes({name: "New name"})
|
|
212
|
-
task.save().then(status => {
|
|
213
|
-
if (status.success) {
|
|
214
|
-
console.log(`Task was updated and name is now: ${task.name()}`)
|
|
215
|
-
} else {
|
|
216
|
-
console.log("Task wasnt updated")
|
|
217
|
-
}
|
|
218
|
-
})
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
```js
|
|
222
|
-
task.update({name: "New name"}).then(status => {
|
|
223
|
-
if (status.success) {
|
|
224
|
-
console.log(`Task was updated and name is now: ${task.name()}`)
|
|
225
|
-
} else {
|
|
226
|
-
console.log("Task wasnt updated")
|
|
227
|
-
}
|
|
228
|
-
})
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
### Deleting a model
|
|
232
|
-
|
|
233
|
-
```js
|
|
234
|
-
task.destroy().then(status => {
|
|
235
|
-
if (status.success) {
|
|
236
|
-
console.log("Task was destroyed")
|
|
237
|
-
} else {
|
|
238
|
-
console.log("Task wasnt destroyed")
|
|
239
|
-
}
|
|
240
|
-
})
|
|
241
|
-
```
|
|
242
|
-
|
|
243
|
-
### Preloading models
|
|
244
|
-
|
|
245
|
-
```js
|
|
246
|
-
Task.ransack().preload("project.customer").toArray().then(tasks => {
|
|
247
|
-
for(var task of tasks) {
|
|
248
|
-
console.log(`Project of task ${task.id()}: ${task.project().name()}`)
|
|
249
|
-
console.log(`Customer of task ${task.id()}: ${task.project().customer().name()}`)
|
|
250
|
-
}
|
|
251
|
-
})
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
### Query models
|
|
255
|
-
|
|
256
|
-
ApiModels uses [Ransack](https://github.com/activerecord-hackery/ransack) to expose a huge amount of options to query data.
|
|
257
|
-
|
|
258
|
-
```js
|
|
259
|
-
Task.ransack({name_cont: "something"}).toArray().then(tasks => {
|
|
260
|
-
console.log(`Found: ${tasks.length} tasks`)
|
|
261
|
-
})
|
|
262
|
-
```
|
|
263
|
-
|
|
264
|
-
Distinct:
|
|
265
|
-
```js
|
|
266
|
-
var tasks = await Task.ransack({relationships_something_eq: "something"}).distinct().toArray()
|
|
267
|
-
```
|
|
268
|
-
|
|
269
|
-
### Selecting only specific attributes
|
|
270
|
-
|
|
271
|
-
```js
|
|
272
|
-
Task.ransack().select({Task: ["id", "name"]}).toArray().then(tasks => this.setState({tasks}))
|
|
273
|
-
```
|
|
274
|
-
|
|
275
|
-
### Sorting models
|
|
276
|
-
|
|
277
|
-
```js
|
|
278
|
-
Task.ransack({s: "id desc"})
|
|
279
|
-
```
|
|
280
|
-
|
|
281
|
-
### Attributes
|
|
282
|
-
|
|
283
|
-
Each attribute is defined as a method on each model. So if you have an attribute called `name` on the `Task`-model, then it be read by doing this: `task.name()`.
|
|
284
|
-
|
|
285
|
-
### Relationships
|
|
286
|
-
|
|
287
|
-
#### Has many
|
|
288
|
-
|
|
289
|
-
A `has many` relationship will return a collection the queries the sub models.
|
|
290
|
-
|
|
291
|
-
```js
|
|
292
|
-
project.tasks().toArray().then(tasks => {
|
|
293
|
-
console.log(`Project ${project.id()} has ${tasks.length} tasks`)
|
|
294
|
-
|
|
295
|
-
for(var key in tasks) {
|
|
296
|
-
var task = tasks[key]
|
|
297
|
-
console.log(`Task ${task.id()} is named: ${task.name()}`)
|
|
298
|
-
}
|
|
299
|
-
})
|
|
300
|
-
```
|
|
301
|
-
|
|
302
|
-
#### Belongs to
|
|
303
|
-
|
|
304
|
-
A `belongs to` relationship will return a promise that will get that model:
|
|
305
|
-
|
|
306
|
-
```js
|
|
307
|
-
task.project().then(project => {
|
|
308
|
-
console.log(`Task ${task.id()} belongs to a project called: ${project.name()}`)
|
|
309
|
-
})
|
|
310
|
-
```
|
|
311
|
-
|
|
312
|
-
#### Has one
|
|
313
|
-
|
|
314
|
-
A `has one` relationship will also return a promise that will get that model like a `belongs to` relationship.
|
|
315
|
-
|
|
316
|
-
#### Getting the current user
|
|
317
|
-
|
|
318
|
-
First include this in your layout, so JS can know which user is signed in:
|
|
319
|
-
```erb
|
|
320
|
-
<body>
|
|
321
|
-
<%= render "/api_maker/data" %>
|
|
322
|
-
```
|
|
323
|
-
|
|
324
|
-
Then you can do like this in JS:
|
|
325
|
-
```js
|
|
326
|
-
import Devise from "api-maker/devise"
|
|
327
|
-
|
|
328
|
-
Devise.currentUser().then(user => {
|
|
329
|
-
console.log(`The current user has this email: ${user.email()}`)
|
|
330
|
-
})
|
|
331
|
-
```
|
|
332
|
-
|
|
333
|
-
## Events from the backend
|
|
334
|
-
|
|
335
|
-
### Custom events
|
|
336
|
-
|
|
337
|
-
Add the relevant access to your abilities:
|
|
338
|
-
|
|
339
|
-
```ruby
|
|
340
|
-
class ApiMakerAbility < ApplicationAbility
|
|
341
|
-
def initialize(args:)
|
|
342
|
-
can :event_new_message, User, id: 5
|
|
343
|
-
end
|
|
344
|
-
end
|
|
345
|
-
```
|
|
346
|
-
|
|
347
|
-
```ruby
|
|
348
|
-
user = User.find(5)
|
|
349
|
-
user.api_maker_event("new_message", message: "Hello world")
|
|
350
|
-
```
|
|
351
|
-
|
|
352
|
-
```js
|
|
353
|
-
User.find(5).then(user => {
|
|
354
|
-
user.connect("new_message", args => {
|
|
355
|
-
console.log(`New message: ${args.message}`)
|
|
356
|
-
})
|
|
357
|
-
})
|
|
358
|
-
```
|
|
359
|
-
|
|
360
|
-
### Update models
|
|
361
|
-
|
|
362
|
-
Add this to your abilities:
|
|
363
|
-
```ruby
|
|
364
|
-
class ApiMakerAbility < ApplicationAbility
|
|
365
|
-
def initialize(args:)
|
|
366
|
-
can [:create_events, :destroy_events, :update_events], User, id: 5
|
|
367
|
-
end
|
|
368
|
-
end
|
|
369
|
-
```
|
|
370
|
-
|
|
371
|
-
Add this to the model you want to broadcast updates:
|
|
372
|
-
```ruby
|
|
373
|
-
class User < ApplicationRecord
|
|
374
|
-
api_maker_broadcast_creates
|
|
375
|
-
api_maker_broadcast_destroys
|
|
376
|
-
api_maker_broadcast_updates
|
|
377
|
-
end
|
|
378
|
-
```
|
|
379
|
-
|
|
380
|
-
```js
|
|
381
|
-
User.find(5).then(user => {
|
|
382
|
-
let subscription = user.connectUpdated(args => {
|
|
383
|
-
console.log(`Model was updated: ${args.model.id()}`)
|
|
384
|
-
})
|
|
385
|
-
})
|
|
386
|
-
```
|
|
387
|
-
|
|
388
|
-
Remember to unsubscrube again:
|
|
389
|
-
```js
|
|
390
|
-
subscription.unsubscribe()
|
|
391
|
-
```
|
|
392
|
-
|
|
393
|
-
You can also use a React component if you use React and dont want to keep track of when to unsubscribe:
|
|
394
|
-
```jsx
|
|
395
|
-
import EventUpdated from "api-maker/event-created"
|
|
396
|
-
import EventUpdated from "api-maker/event-destroyed"
|
|
397
|
-
import EventUpdated from "api-maker/event-updated"
|
|
398
|
-
```
|
|
399
|
-
|
|
400
|
-
```jsx
|
|
401
|
-
<EventCreated modelClass={User} onCreated={(args) => this.onUserCreated(args)} />
|
|
402
|
-
<EventDestroyed model={user} onDestroyed={(args) => this.onUserDestroyed(args)} />
|
|
403
|
-
<EventUpdated model={user} onUpdated={(args) => this.onUserUpdated(args)} />
|
|
404
|
-
```
|
|
405
|
-
|
|
406
|
-
```jsx
|
|
407
|
-
onUserCreated(args) {
|
|
408
|
-
this.setState({user: args.model})
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
onUserDestroyed(args) {
|
|
412
|
-
this.setState({user: args.model})
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
onUserUpdated(args) {
|
|
416
|
-
this.setState({user: args.model})
|
|
417
|
-
}
|
|
418
|
-
```
|
|
419
|
-
|
|
420
|
-
You can also use this React component to show a models attribute with automatic updates:
|
|
421
|
-
|
|
422
|
-
```jsx
|
|
423
|
-
import UpdatedAttribute from "api-maker/updated-attribute"
|
|
424
|
-
```
|
|
425
|
-
|
|
426
|
-
```jsx
|
|
427
|
-
<UpdatedAttribute model={user} attribute="email" />
|
|
428
|
-
```
|
|
429
|
-
|
|
430
|
-
You can also use the `EventConnection` React component so you don't need to keep track of your subscription and unsubscribe:
|
|
431
|
-
```jsx
|
|
432
|
-
import EventConnection from "api-maker/event-connection"
|
|
433
|
-
```
|
|
434
|
-
|
|
435
|
-
```jsx
|
|
436
|
-
<EventConnection model={this.state.user} event="eventName" onCall={(data) => this.onEvent(data)} />
|
|
437
|
-
```
|
|
438
|
-
|
|
439
|
-
## Serializing
|
|
440
|
-
|
|
441
|
-
### Conditional attributes
|
|
442
|
-
|
|
443
|
-
This will only include the email for users, if the current user signed in is an admin.
|
|
444
|
-
|
|
445
|
-
```ruby
|
|
446
|
-
class Resources::UserResource < Resources::ApplicationResource
|
|
447
|
-
attributes :id
|
|
448
|
-
attributes :email, if: :signed_in_as_admin?
|
|
449
|
-
|
|
450
|
-
private
|
|
451
|
-
|
|
452
|
-
def signed_in_as_admin?
|
|
453
|
-
args[:current_user]&.admin?
|
|
454
|
-
end
|
|
455
|
-
end
|
|
456
|
-
```
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
## Reporting errors
|
|
460
|
-
|
|
461
|
-
Add an intializer with something like this:
|
|
462
|
-
|
|
463
|
-
```ruby
|
|
464
|
-
ApiMaker::Configuration.configure do |config|
|
|
465
|
-
config.on_error do |controller:, error:|
|
|
466
|
-
ExceptionNotifier.notify_exception(error, env: controller&.request&.env)
|
|
467
|
-
end
|
|
468
|
-
end
|
|
469
|
-
```
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
## Contributing
|
|
473
|
-
Contribution directions go here.
|
|
474
|
-
|
|
475
|
-
## License
|
|
476
|
-
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
class ApiMaker::DeviseController < ApiMaker::BaseController
|
|
2
|
-
include Devise::Controllers::Rememberable
|
|
3
|
-
|
|
4
|
-
before_action :check_model_exists, only: :do_sign_in
|
|
5
|
-
before_action :check_serializer_exists, only: :do_sign_in
|
|
6
|
-
|
|
7
|
-
def do_sign_in
|
|
8
|
-
if !model.active_for_authentication?
|
|
9
|
-
render json: {success: false, errors: [model.inactive_message]}
|
|
10
|
-
elsif model.valid_password?(params[:password])
|
|
11
|
-
sign_in(model, scope: scope)
|
|
12
|
-
remember_me(model) if params.dig(:args, :rememberMe)
|
|
13
|
-
render json: {success: true, model_data: serializer.result}
|
|
14
|
-
else
|
|
15
|
-
render json: {success: false, errors: [invalid_error_message]}
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def do_sign_out
|
|
20
|
-
scope = params.dig(:args, :scope).presence || "user"
|
|
21
|
-
current_model = __send__("current_#{scope}")
|
|
22
|
-
sign_out current_model
|
|
23
|
-
render json: {success: true}
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
private
|
|
27
|
-
|
|
28
|
-
def check_model_exists
|
|
29
|
-
error_msg = t("devise.failure.not_found_in_database", authentication_keys: model_class.authentication_keys.join(", "))
|
|
30
|
-
render json: {success: false, errors: [error_msg]} unless model
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def check_serializer_exists
|
|
34
|
-
render json: {success: false, errors: ["Serializer doesn't exist for #{scope}"]} unless resource
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def invalid_error_message
|
|
38
|
-
t("devise.failure.invalid", authentication_keys: model_class.authentication_keys.join(", "))
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def model
|
|
42
|
-
@model ||= model_class.find_for_authentication(email: params[:username])
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def model_class
|
|
46
|
-
@model_class ||= scope.camelize.safe_constantize
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def scope
|
|
50
|
-
@scope ||= params.dig(:args, :scope).presence || "user"
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def resource
|
|
54
|
-
@resource ||= ApiMaker::Serializer.resource_for(model.class)
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def serializer
|
|
58
|
-
@serializer ||= ApiMaker::Serializer.new(ability: current_ability, args: api_maker_args, model: model)
|
|
59
|
-
end
|
|
60
|
-
end
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
class ApiMaker::BaseCommand
|
|
2
|
-
attr_reader :api_maker_args, :commands, :command_response, :collection, :controller, :current_ability
|
|
3
|
-
|
|
4
|
-
# Returns true if the gem "goldiloader" is present in the app
|
|
5
|
-
def self.goldiloader?
|
|
6
|
-
@goldiloader = Gem::Specification.find_all_by_name("goldiloader").any? if @goldiloader.nil?
|
|
7
|
-
@goldiloader
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
def initialize(ability:, args:, collection:, commands:, command_response:, controller:)
|
|
11
|
-
@api_maker_args = args
|
|
12
|
-
@current_ability = ability
|
|
13
|
-
@collection = collection
|
|
14
|
-
@commands = commands
|
|
15
|
-
@command_response = command_response
|
|
16
|
-
@controller = controller
|
|
17
|
-
|
|
18
|
-
# Make it possible to do custom preloads (useful in threadded mode that doesnt support Goldiloader)
|
|
19
|
-
@collection = custom_collection(@collection) if respond_to?(:custom_collection)
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def self.execute_in_thread!(**args)
|
|
23
|
-
args.fetch(:command_response).with_thread do
|
|
24
|
-
new(**args).execute!
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def each_command(args = {}, &blk)
|
|
29
|
-
if args[:threadded]
|
|
30
|
-
# Goldiloader doesn't work with threads (loads all relationships for each thread)
|
|
31
|
-
@collection = @collection.auto_include(false) if ApiMaker::BaseCommand.goldiloader?
|
|
32
|
-
|
|
33
|
-
# Load relationship before commands so each command doesn't query on its own
|
|
34
|
-
@collection.load
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
@commands.each do |command_id, command_data|
|
|
38
|
-
if args[:threadded]
|
|
39
|
-
command_response.with_thread do
|
|
40
|
-
run_command(command_id, command_data, &blk)
|
|
41
|
-
end
|
|
42
|
-
else
|
|
43
|
-
run_command(command_id, command_data, &blk)
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
delegate :result_for_command, to: :command_response
|
|
49
|
-
|
|
50
|
-
private
|
|
51
|
-
|
|
52
|
-
def run_command(command_id, command_data)
|
|
53
|
-
command = ApiMaker::IndividualCommand.new(
|
|
54
|
-
args: command_data[:args],
|
|
55
|
-
collection: @collection,
|
|
56
|
-
command: self,
|
|
57
|
-
id: command_id,
|
|
58
|
-
primary_key: command_data[:primary_key],
|
|
59
|
-
response: command_response
|
|
60
|
-
)
|
|
61
|
-
|
|
62
|
-
begin
|
|
63
|
-
yield command
|
|
64
|
-
rescue => e # rubocop:disable Style/RescueStandardError
|
|
65
|
-
command.error(success: false, errors: [command_error_message(e)])
|
|
66
|
-
|
|
67
|
-
Rails.logger.error e.message
|
|
68
|
-
Rails.logger.error e.backtrace.join("\n")
|
|
69
|
-
|
|
70
|
-
ApiMaker::Configuration.current.report_error(controller: controller, error: e)
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def command_error_message(error)
|
|
75
|
-
if Rails.application.config.consider_all_requests_local
|
|
76
|
-
"#{error.class.name}: #{error.message}"
|
|
77
|
-
else
|
|
78
|
-
"Internal server error"
|
|
79
|
-
end
|
|
80
|
-
end
|
|
81
|
-
end
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import CustomError from "./custom-error"
|
|
2
|
-
import qs from "qs"
|
|
3
|
-
|
|
4
|
-
export default class {
|
|
5
|
-
static get(path, data = null) {
|
|
6
|
-
return this.requestLocal({"path": path, "pathParams": data, "method": "GET"})
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
static delete(path, data = null) {
|
|
10
|
-
return this.requestLocal({"path": path, "pathParams": data, "method": "DELETE"})
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
static patch(path, data = {}) {
|
|
14
|
-
return this.requestLocal({"path": path, "data": data, "method": "PATCH"})
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
static post(path, data = {}) {
|
|
18
|
-
return this.requestLocal({"path": path, "data": data, "method": "POST"})
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
static request(args) {
|
|
22
|
-
var path = args.path
|
|
23
|
-
|
|
24
|
-
if (args.pathParams) {
|
|
25
|
-
var pathParamsString = qs.stringify(args.pathParams, {"arrayFormat": "brackets"})
|
|
26
|
-
path += `?${pathParamsString}`
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
return new Promise((resolve, reject) => {
|
|
30
|
-
var xhr = new XMLHttpRequest()
|
|
31
|
-
xhr.open(args.method, path, true)
|
|
32
|
-
|
|
33
|
-
if (args.headers) {
|
|
34
|
-
for(var headerName in args.headers) {
|
|
35
|
-
xhr.setRequestHeader(headerName, args.headers[headerName])
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
xhr.onload = () => {
|
|
40
|
-
var response = this._parseResponse(xhr)
|
|
41
|
-
|
|
42
|
-
if (xhr.status == 200) {
|
|
43
|
-
resolve(response)
|
|
44
|
-
} else {
|
|
45
|
-
reject(new CustomError(`Request failed with code: ${xhr.status}`, {response: response}))
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
xhr.send(args.data)
|
|
50
|
-
})
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
static requestLocal(args) {
|
|
54
|
-
if (!args.headers)
|
|
55
|
-
args["headers"] = {}
|
|
56
|
-
|
|
57
|
-
var token = this._token()
|
|
58
|
-
if (token)
|
|
59
|
-
args["headers"]["X-CSRF-Token"] = token
|
|
60
|
-
|
|
61
|
-
if (args.data) {
|
|
62
|
-
args["headers"]["Content-Type"] = "application/json"
|
|
63
|
-
args["data"] = JSON.stringify(args.data)
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (args.rawData)
|
|
67
|
-
args["data"] = args.rawData
|
|
68
|
-
|
|
69
|
-
return this.request(args)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
static put(path, data = {}) {
|
|
73
|
-
return this.requestLocal({"path": path, "data": data, "method": "PUT"})
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
static _token() {
|
|
77
|
-
var tokenElement = document.querySelector("meta[name='csrf-token']")
|
|
78
|
-
|
|
79
|
-
if (tokenElement)
|
|
80
|
-
return tokenElement.getAttribute("content")
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
static _parseResponse(xhr) {
|
|
84
|
-
var responseType = xhr.getResponseHeader("content-type")
|
|
85
|
-
|
|
86
|
-
if (responseType && responseType.startsWith("application/json")) {
|
|
87
|
-
return JSON.parse(xhr.responseText)
|
|
88
|
-
} else {
|
|
89
|
-
return xhr.responseText
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|