supabase-rb 2.0.0
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/supabase/README.md +90 -0
- data/lib/supabase/auth/README.md +172 -0
- data/lib/supabase/auth/admin_api.rb +218 -0
- data/lib/supabase/auth/admin_oauth_api.rb +51 -0
- data/lib/supabase/auth/api.rb +125 -0
- data/lib/supabase/auth/async/admin_api.rb +36 -0
- data/lib/supabase/auth/async/admin_oauth_api.rb +15 -0
- data/lib/supabase/auth/async/api.rb +32 -0
- data/lib/supabase/auth/async/client.rb +33 -0
- data/lib/supabase/auth/async.rb +14 -0
- data/lib/supabase/auth/client.rb +1217 -0
- data/lib/supabase/auth/constants.rb +32 -0
- data/lib/supabase/auth/errors.rb +207 -0
- data/lib/supabase/auth/helpers.rb +222 -0
- data/lib/supabase/auth/memory_storage.rb +25 -0
- data/lib/supabase/auth/storage.rb +19 -0
- data/lib/supabase/auth/timer.rb +40 -0
- data/lib/supabase/auth/types.rb +517 -0
- data/lib/supabase/auth/version.rb +7 -0
- data/lib/supabase/auth.rb +19 -0
- data/lib/supabase/client.rb +200 -0
- data/lib/supabase/client_options.rb +82 -0
- data/lib/supabase/functions/README.md +71 -0
- data/lib/supabase/functions/async/client.rb +45 -0
- data/lib/supabase/functions/async.rb +8 -0
- data/lib/supabase/functions/client.rb +174 -0
- data/lib/supabase/functions/errors.rb +38 -0
- data/lib/supabase/functions/types.rb +37 -0
- data/lib/supabase/functions/version.rb +7 -0
- data/lib/supabase/functions.rb +11 -0
- data/lib/supabase/postgrest/README.md +84 -0
- data/lib/supabase/postgrest/async/client.rb +50 -0
- data/lib/supabase/postgrest/async.rb +8 -0
- data/lib/supabase/postgrest/client.rb +136 -0
- data/lib/supabase/postgrest/errors.rb +49 -0
- data/lib/supabase/postgrest/request_builder.rb +657 -0
- data/lib/supabase/postgrest/types.rb +60 -0
- data/lib/supabase/postgrest/utils.rb +24 -0
- data/lib/supabase/postgrest/version.rb +7 -0
- data/lib/supabase/postgrest.rb +13 -0
- data/lib/supabase/realtime/README.md +90 -0
- data/lib/supabase/realtime/channel.rb +274 -0
- data/lib/supabase/realtime/client.rb +182 -0
- data/lib/supabase/realtime/errors.rb +19 -0
- data/lib/supabase/realtime/message.rb +38 -0
- data/lib/supabase/realtime/presence.rb +136 -0
- data/lib/supabase/realtime/push.rb +48 -0
- data/lib/supabase/realtime/socket.rb +40 -0
- data/lib/supabase/realtime/sockets/async_websocket.rb +175 -0
- data/lib/supabase/realtime/sockets/websocket_client_simple.rb +94 -0
- data/lib/supabase/realtime/test_socket.rb +65 -0
- data/lib/supabase/realtime/transformers.rb +26 -0
- data/lib/supabase/realtime/types.rb +70 -0
- data/lib/supabase/realtime/version.rb +7 -0
- data/lib/supabase/realtime.rb +18 -0
- data/lib/supabase/storage/README.md +108 -0
- data/lib/supabase/storage/analytics.rb +69 -0
- data/lib/supabase/storage/async/client.rb +52 -0
- data/lib/supabase/storage/async.rb +8 -0
- data/lib/supabase/storage/bucket_api.rb +65 -0
- data/lib/supabase/storage/client.rb +80 -0
- data/lib/supabase/storage/errors.rb +32 -0
- data/lib/supabase/storage/file_api.rb +281 -0
- data/lib/supabase/storage/request.rb +63 -0
- data/lib/supabase/storage/types.rb +236 -0
- data/lib/supabase/storage/utils.rb +35 -0
- data/lib/supabase/storage/vectors.rb +189 -0
- data/lib/supabase/storage/version.rb +7 -0
- data/lib/supabase/storage.rb +17 -0
- data/lib/supabase/version.rb +5 -0
- data/lib/supabase-auth.rb +3 -0
- data/lib/supabase.rb +63 -0
- metadata +272 -0
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "faraday"
|
|
5
|
+
|
|
6
|
+
require_relative "errors"
|
|
7
|
+
require_relative "types"
|
|
8
|
+
require_relative "utils"
|
|
9
|
+
|
|
10
|
+
module Supabase
|
|
11
|
+
module Postgrest
|
|
12
|
+
# Internal: groups method, query params, headers, and JSON body for one PostgREST request.
|
|
13
|
+
class RequestConfig
|
|
14
|
+
MAX_RETRIES = 3
|
|
15
|
+
|
|
16
|
+
attr_accessor :session, :path, :http_method, :headers, :params, :json, :retry_enabled
|
|
17
|
+
|
|
18
|
+
# @param session [Faraday::Connection]
|
|
19
|
+
def initialize(session:, path:, http_method:, headers: {}, params: {}, json: nil, retry_enabled: true)
|
|
20
|
+
@session = session
|
|
21
|
+
@path = path
|
|
22
|
+
@http_method = http_method.to_s.upcase
|
|
23
|
+
@headers = headers || {}
|
|
24
|
+
@params = params || {}
|
|
25
|
+
@json = %w[GET HEAD].include?(@http_method) ? nil : json
|
|
26
|
+
@retry_enabled = retry_enabled
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def send_request(additional_headers = {})
|
|
30
|
+
merged_headers = @headers.merge(additional_headers)
|
|
31
|
+
body = @json ? JSON.generate(@json) : nil
|
|
32
|
+
|
|
33
|
+
@session.run_request(@http_method.downcase.to_sym, @path, body, merged_headers) do |req|
|
|
34
|
+
req.params.update(@params) unless @params.empty?
|
|
35
|
+
req.headers["Content-Type"] ||= "application/json" if body
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def should_retry?(response, attempt_count)
|
|
40
|
+
return false unless @retry_enabled
|
|
41
|
+
return false if attempt_count >= MAX_RETRIES
|
|
42
|
+
return false unless %w[GET HEAD].include?(@http_method)
|
|
43
|
+
|
|
44
|
+
[503, 520].include?(response.status)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Internal: prepares default headers + params + method for the four CRUD verbs.
|
|
49
|
+
module RequestPrep
|
|
50
|
+
module_function
|
|
51
|
+
|
|
52
|
+
def unique_columns(rows)
|
|
53
|
+
keys = rows.each_with_object([]) do |row, acc|
|
|
54
|
+
row.each_key { |k| acc << k unless acc.include?(k) }
|
|
55
|
+
end
|
|
56
|
+
keys.map { |k| %("#{k}") }.join(",")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def cleaned_columns(columns)
|
|
60
|
+
quoted = false
|
|
61
|
+
columns.map do |column|
|
|
62
|
+
column.to_s.each_char.each_with_object(+"") do |char, out|
|
|
63
|
+
if char =~ /\s/ && !quoted
|
|
64
|
+
next
|
|
65
|
+
end
|
|
66
|
+
quoted = !quoted if char == '"'
|
|
67
|
+
out << char
|
|
68
|
+
end
|
|
69
|
+
end.join(",")
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def pre_select(*columns, count: nil, head: nil)
|
|
73
|
+
method = head ? Types::RequestMethod::HEAD : Types::RequestMethod::GET
|
|
74
|
+
cleaned = cleaned_columns(columns.empty? ? %w[*] : columns)
|
|
75
|
+
params = { "select" => cleaned }
|
|
76
|
+
headers = count ? { "Prefer" => "count=#{count}" } : {}
|
|
77
|
+
[method, params, headers, {}]
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def pre_insert(json, count:, returning:, upsert:, default_to_null: true)
|
|
81
|
+
prefer = ["return=#{returning}"]
|
|
82
|
+
prefer << "count=#{count}" if count
|
|
83
|
+
prefer << "resolution=merge-duplicates" if upsert
|
|
84
|
+
prefer << "missing=default" unless default_to_null
|
|
85
|
+
|
|
86
|
+
headers = { "Prefer" => prefer.join(",") }
|
|
87
|
+
params = {}
|
|
88
|
+
params["columns"] = unique_columns(json) if json.is_a?(Array) && !json.empty?
|
|
89
|
+
[Types::RequestMethod::POST, params, headers, json]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def pre_upsert(json, count:, returning:, ignore_duplicates:, on_conflict: "", default_to_null: true)
|
|
93
|
+
prefer = ["return=#{returning}"]
|
|
94
|
+
prefer << "count=#{count}" if count
|
|
95
|
+
resolution = ignore_duplicates ? "ignore" : "merge"
|
|
96
|
+
prefer << "resolution=#{resolution}-duplicates"
|
|
97
|
+
prefer << "missing=default" unless default_to_null
|
|
98
|
+
|
|
99
|
+
headers = { "Prefer" => prefer.join(",") }
|
|
100
|
+
params = {}
|
|
101
|
+
params["on_conflict"] = on_conflict if on_conflict && !on_conflict.empty?
|
|
102
|
+
params["columns"] = unique_columns(json) if json.is_a?(Array) && !json.empty?
|
|
103
|
+
[Types::RequestMethod::POST, params, headers, json]
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def pre_update(json, count:, returning:)
|
|
107
|
+
prefer = ["return=#{returning}"]
|
|
108
|
+
prefer << "count=#{count}" if count
|
|
109
|
+
[Types::RequestMethod::PATCH, {}, { "Prefer" => prefer.join(",") }, json]
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def pre_delete(count:, returning:)
|
|
113
|
+
prefer = ["return=#{returning}"]
|
|
114
|
+
prefer << "count=#{count}" if count
|
|
115
|
+
[Types::RequestMethod::DELETE, {}, { "Prefer" => prefer.join(",") }, {}]
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Result of {QueryRequestBuilder#execute}. `data` is an Array of rows (or whatever
|
|
120
|
+
# PostgREST returned). `count` is populated when the request used a count Prefer header.
|
|
121
|
+
class APIResponse
|
|
122
|
+
attr_reader :data, :count
|
|
123
|
+
|
|
124
|
+
def initialize(data:, count: nil)
|
|
125
|
+
@data = data
|
|
126
|
+
@count = count
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def self.from_response(response, request_prefer: nil)
|
|
130
|
+
count = extract_count(response, request_prefer)
|
|
131
|
+
data =
|
|
132
|
+
begin
|
|
133
|
+
body = response.body
|
|
134
|
+
body && !body.empty? ? JSON.parse(body) : []
|
|
135
|
+
rescue JSON::ParserError
|
|
136
|
+
body.to_s.empty? ? [] : body.to_s
|
|
137
|
+
end
|
|
138
|
+
new(data: data, count: count)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def self.extract_count(response, request_prefer)
|
|
142
|
+
return nil unless request_prefer
|
|
143
|
+
return nil unless request_prefer.match?(/count=(?:exact|planned|estimated)/)
|
|
144
|
+
|
|
145
|
+
content_range = response.headers["content-range"] || response.headers["Content-Range"]
|
|
146
|
+
return nil unless content_range
|
|
147
|
+
|
|
148
|
+
total = content_range.split("/").last
|
|
149
|
+
total == "*" ? nil : total.to_i
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Same as APIResponse but the wire was scalar (single-row endpoints).
|
|
154
|
+
class SingleAPIResponse < APIResponse
|
|
155
|
+
def self.from_response(response, request_prefer: nil)
|
|
156
|
+
count = APIResponse.extract_count(response, request_prefer)
|
|
157
|
+
data =
|
|
158
|
+
begin
|
|
159
|
+
body = response.body
|
|
160
|
+
body && !body.empty? ? JSON.parse(body) : []
|
|
161
|
+
rescue JSON::ParserError
|
|
162
|
+
body.to_s.empty? ? [] : body.to_s
|
|
163
|
+
end
|
|
164
|
+
new(data: data, count: count)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Internal: shared execute helper (retry loop + error mapping).
|
|
169
|
+
module RequestExec
|
|
170
|
+
module_function
|
|
171
|
+
|
|
172
|
+
def send_with_retry(request)
|
|
173
|
+
attempt = 0
|
|
174
|
+
loop do
|
|
175
|
+
extra = attempt.positive? ? { "X-Retry-Count" => attempt.to_s } : {}
|
|
176
|
+
response = request.send_request(extra)
|
|
177
|
+
return response if (200..299).include?(response.status)
|
|
178
|
+
return response unless request.should_retry?(response, attempt)
|
|
179
|
+
|
|
180
|
+
sleep(retry_delay(attempt))
|
|
181
|
+
attempt += 1
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def retry_delay(attempt)
|
|
186
|
+
[2**attempt, 30].min
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def parse_error(response)
|
|
190
|
+
body = response.body
|
|
191
|
+
parsed =
|
|
192
|
+
begin
|
|
193
|
+
body && !body.empty? ? JSON.parse(body) : nil
|
|
194
|
+
rescue JSON::ParserError
|
|
195
|
+
nil
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
Errors::APIError.new(parsed || Errors.generate_default_error_message(response))
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Mixin providing PostgREST filter operators (eq, neq, gt, lt, like, in_, contains, …).
|
|
203
|
+
# Methods return self so they're chainable. Mirrors supabase-py's BaseFilterRequestBuilder.
|
|
204
|
+
module FilterMixin
|
|
205
|
+
def not_
|
|
206
|
+
@negate_next = true
|
|
207
|
+
self
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def filter(column, operator, criteria)
|
|
211
|
+
if @negate_next
|
|
212
|
+
@negate_next = false
|
|
213
|
+
operator = "#{Types::Filters::NOT}.#{operator}"
|
|
214
|
+
end
|
|
215
|
+
key = Utils.sanitize_param(column)
|
|
216
|
+
val = "#{operator}.#{criteria}"
|
|
217
|
+
@request.params = add_param(@request.params, key, val)
|
|
218
|
+
self
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def eq(column, value); filter(column, Types::Filters::EQ, value); end
|
|
222
|
+
def neq(column, value); filter(column, Types::Filters::NEQ, value); end
|
|
223
|
+
def gt(column, value); filter(column, Types::Filters::GT, value); end
|
|
224
|
+
def gte(column, value); filter(column, Types::Filters::GTE, value); end
|
|
225
|
+
def lt(column, value); filter(column, Types::Filters::LT, value); end
|
|
226
|
+
def lte(column, value); filter(column, Types::Filters::LTE, value); end
|
|
227
|
+
|
|
228
|
+
def is_(column, value)
|
|
229
|
+
v = value.nil? ? "null" : value
|
|
230
|
+
filter(column, Types::Filters::IS, v)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def like(column, pattern); filter(column, Types::Filters::LIKE, pattern); end
|
|
234
|
+
def ilike(column, pattern); filter(column, Types::Filters::ILIKE, pattern); end
|
|
235
|
+
|
|
236
|
+
def like_all_of(column, pattern); filter(column, Types::Filters::LIKE_ALL, "{#{pattern}}"); end
|
|
237
|
+
def like_any_of(column, pattern); filter(column, Types::Filters::LIKE_ANY, "{#{pattern}}"); end
|
|
238
|
+
def ilike_all_of(column, pattern); filter(column, Types::Filters::ILIKE_ALL, "{#{pattern}}"); end
|
|
239
|
+
def ilike_any_of(column, pattern); filter(column, Types::Filters::ILIKE_ANY, "{#{pattern}}"); end
|
|
240
|
+
|
|
241
|
+
def fts(column, query); filter(column, Types::Filters::FTS, query); end
|
|
242
|
+
def plfts(column, query); filter(column, Types::Filters::PLFTS, query); end
|
|
243
|
+
def phfts(column, query); filter(column, Types::Filters::PHFTS, query); end
|
|
244
|
+
def wfts(column, query); filter(column, Types::Filters::WFTS, query); end
|
|
245
|
+
|
|
246
|
+
def in_(column, values)
|
|
247
|
+
sanitized = values.map { |v| Utils.sanitize_param(v) }.join(",")
|
|
248
|
+
filter(column, Types::Filters::IN, "(#{sanitized})")
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def cs(column, values)
|
|
252
|
+
joined = values.is_a?(Array) ? values.join(",") : values.to_s
|
|
253
|
+
filter(column, Types::Filters::CS, "{#{joined}}")
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def cd(column, values)
|
|
257
|
+
joined = values.is_a?(Array) ? values.join(",") : values.to_s
|
|
258
|
+
filter(column, Types::Filters::CD, "{#{joined}}")
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def contains(column, value)
|
|
262
|
+
case value
|
|
263
|
+
when String
|
|
264
|
+
filter(column, Types::Filters::CS, value)
|
|
265
|
+
when Hash
|
|
266
|
+
filter(column, Types::Filters::CS, JSON.generate(value))
|
|
267
|
+
when Enumerable
|
|
268
|
+
filter(column, Types::Filters::CS, "{#{value.to_a.join(',')}}")
|
|
269
|
+
else
|
|
270
|
+
filter(column, Types::Filters::CS, JSON.generate(value))
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def contained_by(column, value)
|
|
275
|
+
case value
|
|
276
|
+
when String
|
|
277
|
+
filter(column, Types::Filters::CD, value)
|
|
278
|
+
when Hash
|
|
279
|
+
filter(column, Types::Filters::CD, JSON.generate(value))
|
|
280
|
+
when Enumerable
|
|
281
|
+
filter(column, Types::Filters::CD, "{#{value.to_a.join(',')}}")
|
|
282
|
+
else
|
|
283
|
+
filter(column, Types::Filters::CD, JSON.generate(value))
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def ov(column, value)
|
|
288
|
+
case value
|
|
289
|
+
when String
|
|
290
|
+
filter(column, Types::Filters::OV, value)
|
|
291
|
+
when Hash
|
|
292
|
+
filter(column, Types::Filters::OV, JSON.generate(value))
|
|
293
|
+
when Enumerable
|
|
294
|
+
filter(column, Types::Filters::OV, "{#{value.to_a.join(',')}}")
|
|
295
|
+
else
|
|
296
|
+
filter(column, Types::Filters::OV, JSON.generate(value))
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def sl(column, range); filter(column, Types::Filters::SL, "(#{range[0]},#{range[1]})"); end
|
|
301
|
+
def sr(column, range); filter(column, Types::Filters::SR, "(#{range[0]},#{range[1]})"); end
|
|
302
|
+
def nxl(column, range); filter(column, Types::Filters::NXL, "(#{range[0]},#{range[1]})"); end
|
|
303
|
+
def nxr(column, range); filter(column, Types::Filters::NXR, "(#{range[0]},#{range[1]})"); end
|
|
304
|
+
def adj(column, range); filter(column, Types::Filters::ADJ, "(#{range[0]},#{range[1]})"); end
|
|
305
|
+
|
|
306
|
+
def range_lt(column, range); sl(column, range); end
|
|
307
|
+
def range_gt(column, range); sr(column, range); end
|
|
308
|
+
def range_gte(column, range); nxl(column, range); end
|
|
309
|
+
def range_lte(column, range); nxr(column, range); end
|
|
310
|
+
def range_adjacent(column, range); adj(column, range); end
|
|
311
|
+
def overlaps(column, values); ov(column, values); end
|
|
312
|
+
|
|
313
|
+
def match(query)
|
|
314
|
+
raise ArgumentError, "query must contain at least one key-value pair" if query.nil? || query.empty?
|
|
315
|
+
|
|
316
|
+
result = self
|
|
317
|
+
query.each { |k, v| result = eq(k, v) }
|
|
318
|
+
result
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def or_(filters, reference_table: nil)
|
|
322
|
+
key = reference_table ? "#{Utils.sanitize_param(reference_table)}.or" : "or"
|
|
323
|
+
@request.params = add_param(@request.params, key, "(#{filters})")
|
|
324
|
+
self
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def max_affected(value)
|
|
328
|
+
existing = @request.headers["Prefer"] || ""
|
|
329
|
+
prefer = existing.dup
|
|
330
|
+
unless prefer.empty?
|
|
331
|
+
prefer += ",handling=strict" unless prefer.include?("handling=strict")
|
|
332
|
+
end
|
|
333
|
+
prefer = "handling=strict" if prefer.empty?
|
|
334
|
+
prefer += ",max-affected=#{value}"
|
|
335
|
+
@request.headers["Prefer"] = prefer
|
|
336
|
+
self
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
private
|
|
340
|
+
|
|
341
|
+
# PostgREST allows the same query key to appear multiple times (e.g. multiple
|
|
342
|
+
# `order=` or repeated filter columns). Ruby Hash collapses by key, so we
|
|
343
|
+
# store repeats as Arrays — Faraday emits them as multiple query params.
|
|
344
|
+
def add_param(params, key, value)
|
|
345
|
+
existing = params[key]
|
|
346
|
+
params[key] = if existing.is_a?(Array)
|
|
347
|
+
existing + [value]
|
|
348
|
+
elsif existing
|
|
349
|
+
[existing, value]
|
|
350
|
+
else
|
|
351
|
+
value
|
|
352
|
+
end
|
|
353
|
+
params
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
# Mixin providing select-only modifiers (order/limit/offset/range).
|
|
358
|
+
module SelectMixin
|
|
359
|
+
def order(column, desc: false, nullsfirst: nil, foreign_table: nil)
|
|
360
|
+
key = foreign_table ? "#{foreign_table}.order" : "order"
|
|
361
|
+
direction = desc ? "desc" : "asc"
|
|
362
|
+
nulls = nullsfirst.nil? ? "" : ".#{nullsfirst ? 'nullsfirst' : 'nullslast'}"
|
|
363
|
+
new_value = "#{column}.#{direction}#{nulls}"
|
|
364
|
+
|
|
365
|
+
existing = @request.params[key]
|
|
366
|
+
@request.params[key] = existing ? "#{existing},#{new_value}" : new_value
|
|
367
|
+
self
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def limit(size, foreign_table: nil)
|
|
371
|
+
key = foreign_table ? "#{foreign_table}.limit" : "limit"
|
|
372
|
+
@request.params[key] = size
|
|
373
|
+
self
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def offset(size)
|
|
377
|
+
@request.params["offset"] = size
|
|
378
|
+
self
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def range(start, finish, foreign_table: nil)
|
|
382
|
+
offset_key = foreign_table ? "#{foreign_table}.offset" : "offset"
|
|
383
|
+
limit_key = foreign_table ? "#{foreign_table}.limit" : "limit"
|
|
384
|
+
@request.params[offset_key] = start
|
|
385
|
+
@request.params[limit_key] = finish - start + 1
|
|
386
|
+
self
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
# Builder returned by select() / insert() / upsert() / update() / delete() — call #execute to fire.
|
|
391
|
+
class QueryRequestBuilder
|
|
392
|
+
attr_reader :request
|
|
393
|
+
|
|
394
|
+
def initialize(request)
|
|
395
|
+
@request = request
|
|
396
|
+
@negate_next = false
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
def retry(enabled)
|
|
400
|
+
@request.retry_enabled = enabled
|
|
401
|
+
self
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def select(*columns)
|
|
405
|
+
_, params, _, _ = RequestPrep.pre_select(*columns, count: nil)
|
|
406
|
+
@request.params["select"] = params["select"]
|
|
407
|
+
prefer = @request.headers["Prefer"] || ""
|
|
408
|
+
parts = prefer.split(",").reject { |h| h.start_with?("return=") }
|
|
409
|
+
parts << "return=representation"
|
|
410
|
+
@request.headers["Prefer"] = parts.join(",")
|
|
411
|
+
self
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
def execute
|
|
415
|
+
response = RequestExec.send_with_retry(@request)
|
|
416
|
+
if (200..299).include?(response.status)
|
|
417
|
+
APIResponse.from_response(response, request_prefer: @request.headers["Prefer"])
|
|
418
|
+
else
|
|
419
|
+
raise RequestExec.parse_error(response)
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
# Returned by select().single() and rpc().single(); raises if PostgREST doesn't return exactly one row.
|
|
425
|
+
class SingleRequestBuilder
|
|
426
|
+
attr_reader :request
|
|
427
|
+
|
|
428
|
+
def initialize(request)
|
|
429
|
+
@request = request
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
def retry(enabled)
|
|
433
|
+
@request.retry_enabled = enabled
|
|
434
|
+
self
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
def execute
|
|
438
|
+
response = RequestExec.send_with_retry(@request)
|
|
439
|
+
if (200..299).include?(response.status)
|
|
440
|
+
SingleAPIResponse.from_response(response, request_prefer: @request.headers["Prefer"])
|
|
441
|
+
else
|
|
442
|
+
raise RequestExec.parse_error(response)
|
|
443
|
+
end
|
|
444
|
+
end
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
# Returned by select().maybe_single() — yields the row or nil, raises on >1 result.
|
|
448
|
+
class MaybeSingleRequestBuilder
|
|
449
|
+
attr_reader :request
|
|
450
|
+
|
|
451
|
+
def initialize(request)
|
|
452
|
+
@request = request
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def retry(enabled)
|
|
456
|
+
@request.retry_enabled = enabled
|
|
457
|
+
self
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
def execute
|
|
461
|
+
response = RequestExec.send_with_retry(@request)
|
|
462
|
+
unless (200..299).include?(response.status)
|
|
463
|
+
raise RequestExec.parse_error(response)
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
parsed = APIResponse.from_response(response, request_prefer: @request.headers["Prefer"])
|
|
467
|
+
return nil if parsed.data.is_a?(Array) && parsed.data.empty?
|
|
468
|
+
|
|
469
|
+
if parsed.data.is_a?(Array) && parsed.data.length == 1
|
|
470
|
+
SingleAPIResponse.new(data: parsed.data.first, count: parsed.count)
|
|
471
|
+
else
|
|
472
|
+
raise Errors::APIError.new(
|
|
473
|
+
"message" => "Cannot coerce the result to a single JSON object",
|
|
474
|
+
"code" => "406",
|
|
475
|
+
"hint" => "Please check the result set",
|
|
476
|
+
"details" => "The result contains more than one row."
|
|
477
|
+
)
|
|
478
|
+
end
|
|
479
|
+
end
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
# Returned by select().explain() with format: :text — body is the EXPLAIN plan text.
|
|
483
|
+
class ExplainRequestBuilder
|
|
484
|
+
attr_reader :request
|
|
485
|
+
|
|
486
|
+
def initialize(request)
|
|
487
|
+
@request = request
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
def retry(enabled)
|
|
491
|
+
@request.retry_enabled = enabled
|
|
492
|
+
self
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
def execute
|
|
496
|
+
response = RequestExec.send_with_retry(@request)
|
|
497
|
+
return response.body if (200..299).include?(response.status)
|
|
498
|
+
|
|
499
|
+
raise RequestExec.parse_error(response)
|
|
500
|
+
end
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
# The most common builder type — returned by update() and delete(), and used as
|
|
504
|
+
# the base for SelectRequestBuilder. Combines QueryRequestBuilder's execute with
|
|
505
|
+
# filter operators.
|
|
506
|
+
class FilterRequestBuilder < QueryRequestBuilder
|
|
507
|
+
include FilterMixin
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
# Returned by select() — filters + select modifiers + result-shape switchers.
|
|
511
|
+
class SelectRequestBuilder < FilterRequestBuilder
|
|
512
|
+
include SelectMixin
|
|
513
|
+
|
|
514
|
+
def single
|
|
515
|
+
@request.headers["Accept"] = "application/vnd.pgrst.object+json"
|
|
516
|
+
SingleRequestBuilder.new(@request)
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
def maybe_single
|
|
520
|
+
MaybeSingleRequestBuilder.new(@request)
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
def csv
|
|
524
|
+
@request.headers["Accept"] = "text/csv"
|
|
525
|
+
SingleRequestBuilder.new(@request)
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
def text_search(column, query, options = {})
|
|
529
|
+
type_part = case options[:type] || options["type"]
|
|
530
|
+
when "plain" then "pl"
|
|
531
|
+
when "phrase" then "ph"
|
|
532
|
+
when "web_search" then "w"
|
|
533
|
+
else ""
|
|
534
|
+
end
|
|
535
|
+
config = options[:config] || options["config"]
|
|
536
|
+
config_part = config ? "(#{config})" : ""
|
|
537
|
+
@request.params[column.to_s] = "#{type_part}fts#{config_part}.#{query}"
|
|
538
|
+
QueryRequestBuilder.new(@request)
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
def explain(analyze: false, verbose: false, settings: false, buffers: false, wal: false, format: "text")
|
|
542
|
+
options = []
|
|
543
|
+
options << "analyze" if analyze
|
|
544
|
+
options << "verbose" if verbose
|
|
545
|
+
options << "settings" if settings
|
|
546
|
+
options << "buffers" if buffers
|
|
547
|
+
options << "wal" if wal
|
|
548
|
+
@request.headers["Accept"] = "application/vnd.pgrst.plan+#{format}; options=#{options.join('|')}"
|
|
549
|
+
format == "text" ? ExplainRequestBuilder.new(@request) : SingleRequestBuilder.new(@request)
|
|
550
|
+
end
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
# Returned by rpc() — filters + select modifiers + result-shape switchers, returning
|
|
554
|
+
# SingleAPIResponse instead of APIResponse because PostgREST returns a single value.
|
|
555
|
+
class RPCFilterRequestBuilder < SingleRequestBuilder
|
|
556
|
+
include FilterMixin
|
|
557
|
+
include SelectMixin
|
|
558
|
+
|
|
559
|
+
def initialize(request)
|
|
560
|
+
super
|
|
561
|
+
@negate_next = false
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
def select(*columns)
|
|
565
|
+
_, params, _, _ = RequestPrep.pre_select(*columns, count: nil)
|
|
566
|
+
existing = @request.params["select"]
|
|
567
|
+
@request.params["select"] = existing ? "#{existing},#{params['select']}" : params["select"]
|
|
568
|
+
prefer = @request.headers["Prefer"] || ""
|
|
569
|
+
@request.headers["Prefer"] = prefer.empty? ? "return=representation" : "#{prefer},return=representation"
|
|
570
|
+
self
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
def single
|
|
574
|
+
@request.headers["Accept"] = "application/vnd.pgrst.object+json"
|
|
575
|
+
self
|
|
576
|
+
end
|
|
577
|
+
|
|
578
|
+
def maybe_single
|
|
579
|
+
@request.headers["Accept"] = "application/vnd.pgrst.object+json"
|
|
580
|
+
self
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
def csv
|
|
584
|
+
@request.headers["Accept"] = "text/csv"
|
|
585
|
+
self
|
|
586
|
+
end
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
# Entry point for table operations — produced by Client#from(table). Each method
|
|
590
|
+
# builds an appropriate sub-builder (Select / Filter / Query) and returns it for
|
|
591
|
+
# chaining.
|
|
592
|
+
class RequestBuilder
|
|
593
|
+
def initialize(session, path, headers)
|
|
594
|
+
@session = session
|
|
595
|
+
@path = path
|
|
596
|
+
@headers = headers
|
|
597
|
+
end
|
|
598
|
+
|
|
599
|
+
def select(*columns, count: nil, head: nil)
|
|
600
|
+
method, params, headers, json = RequestPrep.pre_select(*columns, count: count, head: head)
|
|
601
|
+
request = RequestConfig.new(
|
|
602
|
+
session: @session, path: @path, http_method: method,
|
|
603
|
+
headers: headers.merge(@headers), params: params, json: json
|
|
604
|
+
)
|
|
605
|
+
SelectRequestBuilder.new(request)
|
|
606
|
+
end
|
|
607
|
+
|
|
608
|
+
def insert(json, count: nil, returning: Types::ReturnMethod::REPRESENTATION,
|
|
609
|
+
upsert: false, default_to_null: true)
|
|
610
|
+
method, params, headers, body = RequestPrep.pre_insert(
|
|
611
|
+
json, count: count, returning: returning, upsert: upsert, default_to_null: default_to_null
|
|
612
|
+
)
|
|
613
|
+
request = RequestConfig.new(
|
|
614
|
+
session: @session, path: @path, http_method: method,
|
|
615
|
+
headers: headers.merge(@headers), params: params, json: body
|
|
616
|
+
)
|
|
617
|
+
QueryRequestBuilder.new(request)
|
|
618
|
+
end
|
|
619
|
+
|
|
620
|
+
def upsert(json, count: nil, returning: Types::ReturnMethod::REPRESENTATION,
|
|
621
|
+
ignore_duplicates: false, on_conflict: "", default_to_null: true)
|
|
622
|
+
method, params, headers, body = RequestPrep.pre_upsert(
|
|
623
|
+
json, count: count, returning: returning,
|
|
624
|
+
ignore_duplicates: ignore_duplicates, on_conflict: on_conflict,
|
|
625
|
+
default_to_null: default_to_null
|
|
626
|
+
)
|
|
627
|
+
request = RequestConfig.new(
|
|
628
|
+
session: @session, path: @path, http_method: method,
|
|
629
|
+
headers: headers.merge(@headers), params: params, json: body
|
|
630
|
+
)
|
|
631
|
+
QueryRequestBuilder.new(request)
|
|
632
|
+
end
|
|
633
|
+
|
|
634
|
+
def update(json, count: nil, returning: Types::ReturnMethod::REPRESENTATION)
|
|
635
|
+
method, params, headers, body = RequestPrep.pre_update(
|
|
636
|
+
json, count: count, returning: returning
|
|
637
|
+
)
|
|
638
|
+
request = RequestConfig.new(
|
|
639
|
+
session: @session, path: @path, http_method: method,
|
|
640
|
+
headers: headers.merge(@headers), params: params, json: body
|
|
641
|
+
)
|
|
642
|
+
FilterRequestBuilder.new(request)
|
|
643
|
+
end
|
|
644
|
+
|
|
645
|
+
def delete(count: nil, returning: Types::ReturnMethod::REPRESENTATION)
|
|
646
|
+
method, params, headers, body = RequestPrep.pre_delete(
|
|
647
|
+
count: count, returning: returning
|
|
648
|
+
)
|
|
649
|
+
request = RequestConfig.new(
|
|
650
|
+
session: @session, path: @path, http_method: method,
|
|
651
|
+
headers: headers.merge(@headers), params: params, json: body
|
|
652
|
+
)
|
|
653
|
+
FilterRequestBuilder.new(request)
|
|
654
|
+
end
|
|
655
|
+
end
|
|
656
|
+
end
|
|
657
|
+
end
|