api_maker 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
-
}
|