json2sql 1.0.6 → 1.0.8

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d0e99fbd9cb204b8c8ab64c96c810433474f649cdd80af3b5c6e8f38c8f4f57f
4
- data.tar.gz: 3a9ab7dfbee2f9b7509b320edd460b2d8ca177552bfccfac7b10164366628f1a
3
+ metadata.gz: 20cb67d29822ef620acb57a06b9f5cb04c9e01139a83e264567fb595d6e02d1b
4
+ data.tar.gz: 6dbbf3faaa752c434a71a9f1a34d8fc4ca11aac7f104e7e7752d93b66357c902
5
5
  SHA512:
6
- metadata.gz: de83391fec39ab3b474f4af3a360e0c842e2945c12fcee347507963b397c97b3b9157387758f1a07e9f5bb33e6e7ea675e5fb9f40c9365383c19776fc1c280fd
7
- data.tar.gz: e6a6a70bbde21691e4ae1c1991ef4dabeaa4c0596f20a97a71886cf232aeb0b1484aaa2fe3016257d520f8d0aa6e4366a3e90deec85d9671f79d9c9ddcb4c6c9
6
+ metadata.gz: 2117b66124364adedd912a28667416e2e09da573e050dde88f2c94eab66ceb0ec25ccec36d8c88a7f7e7e81332b42bf897f4ed9bc12ed60c2540a2c06ad17159
7
+ data.tar.gz: 96d7046e9c69b7dd66a4c6dbdd1b190dc805c230d450db60c6d3b8f4b634f0e5aac6995080e26a5e1d880ad15fa9b08e08828ee0b6141ba6102fb78502bbe846
@@ -0,0 +1,122 @@
1
+ module Json2sql
2
+
3
+ # Sanitizes a query input Hash before passing it to any Runner.
4
+ #
5
+ # Parameters:
6
+ # mode: :allow (default) — only tables listed in `tables:` are accessible.
7
+ # Tables absent from `tables:` are blocked entirely.
8
+ # Empty `tables:` = no restriction.
9
+ # :deny — all tables pass; only listed columns are stripped.
10
+ # tables: Per-table configuration Hash:
11
+ # { table_name => { columns: [...], where: { "and" => { col => val } } } }
12
+ # columns: column list — filtered by `mode` (allowed or denied).
13
+ # nil or absent = no column restriction for that table.
14
+ # where: server-side conditions merged into "and". Forced keys overwrite
15
+ # user-supplied values — primary IDOR guard.
16
+ #
17
+ # Usage:
18
+ # policy = Json2sql::InputPolicy.new(
19
+ # mode: :allow,
20
+ # tables: {
21
+ # orders: { columns: %w[id total status], where: { "and" => { "user_id" => 42 } } },
22
+ # users: { columns: %w[id name email] }
23
+ # }
24
+ # )
25
+ # safe_input = policy.apply(raw_params)
26
+ # sql = Json2sql::SelectRunner.build(safe_input)
27
+
28
+ class InputPolicy
29
+
30
+ def initialize(mode: :allow, tables: {})
31
+
32
+ @mode = mode
33
+
34
+ @tables = Json2sql.normalize(tables)
35
+ end
36
+
37
+ # Returns a sanitized copy of input ready to pass to any Runner.
38
+ # Runners remain unmodified — they receive only the clean Hash.
39
+
40
+ def apply(input)
41
+
42
+ input = Json2sql.normalize(input)
43
+
44
+ input = filter_tables(input)
45
+
46
+ input.each { |table, params| sanitize_table(params, table) }
47
+
48
+ input
49
+ end
50
+
51
+ private
52
+
53
+ def filter_tables(input)
54
+
55
+ return input if @mode == :deny || @tables.empty?
56
+
57
+ input.select { |table, _| @tables.key?(table) }
58
+ end
59
+
60
+ def sanitize_table(params, table)
61
+
62
+ return unless params.is_a?(Hash)
63
+
64
+ filter_columns(params, table)
65
+
66
+ inject_where(params, table)
67
+
68
+ %w[children parents].each do |relation|
69
+
70
+ next unless params[relation].is_a?(Hash)
71
+
72
+ params[relation].each { |child_table, child_params| sanitize_table(child_params, child_table) }
73
+ end
74
+ end
75
+
76
+ # Filters "columns" using mode (:allow or :deny).
77
+ # Handles Array (SELECT) and Hash (INSERT/UPDATE) column formats.
78
+ # Hash entries (function columns) always pass through in :allow mode.
79
+ # If no column list is defined for the table, columns are untouched.
80
+
81
+ def filter_columns(params, table)
82
+
83
+ columns = params["columns"]
84
+
85
+ return unless columns.is_a?(Array) || columns.is_a?(Hash)
86
+
87
+ list = @tables.dig(table, "columns")
88
+
89
+ return unless list.is_a?(Array)
90
+
91
+ params["columns"] = if @mode == :deny
92
+
93
+ columns.is_a?(Array) \
94
+ ? columns.reject { |c| list.include?(c) } \
95
+ : columns.reject { |k, _| list.include?(k) }
96
+
97
+ else
98
+
99
+ columns.is_a?(Array) \
100
+ ? columns.select { |c| c.is_a?(Hash) || list.include?(c) } \
101
+ : columns.select { |k, _| list.include?(k) }
102
+
103
+ end
104
+ end
105
+
106
+ # Merges forced "and" conditions into params["and"].
107
+ # Forced keys overwrite user-supplied values for the same column,
108
+ # preventing IDOR (e.g. attacker cannot override user_id).
109
+
110
+ def inject_where(params, table)
111
+
112
+ forced_and = @tables.dig(table, "where", "and")
113
+
114
+ return unless forced_and.is_a?(Hash)
115
+
116
+ params["and"] ||= {}
117
+
118
+ params["and"].merge!(forced_and)
119
+ end
120
+
121
+ end
122
+ end
@@ -8,6 +8,8 @@ module Json2sql
8
8
  # Values:
