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,88 @@
1
+ import { Link } from "react-router-dom"
2
+ import qs from "qs"
3
+ import React from "react"
4
+
5
+ const inflection = require("inflection")
6
+
7
+ export default class extends React.Component {
8
+ constructor(props) {
9
+ super(props)
10
+
11
+ var searchKey = this.props.query.queryArgs.searchKey
12
+ if (!searchKey)
13
+ searchKey = "q"
14
+
15
+ this.state = {searchKey: searchKey}
16
+ }
17
+
18
+ attribute() {
19
+ return inflection.underscore(this.props.attribute)
20
+ }
21
+
22
+ href() {
23
+ var currentParams = qs.parse(window.location.search.substr(1))
24
+
25
+ if (!currentParams[this.state.searchKey])
26
+ currentParams[this.state.searchKey] = {}
27
+
28
+ currentParams[this.state.searchKey]["s"] = `${this.attribute()} ${this.sortMode()}`
29
+
30
+ var newParams = qs.stringify(currentParams)
31
+ var newPath = `${location.pathname}?${newParams}`
32
+
33
+ return newPath
34
+ }
35
+
36
+ isSortedByAttribute() {
37
+ if (this.props.query.queryArgs.ransack && this.props.query.queryArgs.ransack.s == this.attribute())
38
+ return true
39
+
40
+ if (this.props.query.queryArgs.ransack && this.props.query.queryArgs.ransack.s == `${this.attribute()} asc`)
41
+ return true
42
+
43
+ return false
44
+ }
45
+
46
+ render() {
47
+ var LinkComponent = this.linkComponent()
48
+ var { attribute, className, linkComponent, query, title, ...other } = this.props
49
+
50
+ return (
51
+ <LinkComponent {...other} className={this.className()} data-attribute={attribute} data-sort-mode={this.sortMode()} to={this.href()}>
52
+ {this.title()}
53
+ </LinkComponent>
54
+ )
55
+ }
56
+
57
+ className() {
58
+ var classNames = ["component-api-maker-bootstrap-sort-link"]
59
+
60
+ if (this.props.className)
61
+ classNames.push(this.props.className)
62
+
63
+ return classNames.join(" ")
64
+ }
65
+
66
+ linkComponent() {
67
+ if (this.props.linkComponent) {
68
+ return this.props.linkComponent
69
+ } else {
70
+ return Link
71
+ }
72
+ }
73
+
74
+ sortMode() {
75
+ if (this.isSortedByAttribute()) {
76
+ return "desc"
77
+ } else {
78
+ return "asc"
79
+ }
80
+ }
81
+
82
+ title() {
83
+ if (this.props.title)
84
+ return this.props.title
85
+
86
+ return this.props.query.modelClass().humanAttributeName(this.props.attribute)
87
+ }
88
+ }
@@ -0,0 +1,60 @@
1
+ import PropTypes from "prop-types"
2
+ import PropTypesExact from "prop-types-exact"
3
+ import React from "react"
4
+
5
+ export default class ApiMakerUpdatedAttribute extends React.Component {
6
+ static propTypes = PropTypesExact({
7
+ attribute: PropTypes.string,
8
+ model: PropTypes.object.isRequired,
9
+ onValue: PropTypes.func
10
+ })
11
+
12
+ constructor(props) {
13
+ super(props)
14
+ this.state = {
15
+ model: this.props.model
16
+ }
17
+ }
18
+
19
+ componentDidMount() {
20
+ this.setAttribute()
21
+ this.connect()
22
+ }
23
+
24
+ componentWillUnmount() {
25
+ this.connectUpdated.unsubscribe()
26
+ }
27
+
28
+ connect() {
29
+ this.connectUpdated = this.props.model.connectUpdated(args =>
30
+ this.setState(
31
+ {model: args.model},
32
+ () => this.setAttribute()
33
+ )
34
+ )
35
+ }
36
+
37
+ setAttribute() {
38
+ var newValue
39
+
40
+ if (this.props.onValue) {
41
+ newValue = this.props.onValue.apply(null, [{model: this.state.model}])
42
+ } else {
43
+ if (!this.state.model[this.props.attribute])
44
+ throw new Error(`No such method: ${this.state.model.modelClassData().name}#${this.props.attribute}()`)
45
+
46
+ newValue = this.state.model[this.props.attribute].apply(this.state.model)
47
+ }
48
+
49
+ this.setState({
50
+ value: newValue
51
+ })
52
+ }
53
+
54
+ render() {
55
+ if (this.state.value === undefined)
56
+ return ""
57
+
58
+ return this.state.value
59
+ }
60
+ }
@@ -0,0 +1,14 @@
1
+ class ApiMaker::Loader
2
+ def self.load_everything
3
+ return if @loaded
4
+
5
+ @loaded = true
6
+
7
+ resources_dir = Rails.root.join("app", "api_maker", "resources")
8
+ files = Dir.glob("#{resources_dir}/**/*.rb")
9
+
10
+ files.each do |file|
11
+ require file
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,65 @@
1
+ class ApiMaker::MemoryStorage
2
+ attr_reader :model_class_for_data, :storage
3
+
4
+ def self.current
5
+ @current ||= ApiMaker::MemoryStorage.new
6
+ end
7
+
8
+ def initialize
9
+ @model_class_for_data = {}
10
+ @storage = {}
11
+ end
12
+
13
+ def storage_for(klass, mode)
14
+ @storage.dig(klass, mode) || {}
15
+ end
16
+
17
+ def add(klass, mode, data, args = {})
18
+ @storage[klass] ||= {}
19
+ @storage[klass][mode] ||= {}
20
+ @storage[klass][mode][data] = {data: data, args: args} unless @storage[klass][mode].key?(data)
21
+ end
22
+
23
+ def load_all_resources
24
+ resources_path = Rails.root.join("app", "api_maker", "resources")
25
+
26
+ Dir.foreach(resources_path) do |file|
27
+ match = file.match(/\A((.+)_resource)\.rb\Z/)
28
+ next unless match
29
+
30
+ resource_name = match[1]
31
+ resource_class_name = "Resources::#{resource_name.camelize}"
32
+
33
+ # Load the resource by constantizing it to support auto loading
34
+ resource_class_name.safe_constantize
35
+ end
36
+
37
+ @resources_loaded = true
38
+ end
39
+
40
+ def resources_loaded?
41
+ @resources_loaded
42
+ end
43
+
44
+ def model_class_for(resource:, klass:)
45
+ model_class_for_data[klass.name] = resource.name
46
+ end
47
+
48
+ def resource_for_model(model_class)
49
+ class_name = resource_name_for_model(model_class)
50
+ resource_class = class_name.safe_constantize
51
+
52
+ if !resource_class && !resources_loaded?
53
+ load_all_resources # Some resources with custom model class won't have been loaded at this point
54
+ return resource_for_model(model_class)
55
+ end
56
+
57
+ raise "Resource couldn't be found from model: #{model_class}" unless resource_class
58
+
59
+ resource_class
60
+ end
61
+
62
+ def resource_name_for_model(model_class)
63
+ model_class_for_data[model_class.name] || "Resources::#{model_class.name.gsub("::", "")}Resource"
64
+ end
65
+ end
@@ -0,0 +1,96 @@
1
+ module ApiMaker::ModelExtensions
2
+ def self.included(base)
3
+ base.extend(ClassMethods)
4
+ end
5
+
6
+ module ClassMethods
7
+ def api_maker_broadcast_creates
8
+ after_commit on: :create do |model| # rubocop:disable Style/SymbolProc
9
+ model.api_maker_broadcast_create
10
+ end
11
+ end
12
+
13
+ def api_maker_broadcast_create_channel_name
14
+ @api_maker_broadcast_create_channel_name ||= "api_maker_creates_#{api_maker_resource.short_name}"
15
+ end
16
+
17
+ def api_maker_broadcast_updates
18
+ after_commit on: :update do |model| # rubocop:disable Style/SymbolProc
19
+ model.api_maker_broadcast_update
20
+ end
21
+ end
22
+
23
+ def api_maker_broadcast_destroys
24
+ after_commit on: :destroy do |model| # rubocop:disable Style/SymbolProc
25
+ model.api_maker_broadcast_destroy
26
+ end
27
+ end
28
+
29
+ def api_maker_resource
30
+ @api_maker_resource ||= ApiMaker::MemoryStorage.current.resource_for_model(self)
31
+ end
32
+ end
33
+
34
+ def api_maker_event(event_name, args = {})
35
+ channel_name = api_maker_event_channel_name(event_name)
36
+ serializer = ApiMaker::Serializer.new(model: self)
37
+ data_to_broadcast = ApiMaker::ResultParser.parse(
38
+ args: args,
39
+ event_name: event_name,
40
+ model_id: id,
41
+ model_type: serializer.resource.collection_name,
42
+ type: :event
43
+ )
44
+ ActionCable.server.broadcast(channel_name, data_to_broadcast)
45
+ end
46
+
47
+ def api_maker_event_channel_name(event_name)
48
+ "api_maker_events_#{api_maker_resource.short_name}_#{id}_#{event_name}"
49
+ end
50
+
51
+ def api_maker_broadcast_create
52
+ serializer = ApiMaker::Serializer.new(model: self)
53
+ data_to_broadcast = ApiMaker::ResultParser.parse(
54
+ model: self,
55
+ model_class_name: self.class.name,
56
+ model_id: id,
57
+ model_type: serializer.resource.collection_name,
58
+ type: :create
59
+ )
60
+ ActionCable.server.broadcast(self.class.api_maker_broadcast_create_channel_name, data_to_broadcast)
61
+ end
62
+
63
+ def api_maker_broadcast_destroy
64
+ serializer = ApiMaker::Serializer.new(model: self)
65
+ data_to_broadcast = ApiMaker::ResultParser.parse(
66
+ model: self,
67
+ model_id: id,
68
+ model_type: serializer.resource.collection_name,
69
+ type: :destroy
70
+ )
71
+ ActionCable.server.broadcast(api_maker_broadcast_destroy_channel_name, data_to_broadcast)
72
+ end
73
+
74
+ def api_maker_broadcast_destroy_channel_name
75
+ @api_maker_broadcast_destroy_channel_name ||= "api_maker_destroys_#{api_maker_resource.short_name}_#{id}"
76
+ end
77
+
78
+ def api_maker_broadcast_update
79
+ serializer = ApiMaker::Serializer.new(model: self)
80
+ data_to_broadcast = ApiMaker::ResultParser.parse(
81
+ model: self,
82
+ model_id: id,
83
+ model_type: serializer.resource.collection_name,
84
+ type: :update
85
+ )
86
+ ActionCable.server.broadcast(api_maker_broadcast_update_channel_name, data_to_broadcast)
87
+ end
88
+
89
+ def api_maker_broadcast_update_channel_name
90
+ @api_maker_broadcast_update_channel_name ||= "api_maker_updates_#{api_maker_resource.short_name}_#{id}"
91
+ end
92
+
93
+ def api_maker_resource
94
+ @api_maker_resource ||= self.class.api_maker_resource
95
+ end
96
+ end
@@ -0,0 +1,12 @@
1
+ class ApiMaker::PermittedParamsArgument
2
+ attr_reader :command, :model
3
+
4
+ def initialize(command:, model:)
5
+ @command = command
6
+ @model = model
7
+ end
8
+
9
+ def params
10
+ command.args
11
+ end
12
+ end
@@ -0,0 +1,91 @@
1
+ class ApiMaker::Preloader
2
+ def initialize(ability: nil, args: nil, collection:, data:, include_param:, records:, select:)
3
+ @ability = ability
4
+ @args = args
5
+ @collection = collection
6
+ @data = data
7
+ @include_param = include_param
8
+ @records = records
9
+ @select = select
10
+ end
11
+
12
+ def fill_data
13
+ parsed = ApiMaker::RelationshipIncluder.parse(@include_param)
14
+ return unless parsed
15
+
16
+ parsed.each do |key, value|
17
+ next unless key
18
+
19
+ reflection = @collection.model.reflections[key]
20
+ raise "Unknown reflection: #{@collection.model.name}##{key}" unless reflection
21
+
22
+ fill_empty_relationships_for_key(reflection, key)
23
+ preload_class = preload_class_for_key(reflection)
24
+
25
+ preload_result = ApiMaker::Configuration.profile("Preloading #{reflection.klass.name} with #{preload_class.name}") do
26
+ preload_class.new(
27
+ ability: @ability,
28
+ args: @args,
29
+ collection: @collection,
30
+ data: @data,
31
+ records: @records,
32
+ reflection: reflection,
33
+ select: @select
34
+ ).preload
35
+ end
36
+
37
+ next if value.blank? || preload_result.fetch(:collection).empty?
38
+
39
+ ApiMaker::Preloader.new(
40
+ ability: @ability,
41
+ args: @args,
42
+ data: @data,
43
+ collection: preload_result.fetch(:collection),
44
+ include_param: value,
45
+ records: @data.fetch(:included),
46
+ select: @select
47
+ ).fill_data
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ def fill_empty_relationships_for_key(reflection, key)
54
+ if @records.is_a?(Hash)
55
+ collection_name = ApiMaker::MemoryStorage.current.resource_for_model(reflection.active_record).collection_name
56
+ records_to_set = @records.fetch(collection_name).values
57
+ else
58
+ records_to_set = @records.select { |record| record.model.class == reflection.active_record }
59
+ end
60
+
61
+ case reflection.macro
62
+ when :has_many
63
+ records_to_set.each do |model|
64
+ model.relationships[key.to_sym] ||= []
65
+ end
66
+ when :belongs_to
67
+ records_to_set.each do |model|
68
+ model.relationships[key.to_sym] ||= nil
69
+ end
70
+ when :has_one
71
+ records_to_set.each do |model|
72
+ model.relationships[key.to_sym] ||= nil
73
+ end
74
+ else
75
+ raise "Unknown macro: #{reflection.macro}"
76
+ end
77
+ end
78
+
79
+ def preload_class_for_key(reflection)
80
+ case reflection.macro
81
+ when :has_many
82
+ ApiMaker::PreloaderHasMany
83
+ when :belongs_to
84
+ ApiMaker::PreloaderBelongsTo
85
+ when :has_one
86
+ ApiMaker::PreloaderHasOne
87
+ else
88
+ raise "Unknown macro: #{reflection.macro}"
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,58 @@
1
+ class ApiMaker::PreloaderBelongsTo
2
+ def initialize(ability:, args:, data:, collection:, records:, reflection:, select:)
3
+ @ability = ability
4
+ @args = args
5
+ @data = data
6
+ @collection = collection
7
+ @reflection = reflection
8
+ @reflection_name = @reflection.name
9
+ @records = records
10
+ @select = select
11
+ end
12
+
13
+ def preload
14
+ models.each do |model|
15
+ records_for_model(model).each do |record|
16
+ record.relationships[@reflection_name] = model.id
17
+ end
18
+
19
+ serializer = ApiMaker::Serializer.new(ability: @ability, args: @args, model: model, select: @select&.dig(model.class))
20
+ collection_name = serializer.resource.collection_name
21
+
22
+ @data.fetch(:included)[collection_name] ||= {}
23
+ @data.fetch(:included).fetch(collection_name)[model.id] ||= serializer
24
+ end
25
+
26
+ {collection: models}
27
+ end
28
+
29
+ private
30
+
31
+ def collection_name
32
+ @collection_name = ApiMaker::MemoryStorage.current.resource_for_model(@reflection.active_record).collection_name
33
+ end
34
+
35
+ def model_class
36
+ @model_class ||= @reflection.klass
37
+ end
38
+
39
+ def models
40
+ @models ||= begin
41
+ models = @reflection.klass.where(look_up_key => @collection.map(&@reflection.foreign_key.to_sym).uniq)
42
+ models = models.accessible_by(@ability) if @ability
43
+ models.load
44
+ models
45
+ end
46
+ end
47
+
48
+ def look_up_key
49
+ @look_up_key ||= @reflection.options[:primary_key] || @reflection.klass.primary_key
50
+ end
51
+
52
+ def records_for_model(model)
53
+ @records
54
+ .fetch(collection_name)
55
+ .values
56
+ .select { |record| record.model.read_attribute(@reflection.foreign_key) == model.read_attribute(look_up_key) }
57
+ end
58
+ end