hadouken-json 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 3f7db7ba69d3b1b011d3c3473d8f8ed062b7db7d81c7e2dd0b579f0fa6aa71ac
4
+ data.tar.gz: 76bdcafe07baa2e145dda9bc628cfa3c3b95e957e7db14fe496d2c942dd24093
5
+ SHA512:
6
+ metadata.gz: fd3597acf4b95916dbd9784ba5c55acf423e18086d29a84210ab40f73e904af48936a8f264be588bef2842d1bf4d8e44581ee3e6b435877cb4fdbf3d3c7104f3
7
+ data.tar.gz: 1c40b20ad5176b88b7b8b173136e057e9f59a06b71c96144b72b575337b26f1b77344cb9cc8bf46ce32b4abe3dcbaea67e7dcd7c96e842c7f157bddbf6bdce0b
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Hadouken::Decorator
4
+ attr_accessor :relation
5
+ class << self
6
+ def columns(column_names, *args)
7
+ @@column_names = column_names.is_a?(Array) ? column_names.map(&:to_s) : []
8
+ @@mapping = args[0].is_a?(Hash) ? args[0] : {}
9
+
10
+ fail "Please specify column names for #{self.class}" if @@column_names.empty?
11
+ fail "Please specify mapping of a columns for #{self.class}" if @@mapping.empty?
12
+ end
13
+ end
14
+
15
+ def initialize(relation)
16
+ @relation = relation
17
+ end
18
+
19
+ def valid?
20
+ @@column_names.present?
21
+ end
22
+
23
+ def join_sql
24
+ "LEFT OUTER JOIN \"#{table_name}\" ON #{join_condition}"
25
+ end
26
+
27
+ def columns_sql
28
+ columns_to_exclude = (@@mapping.keys+@@mapping.values).map(&:to_s)
29
+ (@@column_names - columns_to_exclude).map do |column|
30
+ "\"#{table_name}\".\"#{column.downcase}\" AS #{column}"
31
+ end
32
+ end
33
+
34
+ def data
35
+ fail "#{self.class} does not implement #data"
36
+ end
37
+
38
+ def data_table_sql
39
+ "WITH #{table_name}(#{@@column_names.join(', ')}) AS ( VALUES #{values_sql} ) "
40
+ end
41
+
42
+
43
+ private
44
+
45
+ def join_condition
46
+ @@mapping.map { |decorator_column, relation_column|
47
+ "\"#{@relation.table_name}\".\"#{relation_column}\" = \"#{table_name}\".\"#{decorator_column}\""
48
+ }.join(' AND ')
49
+ end
50
+
51
+ def sanitize(value)
52
+ ActiveRecord::Base::sanitize_sql(value)
53
+ end
54
+
55
+ def values_sql
56
+ values.map { |vals| "(#{vals.map{|v| "'#{sanitize(v)}'"}.join(', ') })" }.join(', ')
57
+ end
58
+
59
+ def values
60
+ data.present? ? data : [ @@column_names.map{|_| ''} ]
61
+ end
62
+
63
+ def table_name
64
+ self.class.name.underscore
65
+ end
66
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Hadouken::Json
4
+ include ::Virtus.model
5
+ attribute :relation, Hadouken::Virtus::ActiveRecordRelation
6
+
7
+ def self.call(*args)
8
+ new(*args).call
9
+ end
10
+
11
+ def call
12
+ execute_query array_of(structure)
13
+ end
14
+
15
+ protected
16
+
17
+ def array_of(json_schema, *args)
18
+ options = args[0] || {}
19
+
20
+ Hadouken::SqlBuilder.call(
21
+ main_class: relation.klass,
22
+ scope: options[:for],
23
+ schema: json_schema,
24
+ decorator: (options[:decorate_with].is_a?(Hadouken::Decorator) ? options[:decorate_with] : nil)
25
+ )
26
+ end
27
+
28
+ private
29
+
30
+ def execute_query(sql_query)
31
+ ActiveRecord::Base.connection.execute(sql_query).values.first.first
32
+ end
33
+
34
+ end
@@ -0,0 +1,111 @@
1
+ class Hadouken::SqlBuilder
2
+ include Virtus.model
3
+
4
+ attribute :main_class
5
+ attribute :scope
6
+ attribute :schema, Hash, default: {}
7
+ attribute :decorator, Hadouken::Decorator
8
+ def self.call(*args)
9
+ new(*args).call
10
+ end
11
+
12
+ def call
13
+ return json_build_object_sql(schema) if scope.nil?
14
+
15
+ @sql = ''
16
+ @relation = build_relation
17
+ apply_decorator if decorator&.valid?
18
+ @sql << @relation.select(*columns_to_select).to_sql.gsub(sample_id.to_s, primary_key)
19
+
20
+ "SELECT COALESCE(json_agg(a), '[]'::JSON ) FROM (#{@sql}) a"
21
+ end
22
+
23
+ private
24
+
25
+ def columns_to_select
26
+ columns = []
27
+ columns += static_columns.map { |field, value| "'#{value}' AS '#{field}' " }
28
+ columns += association_columns
29
+ columns += regular_columns.map { |field, column_name| "#{sanitize_column_name(column_name)} AS \"#{field}\"" }
30
+ columns += decorator.columns_sql if decorator&.valid?
31
+ columns += nested_columns.map { |field, column_schema| "(#{sql_for(column_schema)}) AS \"#{field}\"" }
32
+ end
33
+
34
+
35
+ def json_build_object_sql(schema)
36
+ <<~EOQ
37
+ SELECT json_build_object(#{(
38
+ static_columns.map { |field, value| "'#{field}', '#{value}'"} +
39
+ regular_columns.map { |field, data| "'#{field}', (#{data})"} +
40
+ nested_columns.map {|field, column_schema| "'#{field}', (#{sql_for(column_schema)})" }
41
+ ).join(', ')})
42
+ EOQ
43
+ end
44
+
45
+ def apply_decorator
46
+ @sql << decorator.data_table_sql
47
+ @relation.joins!(decorator.join_sql)
48
+ end
49
+
50
+ def build_relation
51
+ fail 'Scope should be ActiveRecord::Relation or string' if [ActiveRecord::Relation, String].none? { |klass| scope.is_a?( klass ) }
52
+
53
+ scope.is_a?(ActiveRecord::Relation) ? scope : main_class.new(id: sample_id).instance_eval(scope)
54
+ end
55
+
56
+ def regular_columns
57
+ schema.extract!(*schema.select{ |_,v| v.is_a?(String) }.keys)
58
+ .inject({}) do |h, (field, column)|
59
+ col = (@relation&.klass&.column_names||[]).include?(column) ? [@relation.klass.table_name, column].join('.') : column
60
+ h.merge(field => col)
61
+ end
62
+ end
63
+
64
+ def nested_columns
65
+ schema.extract!(*schema.select{ |_,v| v.is_a?(Hash) }.keys)
66
+ end
67
+
68
+ def association_columns
69
+ belongs_to_associations = @relation.klass.reflections.select {|_, v| v.is_a?(ActiveRecord::Reflection::BelongsToReflection) }.keys
70
+ split_regex = /^[\.](?<association>#{belongs_to_associations.join('|')})[.]?(?<scope>.+?)[.](?<column>[^.]+)$/
71
+
72
+ cols = schema.extract!(*schema.select{ |_, v| v.to_s.starts_with?('.') }.keys)
73
+
74
+ cols.map do |json_field, relation_field|
75
+ s = relation_field.match(split_regex)
76
+ reflection = @relation.klass.reflections[s[:association]]
77
+ join_table_name = [reflection.table_name, reflection.object_id].join('_')
78
+ @relation.joins!(<<~EOQ
79
+ LEFT OUTER JOIN #{reflection.table_name} #{join_table_name}
80
+ ON "#{join_table_name}"."#{reflection.join_primary_key}" = "#{@relation.table_name}"."#{reflection.join_foreign_key}"
81
+ EOQ
82
+ )
83
+ # Scope can not be merged now because i need to do named joins
84
+ # @relation.merge!(reflection.klass.instance_eval(s[:scope])) if(s[:scope])
85
+ "\"#{join_table_name}\".\"#{s[:column]}\" AS \"#{json_field}\""
86
+ end
87
+
88
+ end
89
+
90
+ def static_columns
91
+ schema.extract!(*schema.select{ |k,_| k.to_s.starts_with?('_') }.keys)
92
+ .inject({}) { |h, (k, v)| h.merge(k.to_s[1..-1] => v) }
93
+ end
94
+
95
+ def sql_for(json_schema)
96
+ self.class.call!(main_class: main_class, schema: json_schema, decorator: decorator)
97
+ end
98
+
99
+ def sample_id
100
+ @sample_id ||= Faker::Number.number(digits: 5)
101
+ end
102
+
103
+ def primary_key
104
+ "\"#{main_class.table_name}\".\"#{main_class.primary_key}\""
105
+ end
106
+
107
+ def sanitize_column_name(column_name)
108
+ column_name.scan(/[.]/).length <= 1 ? "\"#{column_name.split('.').join('"."')}\"" : "(#{column_name})"
109
+ end
110
+
111
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hadouken
4
+ module Virtus
5
+ # Polymorphic class to use for coercing ActiveRecord models
6
+ class ActiveRecordRelation < ::Virtus::Attribute
7
+ def coerce(value)
8
+ # Raise an error if we got something other than a relation or array
9
+ fail ::Virtus::CoercionError.new(value.class, self) unless value.is_a?(ActiveRecord::Relation) || value.is_a?(Array)
10
+
11
+ value
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'hadouken/virtus/active_record_relation'
4
+ require 'hadouken/decorator'
5
+ require 'hadouken/sql_builder'
6
+ require 'hadouken/json'
metadata ADDED
@@ -0,0 +1,81 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: hadouken-json
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Roman Zaytsev
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-08-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: virtus
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 1.0.5
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 1.0.5
27
+ - !ruby/object:Gem::Dependency
28
+ name: activerecord
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 4.0.0
34
+ - - "<="
35
+ - !ruby/object:Gem::Version
36
+ version: 7.0.0
37
+ type: :runtime
38
+ prerelease: false
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: 4.0.0
44
+ - - "<="
45
+ - !ruby/object:Gem::Version
46
+ version: 7.0.0
47
+ description: ''
48
+ email: gnemot@gmail.com
49
+ executables: []
50
+ extensions: []
51
+ extra_rdoc_files: []
52
+ files:
53
+ - lib/hadouken-json.rb
54
+ - lib/hadouken/decorator.rb
55
+ - lib/hadouken/json.rb
56
+ - lib/hadouken/sql_builder.rb
57
+ - lib/hadouken/virtus/active_record_relation.rb
58
+ homepage: https://github.com/nemot/hadouken-json
59
+ licenses:
60
+ - MIT
61
+ metadata: {}
62
+ post_install_message:
63
+ rdoc_options: []
64
+ require_paths:
65
+ - lib
66
+ required_ruby_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: '0'
71
+ required_rubygems_version: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ requirements: []
77
+ rubygems_version: 3.0.8
78
+ signing_key:
79
+ specification_version: 4
80
+ summary: A tool to create a JSON response right inside of PostgreSQL 9.3+
81
+ test_files: []