9
9
  # Integer / Float → inserted as raw numbers
10
10
  # String → wrapped in single quotes with SQL escaping
11
+ #
12
+ # Auto-injected when absent: created_at and updated_at → NOW()
11
13
 
12
14
  class InsertModel
13
15
 
@@ -20,28 +22,41 @@ module Json2sql
20
22
 
21
23
  def build(params)
22
24
 
25
+ columns = params["columns"]
26
+
27
+ return unless columns.is_a?(Hash)
28
+
29
+ columns = build_timestamps(columns)
30
+
23
31
  @sql << "INSERT INTO "
24
32
 
25
33
  @sql << Sanitizer.keyword_wrap(@table)
26
34
 
27
35
  @sql << " ("
28
36
 
29
- build_columns(params)
37
+ build_columns(columns)
30
38
 
31
39
  @sql << ") VALUES ("
32
40
 
33
- build_values(params)
41
+ build_values(columns)
34
42
 
35
43
  @sql << ")"
36
44
  end
37
45
 
38
46
  private
39
47
 
40
- def build_columns(params)
48
+ def build_timestamps(columns)
41
49
 
42
- columns = params["columns"]
50
+ timestamps = {}
43
51
 
44
- return unless columns.is_a?(Hash)
52
+ timestamps["created_at"] = :now unless columns.key?("created_at")
53
+
54
+ timestamps["updated_at"] = :now unless columns.key?("updated_at")
55
+
56
+ timestamps.empty? ? columns : columns.merge(timestamps)
57
+ end
58
+
59
+ def build_columns(columns)
45
60
 
46
61
  separator = false
47
62
 
@@ -55,11 +70,7 @@ module Json2sql
55
70
  end
56
71
  end
57
72
 
58
- def build_values(params)
59
-
60
- columns = params["columns"]
61
-
62
- return unless columns.is_a?(Hash)
73
+ def build_values(columns)
63
74
 
64
75
  separator = false
65
76
 
@@ -73,6 +84,7 @@ module Json2sql
73
84
  when Float then @sql << value.to_s
74
85
  when Integer then @sql << value.to_s
