api_maker 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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