wowsql-sdk 1.3.0 → 3.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 +4 -4
- data/README.md +359 -417
- data/lib/wowmysql.rb +3 -2
- data/lib/wowsql/auth.rb +313 -164
- data/lib/wowsql/client.rb +54 -25
- data/lib/wowsql/exceptions.rb +15 -14
- data/lib/wowsql/query_builder.rb +229 -120
- data/lib/wowsql/schema.rb +250 -0
- data/lib/wowsql/storage.rb +327 -176
- data/lib/wowsql/table.rb +111 -10
- data/lib/wowsql/version.rb +1 -1
- data/lib/wowsql.rb +2 -2
- metadata +20 -5
data/lib/wowsql/client.rb
CHANGED
|
@@ -5,27 +5,54 @@ require_relative 'table'
|
|
|
5
5
|
|
|
6
6
|
module WOWSQL
|
|
7
7
|
# WOWSQL client for interacting with your database via REST API.
|
|
8
|
-
#
|
|
8
|
+
#
|
|
9
9
|
# This client is used for DATABASE OPERATIONS (CRUD on tables).
|
|
10
10
|
# Use Service Role Key or Anonymous Key for authentication.
|
|
11
|
+
#
|
|
12
|
+
# Key Types:
|
|
13
|
+
# - Service Role Key: Full access to all database operations (server-side)
|
|
14
|
+
# - Anonymous Key: Public access with limited permissions (client-side)
|
|
15
|
+
#
|
|
16
|
+
# @example
|
|
17
|
+
# client = WOWSQL::WOWSQLClient.new(
|
|
18
|
+
# "myproject",
|
|
19
|
+
# "wowbase_service_..."
|
|
20
|
+
# )
|
|
21
|
+
# users = client.table("users").get
|
|
11
22
|
class WOWSQLClient
|
|
12
|
-
|
|
23
|
+
attr_reader :api_url, :api_key, :timeout, :verify_ssl
|
|
24
|
+
|
|
25
|
+
# @param project_url [String] Project subdomain or full URL
|
|
26
|
+
# @param api_key [String] API key for database operations
|
|
27
|
+
# @param base_domain [String] Base domain (default: "wowsql.com")
|
|
28
|
+
# @param secure [Boolean] Use HTTPS (default: true)
|
|
29
|
+
# @param timeout [Integer] Request timeout in seconds (default: 30)
|
|
30
|
+
# @param verify_ssl [Boolean] Verify SSL certificates (default: true)
|
|
31
|
+
def initialize(project_url, api_key, base_domain: 'wowsql.com', secure: true, timeout: 30, verify_ssl: true)
|
|
13
32
|
@api_key = api_key
|
|
14
33
|
@timeout = timeout
|
|
34
|
+
@verify_ssl = verify_ssl
|
|
15
35
|
|
|
16
|
-
# Build API URL
|
|
17
36
|
if project_url.start_with?('http://') || project_url.start_with?('https://')
|
|
18
37
|
base_url = project_url.chomp('/')
|
|
19
38
|
if base_url.include?('/api')
|
|
20
|
-
|
|
39
|
+
base_url = base_url.split('/api').first
|
|
40
|
+
@api_url = "#{base_url}/api/v2"
|
|
21
41
|
else
|
|
22
|
-
@api_url = base_url
|
|
42
|
+
@api_url = "#{base_url}/api/v2"
|
|
23
43
|
end
|
|
24
44
|
else
|
|
25
|
-
|
|
45
|
+
protocol = secure ? 'https' : 'http'
|
|
46
|
+
if project_url.include?(".#{base_domain}") || project_url.end_with?(base_domain)
|
|
47
|
+
@api_url = "#{protocol}://#{project_url}/api/v2"
|
|
48
|
+
else
|
|
49
|
+
@api_url = "#{protocol}://#{project_url}.#{base_domain}/api/v2"
|
|
50
|
+
end
|
|
26
51
|
end
|
|
27
52
|
|
|
28
|
-
|
|
53
|
+
ssl_options = verify_ssl ? {} : { verify: false }
|
|
54
|
+
|
|
55
|
+
@conn = Faraday.new(url: @api_url, ssl: ssl_options) do |f|
|
|
29
56
|
f.request :json
|
|
30
57
|
f.response :json
|
|
31
58
|
f.adapter Faraday.default_adapter
|
|
@@ -37,7 +64,7 @@ module WOWSQL
|
|
|
37
64
|
end
|
|
38
65
|
|
|
39
66
|
# Get a table interface for fluent API.
|
|
40
|
-
#
|
|
67
|
+
#
|
|
41
68
|
# @param table_name [String] Name of the table
|
|
42
69
|
# @return [Table] Table instance for the specified table
|
|
43
70
|
def table(table_name)
|
|
@@ -45,7 +72,7 @@ module WOWSQL
|
|
|
45
72
|
end
|
|
46
73
|
|
|
47
74
|
# List all tables in the database.
|
|
48
|
-
#
|
|
75
|
+
#
|
|
49
76
|
# @return [Array<String>] List of table names
|
|
50
77
|
def list_tables
|
|
51
78
|
response = request('GET', '/tables', nil, nil)
|
|
@@ -53,32 +80,34 @@ module WOWSQL
|
|
|
53
80
|
end
|
|
54
81
|
|
|
55
82
|
# Get table schema information.
|
|
56
|
-
#
|
|
83
|
+
#
|
|
57
84
|
# @param table_name [String] Name of the table
|
|
58
85
|
# @return [Hash] Table schema with columns and primary key
|
|
59
86
|
def get_table_schema(table_name)
|
|
60
87
|
request('GET', "/tables/#{table_name}/schema", nil, nil)
|
|
61
88
|
end
|
|
62
89
|
|
|
90
|
+
# Close the HTTP connection.
|
|
91
|
+
def close
|
|
92
|
+
@conn.close if @conn.respond_to?(:close)
|
|
93
|
+
end
|
|
94
|
+
|
|
63
95
|
# Make HTTP request to API.
|
|
64
96
|
def request(method, path, params = nil, json = nil)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
if response.status >= 400
|
|
72
|
-
error_data = response.body.is_a?(Hash) ? response.body : {}
|
|
73
|
-
error_msg = error_data['detail'] || error_data['message'] || "Request failed with status #{response.status}"
|
|
74
|
-
raise WOWSQLException.new(error_msg, response.status, error_data)
|
|
75
|
-
end
|
|
97
|
+
response = @conn.public_send(method.downcase, path) do |req|
|
|
98
|
+
req.params = params if params
|
|
99
|
+
req.body = json if json
|
|
100
|
+
end
|
|
76
101
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
102
|
+
if response.status >= 400
|
|
103
|
+
error_data = response.body.is_a?(Hash) ? response.body : {}
|
|
104
|
+
error_msg = error_data['detail'] || error_data['message'] || "Request failed with status #{response.status}"
|
|
105
|
+
raise WOWSQLError.new(error_msg, response.status, error_data)
|
|
80
106
|
end
|
|
107
|
+
|
|
108
|
+
response.body
|
|
109
|
+
rescue Faraday::Error => e
|
|
110
|
+
raise WOWSQLError.new("Request failed: #{e.message}")
|
|
81
111
|
end
|
|
82
112
|
end
|
|
83
113
|
end
|
|
84
|
-
|
data/lib/wowsql/exceptions.rb
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
module WOWSQL
|
|
2
|
-
|
|
3
|
-
class WOWSQLException < StandardError
|
|
2
|
+
class WOWSQLError < StandardError
|
|
4
3
|
attr_reader :status_code, :response
|
|
5
4
|
|
|
6
5
|
def initialize(message, status_code = nil, response = nil)
|
|
@@ -10,22 +9,24 @@ module WOWSQL
|
|
|
10
9
|
end
|
|
11
10
|
end
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
end
|
|
12
|
+
WOWSQLException = WOWSQLError
|
|
13
|
+
|
|
14
|
+
class StorageError < WOWSQLError; end
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
super(message, 413, response)
|
|
16
|
+
class StorageLimitExceededError < StorageError
|
|
17
|
+
def initialize(message, status_code = 413, response = nil)
|
|
18
|
+
super(message, status_code, response)
|
|
21
19
|
end
|
|
22
20
|
end
|
|
23
21
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
StorageException = StorageError
|
|
23
|
+
StorageLimitExceededException = StorageLimitExceededError
|
|
24
|
+
|
|
25
|
+
class SchemaPermissionError < WOWSQLError
|
|
26
|
+
def initialize(message, status_code = 403, response = nil)
|
|
27
|
+
super(message, status_code, response)
|
|
28
28
|
end
|
|
29
29
|
end
|
|
30
|
-
end
|
|
31
30
|
|
|
31
|
+
PermissionException = SchemaPermissionError
|
|
32
|
+
end
|
data/lib/wowsql/query_builder.rb
CHANGED
|
@@ -4,152 +4,246 @@ module WOWSQL
|
|
|
4
4
|
def initialize(client, table_name)
|
|
5
5
|
@client = client
|
|
6
6
|
@table_name = table_name
|
|
7
|
-
@
|
|
8
|
-
@select_columns = nil
|
|
9
|
-
@filters = []
|
|
10
|
-
@group_by = nil
|
|
11
|
-
@having = []
|
|
12
|
-
@order_items = nil
|
|
7
|
+
@options = {}
|
|
13
8
|
end
|
|
14
9
|
|
|
10
|
+
# Select specific columns or expressions.
|
|
11
|
+
#
|
|
12
|
+
# @param columns [Array<String>] Column names or expressions
|
|
13
|
+
# @return [QueryBuilder] self for chaining
|
|
15
14
|
def select(*columns)
|
|
16
|
-
|
|
15
|
+
if columns.length == 1 && columns[0].is_a?(Array)
|
|
16
|
+
@options['select'] = columns[0]
|
|
17
|
+
else
|
|
18
|
+
@options['select'] = columns.length > 1 ? columns.map(&:to_s) : (columns.first || '*')
|
|
19
|
+
end
|
|
17
20
|
self
|
|
18
21
|
end
|
|
19
22
|
|
|
20
|
-
# Add a filter condition
|
|
21
|
-
#
|
|
22
|
-
# @param column [String] Column name
|
|
23
|
-
# @param operator [String] Operator (eq, neq, gt, gte, lt, lte, like, in, not_in, between, not_between, is, is_not)
|
|
23
|
+
# Add a filter condition.
|
|
24
|
+
#
|
|
25
|
+
# @param column [String, Hash] Column name or filter hash
|
|
26
|
+
# @param operator [String, nil] Operator (eq, neq, gt, gte, lt, lte, like, in, not_in, between, not_between, is, is_not)
|
|
24
27
|
# @param value [Object] Filter value
|
|
25
|
-
# @param logical_op [String]
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
# @param logical_op [String] "AND" or "OR"
|
|
29
|
+
# @return [QueryBuilder] self for chaining
|
|
30
|
+
def filter(column, operator = nil, value = nil, logical_op: 'AND')
|
|
31
|
+
@options['filter'] ||= []
|
|
32
|
+
|
|
33
|
+
if column.is_a?(Hash)
|
|
34
|
+
if column.key?('column') || column.key?(:column)
|
|
35
|
+
col = column['column'] || column[:column]
|
|
36
|
+
op = column['operator'] || column[:operator]
|
|
37
|
+
val = column['value'] || column[:value]
|
|
38
|
+
lo = column['logical_op'] || column[:logical_op] || logical_op
|
|
39
|
+
@options['filter'] << { column: col, operator: op, value: val, logical_op: lo }
|
|
40
|
+
else
|
|
41
|
+
column.each do |col_name, col_value|
|
|
42
|
+
@options['filter'] << { column: col_name.to_s, operator: 'eq', value: col_value, logical_op: logical_op }
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
else
|
|
46
|
+
raise ArgumentError, "filter() missing required argument: 'operator'" if operator.nil?
|
|
47
|
+
@options['filter'] << { column: column, operator: operator, value: value, logical_op: logical_op }
|
|
48
|
+
end
|
|
49
|
+
|
|
28
50
|
self
|
|
29
51
|
end
|
|
30
52
|
|
|
31
53
|
def eq(column, value)
|
|
32
|
-
|
|
33
|
-
self
|
|
54
|
+
filter(column, 'eq', value)
|
|
34
55
|
end
|
|
35
56
|
|
|
36
57
|
def neq(column, value)
|
|
37
|
-
|
|
38
|
-
self
|
|
58
|
+
filter(column, 'neq', value)
|
|
39
59
|
end
|
|
40
60
|
|
|
41
61
|
def gt(column, value)
|
|
42
|
-
|
|
43
|
-
self
|
|
62
|
+
filter(column, 'gt', value)
|
|
44
63
|
end
|
|
45
64
|
|
|
46
65
|
def gte(column, value)
|
|
47
|
-
|
|
48
|
-
self
|
|
66
|
+
filter(column, 'gte', value)
|
|
49
67
|
end
|
|
50
68
|
|
|
51
69
|
def lt(column, value)
|
|
52
|
-
|
|
53
|
-
self
|
|
70
|
+
filter(column, 'lt', value)
|
|
54
71
|
end
|
|
55
72
|
|
|
56
73
|
def lte(column, value)
|
|
57
|
-
|
|
58
|
-
self
|
|
74
|
+
filter(column, 'lte', value)
|
|
59
75
|
end
|
|
60
76
|
|
|
61
77
|
def like(column, pattern)
|
|
62
|
-
|
|
63
|
-
self
|
|
78
|
+
filter(column, 'like', pattern)
|
|
64
79
|
end
|
|
65
80
|
|
|
66
81
|
def is_null(column)
|
|
67
|
-
|
|
68
|
-
self
|
|
82
|
+
filter(column, 'is', nil)
|
|
69
83
|
end
|
|
70
84
|
|
|
71
85
|
def is_not_null(column)
|
|
72
|
-
|
|
73
|
-
self
|
|
86
|
+
filter(column, 'is_not', nil)
|
|
74
87
|
end
|
|
75
88
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
89
|
+
# Filter where column is in list of values.
|
|
90
|
+
def in_list(column, values)
|
|
91
|
+
filter(column, 'in', values)
|
|
79
92
|
end
|
|
80
93
|
|
|
81
94
|
def not_in(column, values)
|
|
82
|
-
|
|
83
|
-
self
|
|
95
|
+
filter(column, 'not_in', values)
|
|
84
96
|
end
|
|
85
97
|
|
|
86
|
-
def between(column,
|
|
87
|
-
|
|
88
|
-
self
|
|
98
|
+
def between(column, min_val, max_val)
|
|
99
|
+
filter(column, 'between', [min_val, max_val])
|
|
89
100
|
end
|
|
90
101
|
|
|
91
|
-
def not_between(column,
|
|
92
|
-
|
|
93
|
-
self
|
|
102
|
+
def not_between(column, min_val, max_val)
|
|
103
|
+
filter(column, 'not_between', [min_val, max_val])
|
|
94
104
|
end
|
|
95
105
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
106
|
+
# Add an OR filter condition.
|
|
107
|
+
def or_filter(column, operator, value)
|
|
108
|
+
filter(column, operator, value, logical_op: 'OR')
|
|
99
109
|
end
|
|
100
110
|
|
|
101
|
-
|
|
102
|
-
|
|
111
|
+
# Order results by column(s).
|
|
112
|
+
#
|
|
113
|
+
# @param column [String, Array] Column name or array of order items
|
|
114
|
+
# @param direction [String, nil] 'asc' or 'desc'
|
|
115
|
+
# @return [QueryBuilder] self for chaining
|
|
116
|
+
def order_by(column, direction = nil)
|
|
117
|
+
if column.is_a?(String)
|
|
118
|
+
@options['order'] = column
|
|
119
|
+
@options['order_direction'] = direction if direction
|
|
120
|
+
else
|
|
121
|
+
order_items = column.map do |item|
|
|
122
|
+
if item.is_a?(Hash)
|
|
123
|
+
item
|
|
124
|
+
elsif item.is_a?(Array) && item.length == 2
|
|
125
|
+
{ column: item[0], direction: item[1] }
|
|
126
|
+
end
|
|
127
|
+
end.compact
|
|
128
|
+
@options['order'] = order_items
|
|
129
|
+
end
|
|
103
130
|
self
|
|
104
131
|
end
|
|
105
132
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
133
|
+
# Order results by a single column (alias for order_by).
|
|
134
|
+
def order(column, direction = 'asc')
|
|
135
|
+
order_by(column, direction)
|
|
109
136
|
end
|
|
110
137
|
|
|
111
|
-
|
|
112
|
-
|
|
138
|
+
# Limit number of results.
|
|
139
|
+
def limit(n)
|
|
140
|
+
@options['limit'] = n
|
|
113
141
|
self
|
|
114
142
|
end
|
|
115
143
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
@
|
|
144
|
+
# Skip records (pagination).
|
|
145
|
+
def offset(n)
|
|
146
|
+
@options['offset'] = n
|
|
119
147
|
self
|
|
120
148
|
end
|
|
121
149
|
|
|
122
|
-
#
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
150
|
+
# Group results by column(s).
|
|
151
|
+
#
|
|
152
|
+
# @param columns [String, Array<String>] Column(s) to group by
|
|
153
|
+
# @return [QueryBuilder] self for chaining
|
|
154
|
+
def group_by(*columns)
|
|
155
|
+
if columns.length == 1 && columns[0].is_a?(Array)
|
|
156
|
+
@options['group_by'] = columns[0]
|
|
157
|
+
elsif columns.length == 1 && columns[0].is_a?(String)
|
|
158
|
+
@options['group_by'] = [columns[0]]
|
|
159
|
+
else
|
|
160
|
+
@options['group_by'] = columns.map(&:to_s)
|
|
161
|
+
end
|
|
129
162
|
self
|
|
130
163
|
end
|
|
131
164
|
|
|
132
|
-
|
|
133
|
-
|
|
165
|
+
# Add a HAVING clause filter.
|
|
166
|
+
#
|
|
167
|
+
# @param column [String] Column or aggregate expression
|
|
168
|
+
# @param operator [String] Operator (eq, neq, gt, gte, lt, lte)
|
|
169
|
+
# @param value [Object] Filter value
|
|
170
|
+
# @return [QueryBuilder] self for chaining
|
|
171
|
+
def having(column, operator, value)
|
|
172
|
+
@options['having'] ||= []
|
|
173
|
+
@options['having'] << { column: column, operator: operator, value: value }
|
|
134
174
|
self
|
|
135
175
|
end
|
|
136
176
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
177
|
+
# Execute the query.
|
|
178
|
+
#
|
|
179
|
+
# @return [Hash] Query response with data and metadata
|
|
180
|
+
def get(additional_options = nil)
|
|
181
|
+
final_options = @options.dup
|
|
182
|
+
final_options.merge!(additional_options) if additional_options
|
|
183
|
+
|
|
184
|
+
body = {}
|
|
185
|
+
|
|
186
|
+
if final_options['select']
|
|
187
|
+
sel = final_options['select']
|
|
188
|
+
body['select'] = sel.is_a?(String) ? sel.split(',').map(&:strip) : Array(sel)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
if final_options['filter']
|
|
192
|
+
filters = final_options['filter']
|
|
193
|
+
body['filters'] = filters.is_a?(Array) ? filters : [filters]
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
if final_options['group_by']
|
|
197
|
+
gb = final_options['group_by']
|
|
198
|
+
body['group_by'] = gb.is_a?(String) ? gb.split(',').map(&:strip) : Array(gb)
|
|
199
|
+
end
|
|
144
200
|
|
|
145
|
-
if
|
|
146
|
-
|
|
147
|
-
body =
|
|
201
|
+
if final_options['having']
|
|
202
|
+
hv = final_options['having']
|
|
203
|
+
body['having'] = hv.is_a?(Array) ? hv : [hv]
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
if final_options['order']
|
|
207
|
+
ord = final_options['order']
|
|
208
|
+
if ord.is_a?(String)
|
|
209
|
+
body['order_by'] = ord
|
|
210
|
+
body['order_direction'] = final_options['order_direction'] || 'asc'
|
|
211
|
+
else
|
|
212
|
+
body['order_by'] = ord
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
body['limit'] = final_options['limit'] if final_options['limit']
|
|
217
|
+
body['offset'] = final_options['offset'] if final_options['offset']
|
|
218
|
+
|
|
219
|
+
has_advanced = body.key?('group_by') ||
|
|
220
|
+
body.key?('having') ||
|
|
221
|
+
body['order_by'].is_a?(Array) ||
|
|
222
|
+
(body['filters'] || []).any? { |f| %w[in not_in between not_between].include?(f[:operator]) }
|
|
223
|
+
|
|
224
|
+
if has_advanced
|
|
148
225
|
@client.request('POST', "/#{@table_name}/query", nil, body)
|
|
149
226
|
else
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
227
|
+
params = {}
|
|
228
|
+
if body['select']
|
|
229
|
+
params['select'] = body['select'].is_a?(Array) ? body['select'].join(',') : body['select']
|
|
230
|
+
end
|
|
231
|
+
if body['filters']
|
|
232
|
+
filter_strs = body['filters'].map do |f|
|
|
233
|
+
if f[:value].is_a?(Array)
|
|
234
|
+
return @client.request('POST', "/#{@table_name}/query", nil, body)
|
|
235
|
+
end
|
|
236
|
+
"#{f[:column]}.#{f[:operator]}.#{f[:value]}"
|
|
237
|
+
end
|
|
238
|
+
params['filter'] = filter_strs.join(',')
|
|
239
|
+
end
|
|
240
|
+
if body['order_by'].is_a?(String)
|
|
241
|
+
params['order'] = body['order_by']
|
|
242
|
+
params['order_direction'] = body['order_direction'] || 'asc'
|
|
243
|
+
end
|
|
244
|
+
params['limit'] = body['limit'] if body['limit']
|
|
245
|
+
params['offset'] = body['offset'] if body['offset']
|
|
246
|
+
@client.request('GET', "/#{@table_name}", params, nil)
|
|
153
247
|
end
|
|
154
248
|
end
|
|
155
249
|
|
|
@@ -158,59 +252,74 @@ module WOWSQL
|
|
|
158
252
|
get
|
|
159
253
|
end
|
|
160
254
|
|
|
255
|
+
# Get first record matching query.
|
|
256
|
+
#
|
|
257
|
+
# @return [Hash, nil] First record or nil
|
|
161
258
|
def first
|
|
162
259
|
result = limit(1).get
|
|
163
|
-
result['data']
|
|
260
|
+
data = result['data']
|
|
261
|
+
data && !data.empty? ? data[0] : nil
|
|
164
262
|
end
|
|
165
263
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
264
|
+
# Get exactly one record. Raises if zero or more than one found.
|
|
265
|
+
#
|
|
266
|
+
# @return [Hash] The single matching record
|
|
267
|
+
# @raise [WOWSQLError] If zero or more than one record found
|
|
268
|
+
def single
|
|
269
|
+
result = limit(2).get
|
|
270
|
+
data = result['data'] || []
|
|
271
|
+
raise WOWSQLError.new('No records found') if data.empty?
|
|
272
|
+
raise WOWSQLError.new('Multiple records found, expected exactly one') if data.length > 1
|
|
273
|
+
data[0]
|
|
170
274
|
end
|
|
171
275
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
276
|
+
# Get the total count of records matching the current filters.
|
|
277
|
+
#
|
|
278
|
+
# @return [Integer] Number of matching records
|
|
279
|
+
def count
|
|
280
|
+
saved_select = @options['select']
|
|
281
|
+
saved_group = @options.delete('group_by')
|
|
282
|
+
saved_having = @options.delete('having')
|
|
283
|
+
saved_order = @options.delete('order')
|
|
284
|
+
saved_dir = @options.delete('order_direction')
|
|
285
|
+
|
|
286
|
+
@options['select'] = ['COUNT(*) as count']
|
|
287
|
+
|
|
288
|
+
begin
|
|
289
|
+
result = get
|
|
290
|
+
ensure
|
|
291
|
+
if saved_select
|
|
292
|
+
@options['select'] = saved_select
|
|
293
|
+
else
|
|
294
|
+
@options.delete('select')
|
|
190
295
|
end
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
296
|
+
@options['group_by'] = saved_group if saved_group
|
|
297
|
+
@options['having'] = saved_having if saved_having
|
|
298
|
+
@options['order'] = saved_order if saved_order
|
|
299
|
+
@options['order_direction'] = saved_dir if saved_dir
|
|
194
300
|
end
|
|
195
301
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
body
|
|
302
|
+
data = result['data']
|
|
303
|
+
data && !data.empty? ? data[0]['count'].to_i : 0
|
|
200
304
|
end
|
|
201
305
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
306
|
+
# Paginate results with page-based interface.
|
|
307
|
+
#
|
|
308
|
+
# @param page [Integer] Page number (1-indexed)
|
|
309
|
+
# @param per_page [Integer] Records per page
|
|
310
|
+
# @return [Hash] Hash with data, page, per_page, total, total_pages
|
|
311
|
+
def paginate(page: 1, per_page: 20)
|
|
312
|
+
offset_val = ([page, 1].max - 1) * per_page
|
|
313
|
+
result = limit(per_page).offset(offset_val).get
|
|
314
|
+
total = result['total'] || result['count'] || 0
|
|
315
|
+
total_pages = total > 0 ? (total + per_page - 1) / per_page : 0
|
|
316
|
+
{
|
|
317
|
+
'data' => result['data'],
|
|
318
|
+
'page' => page,
|
|
319
|
+
'per_page' => per_page,
|
|
320
|
+
'total' => total,
|
|
321
|
+
'total_pages' => total_pages
|
|
322
|
+
}
|
|
213
323
|
end
|
|
214
324
|
end
|
|
215
325
|
end
|
|
216
|
-
|