75
86
  when String then @sql << Sanitizer.value_wrap(value)
87
+ when :now then @sql << "NOW()"
76
88
  end
77
89
  end
78
90
  end
@@ -8,6 +8,8 @@ module Json2sql
8
8
  # "or" => { ... } – WHERE conditions (OR)
9
9
  #
10
10
  # Value types follow the same rules as InsertModel.
11
+ #
12
+ # Auto-injected when absent: updated_at → NOW()
11
13
 
12
14
  class UpdateModel
13
15
 
@@ -22,31 +24,40 @@ module Json2sql
22
24
 
23
25
  def build(params)
24
26
 
27
+ columns = params["columns"]
28
+
29
+ return unless columns.is_a?(Hash)
30
+
31
+ columns = build_timestamp(columns)
32
+
25
33
  @sql << "UPDATE "
26
34
 
27
35
  @sql << Sanitizer.keyword_wrap(@table)
28
36
 
29
37
  @sql << " SET "
30
-
31
- build_columns(params)
38
+
39
+ build_columns(columns)
32
40
 
33
41
  WhereModel.new(@sql, @table, @relation).build(params)
34
42
  end
35
43
 
36
44
  private
37
45
 
38
- def build_columns(params)
46
+ def build_timestamp(columns)
39
47
 
40
- columns = params["columns"]
48
+ return columns if columns.key?("updated_at")
41
49
 
42
- return unless columns.is_a?(Hash)
50
+ columns.merge("updated_at" => :now)
51
+ end
52
+
53
+ def build_columns(columns)
43
54
 
44
55
  separator = false
45
56
 
46
57
  columns.each do |key, value|
47
58
 
48
59
  @sql << ", " if separator
49
-
60
+
50
61
  separator = true
51
62
 
52
63
  column = key.to_s
@@ -61,6 +72,7 @@ module Json2sql
61
72
  when Float then @sql << value.to_s
62
73
  when Integer then @sql << value.to_s
63
74
  when String then @sql << Sanitizer.value_wrap(value)
75
+ when :now then @sql << "NOW()"
64
76
  end
65
77
  end
66
78
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Json2sql
4
- VERSION = "1.0.6"
4
+ VERSION = "1.0.8"
5
5
  end
data/lib/json2sql.rb CHANGED
@@ -10,6 +10,7 @@ require_relative "json2sql/update_model"
10
10
  require_relative "json2sql/update_runner"
11
11
  require_relative "json2sql/delete_model"
12
12
  require_relative "json2sql/delete_runner"
13
+ require_relative "json2sql/input_policy"
13
14
 
14
15
  # Json2sql — SQL builder that generates MySQL/MariaDB query strings from
15
16
  # plain Ruby Hashes (or parsed JSON).
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: json2sql
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.6
4
+ version: 1.0.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tiago da Silva
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2026-05-03 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: minitest
@@ -50,6 +49,7 @@ files:
50
49
  - lib/json2sql.rb
51
50
  - lib/json2sql/delete_model.rb
52
51
  - lib/json2sql/delete_runner.rb
52
+ - lib/json2sql/input_policy.rb
53
53
  - lib/json2sql/insert_model.rb
54
54
  - lib/json2sql/insert_runner.rb
55
55
  - lib/json2sql/sanitizer.rb
@@ -66,7 +66,6 @@ licenses:
66
66
  metadata:
67
67
  homepage_uri: https://github.com/tyagoy/json2sql
68
68
  source_code_uri: https://github.com/tyagoy/json2sql
69
- post_install_message:
70
69
  rdoc_options: []
71
70
  require_paths:
72
71
  - lib
@@ -81,8 +80,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
81
80
  - !ruby/object:Gem::Version
82
81
  version: '0'
83
82
  requirements: []
84
- rubygems_version: 3.4.10
85
- signing_key:
83
+ rubygems_version: 3.6.7
86
84
  specification_version: 4
87
85
  summary: Translates Ruby Hashes (or parsed JSON) into MySQL/MariaDB query strings.
88
86
  test_files: []