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 +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: []
|