hadouken-json 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/hadouken/decorator.rb +66 -0
- data/lib/hadouken/json.rb +34 -0
- data/lib/hadouken/sql_builder.rb +111 -0
- data/lib/hadouken/virtus/active_record_relation.rb +15 -0
- data/lib/hadouken-json.rb +6 -0
- metadata +81 -0
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
|
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: []
|