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,69 @@
1
+ class ApiMaker::PreloaderHasMany
2
+ def initialize(ability:, args:, data:, collection:, reflection:, records:, select:)
3
+ @ability = ability
4
+ @args = args
5
+ @data = data
6
+ @collection = collection
7
+ @reflection = reflection
8
+ @records = records
9
+ @select = select
10
+
11
+ raise "No inverse of for #{@reflection.active_record.name}##{@reflection.name}" unless @reflection.inverse_of
12
+ end
13
+
14
+ def preload
15
+ models.each do |model|
16
+ preload_model(model)
17
+ end
18
+
19
+ {collection: models}
20
+ end
21
+
22
+ private
23
+
24
+ def models
25
+ @models ||= begin
26
+ if @reflection.is_a?(ActiveRecord::Reflection::ThroughReflection)
27
+ query = ApiMaker::PreloaderThrough.new(collection: @collection, reflection: @reflection).models_query_through_reflection
28
+ else
29
+ primary_key_column = @reflection.options[:primary_key]&.to_sym || @collection.primary_key.to_sym
30
+ query = @reflection.klass.where(@reflection.foreign_key => @collection.map(&primary_key_column))
31
+ query = query.joins(@reflection.inverse_of.name)
32
+ end
33
+
34
+ query = query
35
+ .select(@reflection.klass.arel_table[Arel.star])
36
+ .select(@reflection.active_record.arel_table[@reflection.active_record.primary_key].as("api_maker_origin_id"))
37
+
38
+ query = query.accessible_by(@ability) if @ability
39
+ query.load
40
+ query
41
+ end
42
+ end
43
+
44
+ def collection_name
45
+ @collection_name ||= ApiMaker::MemoryStorage.current.resource_for_model(@reflection.active_record).collection_name
46
+ end
47
+
48
+ def preload_model(model)
49
+ origin_data = find_origin_data_for_model(model)
50
+
51
+ origin_data.fetch(:r)[@reflection.name] ||= []
52
+ origin_data.fetch(:r).fetch(@reflection.name) << model.id
53
+
54
+ serializer = ApiMaker::Serializer.new(ability: @ability, args: @args, model: model, select: @select&.dig(model.class))
55
+ collection_name = serializer.resource.collection_name
56
+
57
+ @data.fetch(:included)[collection_name] ||= {}
58
+ @data.fetch(:included).fetch(collection_name)[model.id] ||= serializer
59
+ end
60
+
61
+ def find_origin_data_for_model(model)
62
+ origin_id = model.read_attribute("api_maker_origin_id")
63
+ origin_data = @records.fetch(collection_name).fetch(origin_id)
64
+
65
+ raise "Couldn't find any origin data by that type (#{collection_name}) and ID (#{origin_id})" unless origin_data
66
+
67
+ origin_data
68
+ end
69
+ end
@@ -0,0 +1,70 @@
1
+ class ApiMaker::PreloaderHasOne
2
+ def initialize(ability:, args:, data:, collection:, reflection:, records:, select: @select)
3
+ @ability = ability
4
+ @args = args
5
+ @data = data
6
+ @collection = collection
7
+ @reflection = reflection
8
+ @records = records
9
+ @select = select
10
+
11
+ raise "Records was nil" unless records
12
+ end
13
+
14
+ def collection_name
15
+ @collection_name ||= ApiMaker::MemoryStorage.current.resource_for_model(@reflection.active_record).collection_name
16
+ end
17
+
18
+ def preload
19
+ models.each do |model|
20
+ ApiMaker::Configuration.profile("Preloading #{model.class.name}##{model.id}") do
21
+ origin_data = origin_data_for_model(model)
22
+ origin_data.fetch(:r)[@reflection.name] = model.id
23
+
24
+ serializer = ApiMaker::Serializer.new(ability: @ability, args: @args, model: model, select: @select&.dig(model.class))
25
+ collection_name = serializer.resource.collection_name
26
+
27
+ @data.fetch(:included)[collection_name] ||= {}
28
+ @data.fetch(:included).fetch(collection_name)[model.id] ||= serializer
29
+ end
30
+ end
31
+
32
+ {collection: models}
33
+ end
34
+
35
+ def models
36
+ @models ||= begin
37
+ if @reflection.is_a?(ActiveRecord::Reflection::ThroughReflection)
38
+ query = query_through
39
+ else
40
+ query = query_normal
41
+ end
42
+
43
+ query = query.accessible_by(@ability) if @ability
44
+ query = query.fix
45
+ query.load
46
+ query
47
+ end
48
+ end
49
+
50
+ def origin_data_for_model(model)
51
+ origin_id = model.read_attribute("api_maker_origin_id")
52
+ @data.fetch(:included).fetch(collection_name).fetch(origin_id)
53
+ end
54
+
55
+ def query_through
56
+ ApiMaker::PreloaderThrough.new(collection: @collection, reflection: @reflection).models_query_through_reflection
57
+ .select(@reflection.klass.arel_table[Arel.star])
58
+ .select(@reflection.active_record.arel_table[@reflection.active_record.primary_key].as("api_maker_origin_id"))
59
+ end
60
+
61
+ def query_normal
62
+ @reflection.klass.where(@reflection.foreign_key => @collection.map(&:id))
63
+ .select(@reflection.klass.arel_table[Arel.star])
64
+ .select(@reflection.klass.arel_table[@reflection.foreign_key].as("api_maker_origin_id"))
65
+ end
66
+
67
+ def resource
68
+ @resource ||= ApiMaker::MemoryStorage.current.resource_for_model(@reflection.klass)
69
+ end
70
+ end
@@ -0,0 +1,101 @@
1
+ class ApiMaker::PreloaderThrough
2
+ def initialize(collection:, reflection:)
3
+ @collection = collection
4
+ @reflection = reflection
5
+ end
6
+
7
+ def models_query_through_reflection
8
+ last_reflection = @reflection.through_reflection.inverse_of
9
+
10
+ if last_reflection
11
+ table_name = last_reflection.table_name
12
+ primary_key = last_reflection.klass.primary_key
13
+ else
14
+ table_name = @reflection.through_reflection.active_record.model_name.plural
15
+ primary_key = @reflection.through_reflection.active_record.primary_key
16
+ end
17
+
18
+ joins_hash = joins_array_to_hash(joins_for_reflection(@reflection))
19
+
20
+ @reflection.klass.joins(joins_hash)
21
+ .where(table_name => {primary_key => @collection.map(&:id)})
22
+ end
23
+
24
+ def joins_for_reflection(current_reflection)
25
+ joins = []
26
+
27
+ loop do
28
+ # Resolve if the through relationship is through multiple other through relationships
29
+ current_reflection = resolve_through(current_reflection)
30
+
31
+ macro = current_reflection.through_reflection.macro
32
+ inverse_name = current_reflection.through_reflection.__send__(:inverse_name)
33
+
34
+ if inverse_name
35
+ joins << inverse_name
36
+ elsif macro == :has_many
37
+ joins << current_reflection.through_reflection.name
38
+ elsif macro == :belongs_to || macro == :has_one
39
+ joins << current_reflection.through_reflection.active_record.model_name.plural.to_sym
40
+ else
41
+ raise "Unknown class: #{current_reflection.through_reflection.class.name}"
42
+ end
43
+
44
+ current_reflection = next_reflection_for(current_reflection)
45
+
46
+ unless current_reflection.is_a?(ActiveRecord::Reflection::ThroughReflection)
47
+ joins.append(append_name_for_current_reflection(current_reflection))
48
+ break
49
+ end
50
+ end
51
+
52
+ joins
53
+ end
54
+
55
+ def append_name_for_current_reflection(current_reflection)
56
+ singular_name = current_reflection.__send__(:inverse_name)&.to_s || current_reflection.active_record.model_name.param_key
57
+ return singular_name.to_sym if @reflection.klass.reflections.key?(singular_name)
58
+
59
+ plural_name = singular_name.pluralize
60
+ return plural_name.to_sym if @reflection.klass.reflections.key?(plural_name)
61
+
62
+ raise "Couldn't find a reflection name #{singular_name} or #{plural_name} on #{@reflection.klass.name}"
63
+ end
64
+
65
+ def joins_array_to_hash(array)
66
+ array = array.clone
67
+
68
+ result = {}
69
+ work_result = result
70
+
71
+ while array.any?
72
+ element = array.pop
73
+
74
+ if array.length == 1
75
+ work_result[element] = array.pop
76
+ else
77
+ work_result[element] = {}
78
+ end
79
+
80
+ work_result = work_result[element]
81
+ end
82
+
83
+ result
84
+ end
85
+
86
+ def next_reflection_for(current_reflection)
87
+ reflection_name = (current_reflection.source_reflection_name || @reflection.name).to_s
88
+
89
+ new_reflection = current_reflection.through_reflection.klass.reflections[reflection_name.pluralize]
90
+ new_reflection ||= current_reflection.through_reflection.klass.reflections[reflection_name.singularize]
91
+
92
+ raise "No such reflection: #{current_reflection.through_reflection.klass.name}##{reflection_name}" unless new_reflection
93
+
94
+ new_reflection
95
+ end
96
+
97
+ def resolve_through(current_reflection)
98
+ current_reflection = current_reflection.through_reflection while current_reflection.through_reflection.is_a?(ActiveRecord::Reflection::ThroughReflection)
99
+ current_reflection
100
+ end
101
+ end
@@ -0,0 +1,14 @@
1
+ class ApiMaker::Railtie < Rails::Railtie
2
+ initializer "watch routes.json for changes and reload Rails routes if changed" do |app|
3
+ file_path = Rails.root.join("app", "javascript", "shared", "routes.json")
4
+
5
+ reloader = app.config.file_watcher.new([file_path]) do
6
+ app.reload_routes!
7
+ end
8
+
9
+ app.reloaders << reloader
10
+ app.reloader.to_run do
11
+ reloader.execute_if_updated
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,42 @@
1
+ class ApiMaker::RelationshipIncluder
2
+ def self.parse(include_param)
3
+ if include_param.nil?
4
+ nil
5
+ elsif include_param.is_a?(String)
6
+ ApiMaker::RelationshipIncluder.parse_string(include_param)
7
+ elsif include_param.is_a?(Array)
8
+ ApiMaker::RelationshipIncluder.parse_array(include_param)
9
+ else
10
+ raise "Unexpected parameter given (#{include_param.class.name}): #{include_param}"
11
+ end
12
+ end
13
+
14
+ def self.parse_string(include_param)
15
+ splitted = include_param.split(".")
16
+ initial = splitted.shift
17
+ rest = splitted.join(".")
18
+ {initial => rest}
19
+ end
20
+
21
+ def self.parse_array(include_param)
22
+ result = {}
23
+ include_param.each do |include_param_i|
24
+ parsed = ApiMaker::RelationshipIncluder.parse(include_param_i)
25
+ parsed.each do |key, value|
26
+ if result.key?(key)
27
+ if result[key].is_a?(String)
28
+ result[key] = [result[key], value]
29
+ elsif result[key].is_a?(Array)
30
+ result[key] << value
31
+ else
32
+ raise "Unknown object: #{result[key].class.name}"
33
+ end
34
+ else
35
+ result[key] = value
36
+ end
37
+ end
38
+ end
39
+
40
+ result
41
+ end
42
+ end
@@ -0,0 +1,8 @@
1
+ class ApiMaker::ResourceRouting
2
+ def self.install_resource_routes(rails_routes, layout: "react", routes: nil)
3
+ routes ||= JSON.parse(File.read(Rails.root.join("app", "javascript", "shared", "routes.json")))
4
+ routes.fetch("routes").each do |route|
5
+ rails_routes.get route.fetch("path") => "#{layout}#show", as: route.fetch("name").to_sym
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,50 @@
1
+ class ApiMaker::ResultParser
2
+ attr_reader :ability, :args
3
+
4
+ def self.parse(*args)
5
+ ApiMaker::ResultParser.new(*args).result
6
+ end
7
+
8
+ def initialize(object, ability: nil, args: nil, controller: nil)
9
+ @object = object
10
+ @ability = ability || controller&.__send__(:current_ability)
11
+ @args = args || controller&.__send__(:api_maker_args) || {}
12
+ end
13
+
14
+ def result
15
+ parse_object(@object)
16
+ end
17
+
18
+ private
19
+
20
+ def parse_object(object)
21
+ if object.is_a?(Hash)
22
+ result = {}
23
+ object.each do |key, value|
24
+ result[key] = parse_object(value)
25
+ end
26
+
27
+ result
28
+ elsif object.is_a?(Array)
29
+ object.map { |value| parse_object(value) }
30
+ elsif object.class.name == "Money"
31
+ {api_maker_type: :money, amount: object.cents, currency: object.currency.iso_code}
32
+ elsif object.is_a?(Date)
33
+ object.iso8601
34
+ elsif object.is_a?(Time)
35
+ object.utc.iso8601
36
+ elsif object.is_a?(ApiMaker::CollectionSerializer) || object.is_a?(ApiMaker::Serializer)
37
+ parse_object(object.as_json)
38
+ elsif object.is_a?(ActiveRecord::Base)
39
+ serializer = ApiMaker::Serializer.new(ability: ability, args: args, model: object)
40
+
41
+ {
42
+ api_maker_type: :model,
43
+ model_name: serializer.resource.collection_name,
44
+ serialized: parse_object(serializer.as_json)
45
+ }
46
+ else
47
+ object
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,86 @@
1
+ class ApiMaker::Serializer
2
+ attr_reader :ability, :args, :model
3
+
4
+ def self.resource_for(klass)
5
+ ApiMaker::MemoryStorage.current.resource_for_model(klass)
6
+ rescue NameError
7
+ nil
8
+ end
9
+
10
+ def self.resource_for!(klass)
11
+ ApiMaker::MemoryStorage.current.resource_for_model(klass)
12
+ end
13
+
14
+ def initialize(ability: nil, args: {}, model:, select: nil)
15
+ @args = args
16
+ @model = model
17
+ @ability = ability
18
+ @select = select
19
+ end
20
+
21
+ def attributes
22
+ ApiMaker::Configuration.profile("attributes") do
23
+ result = {}
24
+ attributes_to_read.each do |attribute, data|
25
+ if (if_name = data.dig(:args, :if))
26
+ condition_result = attribute_value(if_name)
27
+ next unless condition_result
28
+ end
29
+
30
+ result[attribute] = attribute_value(attribute)
31
+ end
32
+
33
+ result
34
+ end
35
+ end
36
+
37
+ def attributes_to_read
38
+ @attributes_to_read ||= @select || resource.default_select
39
+ end
40
+
41
+ def attribute_value(attribute)
42
+ if resource_instance.respond_to?(attribute)
43
+ resource_instance.__send__(attribute)
44
+ else
45
+ model.__send__(attribute)
46
+ end
47
+ end
48
+
49
+ def fetch(*args, &blk)
50
+ result.fetch(*args, &blk)
51
+ end
52
+
53
+ def relationships
54
+ @relationships ||= {}
55
+ end
56
+
57
+ def resource
58
+ @resource ||= ApiMaker::MemoryStorage.current.resource_for_model(model.class)
59
+ end
60
+
61
+ def resource_instance
62
+ @resource_instance ||= resource.new(ability: ability, args: args, model: model)
63
+ end
64
+
65
+ def result
66
+ @result ||= begin
67
+ result = {a: attributes}
68
+ result[:r] = @relationships if @relationships # Only include relationships if set
69
+ result
70
+ end
71
+ end
72
+
73
+ def as_json(_options = nil)
74
+ result
75
+ end
76
+
77
+ def to_json(_options = nil)
78
+ JSON.generate(as_json)
79
+ end
80
+
81
+ def inspect
82
+ "<ApiMaker::Serializer id=\"#{model.id}\" model=\"#{model.class.name}\" relationships=\"#{relationships}\">"
83
+ end
84
+
85
+ alias to_s inspect
86
+ end