api_maker 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (103) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +476 -0
  4. data/Rakefile +27 -0
  5. data/app/channels/api_maker/subscriptions_channel.rb +80 -0
  6. data/app/controllers/api_maker/base_controller.rb +32 -0
  7. data/app/controllers/api_maker/commands_controller.rb +26 -0
  8. data/app/controllers/api_maker/devise_controller.rb +60 -0
  9. data/app/controllers/api_maker/session_statuses_controller.rb +33 -0
  10. data/app/services/api_maker/application_service.rb +7 -0
  11. data/app/services/api_maker/collection_command_service.rb +24 -0
  12. data/app/services/api_maker/command_response.rb +67 -0
  13. data/app/services/api_maker/command_service.rb +31 -0
  14. data/app/services/api_maker/create_command.rb +62 -0
  15. data/app/services/api_maker/create_command_service.rb +18 -0
  16. data/app/services/api_maker/destroy_command.rb +39 -0
  17. data/app/services/api_maker/destroy_command_service.rb +22 -0
  18. data/app/services/api_maker/generate_react_native_api_service.rb +61 -0
  19. data/app/services/api_maker/index_command.rb +96 -0
  20. data/app/services/api_maker/index_command_service.rb +22 -0
  21. data/app/services/api_maker/js_method_namer_service.rb +11 -0
  22. data/app/services/api_maker/member_command_service.rb +25 -0
  23. data/app/services/api_maker/model_content_generator_service.rb +108 -0
  24. data/app/services/api_maker/models_finder_service.rb +22 -0
  25. data/app/services/api_maker/models_generator_service.rb +104 -0
  26. data/app/services/api_maker/update_command.rb +43 -0
  27. data/app/services/api_maker/update_command_service.rb +21 -0
  28. data/app/services/api_maker/valid_command.rb +35 -0
  29. data/app/services/api_maker/valid_command_service.rb +21 -0
  30. data/app/views/api_maker/_data.html.erb +15 -0
  31. data/config/rails_best_practices.yml +55 -0
  32. data/config/routes.rb +7 -0
  33. data/lib/api_maker.rb +36 -0
  34. data/lib/api_maker/ability.rb +39 -0
  35. data/lib/api_maker/ability_loader.rb +21 -0
  36. data/lib/api_maker/action_controller_base_extensions.rb +5 -0
  37. data/lib/api_maker/base_command.rb +81 -0
  38. data/lib/api_maker/base_resource.rb +78 -0
  39. data/lib/api_maker/collection_serializer.rb +69 -0
  40. data/lib/api_maker/command_spec_helper.rb +57 -0
  41. data/lib/api_maker/configuration.rb +34 -0
  42. data/lib/api_maker/engine.rb +5 -0
  43. data/lib/api_maker/individual_command.rb +37 -0
  44. data/lib/api_maker/javascript/api.js +92 -0
  45. data/lib/api_maker/javascript/base-model.js +543 -0
  46. data/lib/api_maker/javascript/bootstrap/attribute-row.jsx +16 -0
  47. data/lib/api_maker/javascript/bootstrap/attribute-rows.jsx +47 -0
  48. data/lib/api_maker/javascript/bootstrap/card.jsx +79 -0
  49. data/lib/api_maker/javascript/bootstrap/checkbox.jsx +127 -0
  50. data/lib/api_maker/javascript/bootstrap/checkboxes.jsx +105 -0
  51. data/lib/api_maker/javascript/bootstrap/live-table.jsx +168 -0
  52. data/lib/api_maker/javascript/bootstrap/money-input.jsx +136 -0
  53. data/lib/api_maker/javascript/bootstrap/radio-buttons.jsx +80 -0
  54. data/lib/api_maker/javascript/bootstrap/select.jsx +168 -0
  55. data/lib/api_maker/javascript/bootstrap/string-input.jsx +203 -0
  56. data/lib/api_maker/javascript/cable-connection-pool.js +169 -0
  57. data/lib/api_maker/javascript/cable-subscription-pool.js +111 -0
  58. data/lib/api_maker/javascript/cable-subscription.js +33 -0
  59. data/lib/api_maker/javascript/collection.js +186 -0
  60. data/lib/api_maker/javascript/commands-pool.js +123 -0
  61. data/lib/api_maker/javascript/custom-error.js +14 -0
  62. data/lib/api_maker/javascript/deserializer.js +35 -0
  63. data/lib/api_maker/javascript/devise.js.erb +113 -0
  64. data/lib/api_maker/javascript/error-logger.js +119 -0
  65. data/lib/api_maker/javascript/event-connection.jsx +24 -0
  66. data/lib/api_maker/javascript/event-created.jsx +26 -0
  67. data/lib/api_maker/javascript/event-destroyed.jsx +26 -0
  68. data/lib/api_maker/javascript/event-emitter-listener.jsx +32 -0
  69. data/lib/api_maker/javascript/event-listener.jsx +41 -0
  70. data/lib/api_maker/javascript/event-updated.jsx +26 -0
  71. data/lib/api_maker/javascript/form-data-to-object.js +70 -0
  72. data/lib/api_maker/javascript/included.js +39 -0
  73. data/lib/api_maker/javascript/key-value-store.js +47 -0
  74. data/lib/api_maker/javascript/logger.js +23 -0
  75. data/lib/api_maker/javascript/model-name.js +21 -0
  76. data/lib/api_maker/javascript/model-template.js.erb +110 -0
  77. data/lib/api_maker/javascript/models-response-reader.js +43 -0
  78. data/lib/api_maker/javascript/paginate.jsx +128 -0
  79. data/lib/api_maker/javascript/params.js +68 -0
  80. data/lib/api_maker/javascript/resource-route.jsx +75 -0
  81. data/lib/api_maker/javascript/resource-routes.jsx +36 -0
  82. data/lib/api_maker/javascript/result.js +25 -0
  83. data/lib/api_maker/javascript/session-status-updater.js +113 -0
  84. data/lib/api_maker/javascript/sort-link.jsx +88 -0
  85. data/lib/api_maker/javascript/updated-attribute.jsx +60 -0
  86. data/lib/api_maker/loader.rb +14 -0
  87. data/lib/api_maker/memory_storage.rb +65 -0
  88. data/lib/api_maker/model_extensions.rb +96 -0
  89. data/lib/api_maker/permitted_params_argument.rb +12 -0
  90. data/lib/api_maker/preloader.rb +91 -0
  91. data/lib/api_maker/preloader_belongs_to.rb +58 -0
  92. data/lib/api_maker/preloader_has_many.rb +69 -0
  93. data/lib/api_maker/preloader_has_one.rb +70 -0
  94. data/lib/api_maker/preloader_through.rb +101 -0
  95. data/lib/api_maker/railtie.rb +14 -0
  96. data/lib/api_maker/relationship_includer.rb +42 -0
  97. data/lib/api_maker/resource_routing.rb +8 -0
  98. data/lib/api_maker/result_parser.rb +50 -0
  99. data/lib/api_maker/serializer.rb +86 -0
  100. data/lib/api_maker/spec_helper.rb +100 -0
  101. data/lib/api_maker/version.rb +3 -0
  102. data/lib/tasks/api_maker_tasks.rake +5 -0
  103. metadata +581 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7d65964fbfeb5bd8a5d89702083930f77e3d9c46956695c4f0877039a43c3010
4
+ data.tar.gz: 2a9431af5f0bfebcfb9605d85fafc40b824448aeac9ccd5a0f595b8e9f6d642d
5
+ SHA512:
6
+ metadata.gz: bf2858ef950edf1aa8cc9ce45938ebd1b5ea5f3dd1f11223fd42959bff910a5cceae21599abbfdce7dbabedcdcd481c54c832c77712417436d02f423285fb48d
7
+ data.tar.gz: 7f63b82811ef1a5207d2ecbdcc62072b4d4297263884de1c4b36704e7b114a2d027cbbb3dac324de69af6c19fc79f185f273b4bf070def5f7c9282e1b1caa404
@@ -0,0 +1,20 @@
1
+ Copyright 2018 kjabtion
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,476 @@
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).