hadouken-json 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.
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: []