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.
Files changed (74) hide show
  1. checksums.yaml +7 -0
  2. data/lib/supabase/README.md +90 -0
  3. data/lib/supabase/auth/README.md +172 -0
  4. data/lib/supabase/auth/admin_api.rb +218 -0
  5. data/lib/supabase/auth/admin_oauth_api.rb +51 -0
  6. data/lib/supabase/auth/api.rb +125 -0
  7. data/lib/supabase/auth/async/admin_api.rb +36 -0
  8. data/lib/supabase/auth/async/admin_oauth_api.rb +15 -0
  9. data/lib/supabase/auth/async/api.rb +32 -0
  10. data/lib/supabase/auth/async/client.rb +33 -0
  11. data/lib/supabase/auth/async.rb +14 -0
  12. data/lib/supabase/auth/client.rb +1217 -0
  13. data/lib/supabase/auth/constants.rb +32 -0
  14. data/lib/supabase/auth/errors.rb +207 -0
  15. data/lib/supabase/auth/helpers.rb +222 -0
  16. data/lib/supabase/auth/memory_storage.rb +25 -0
  17. data/lib/supabase/auth/storage.rb +19 -0
  18. data/lib/supabase/auth/timer.rb +40 -0
  19. data/lib/supabase/auth/types.rb +517 -0
  20. data/lib/supabase/auth/version.rb +7 -0
  21. data/lib/supabase/auth.rb +19 -0
  22. data/lib/supabase/client.rb +200 -0
  23. data/lib/supabase/client_options.rb +82 -0
  24. data/lib/supabase/functions/README.md +71 -0
  25. data/lib/supabase/functions/async/client.rb +45 -0
  26. data/lib/supabase/functions/async.rb +8 -0
  27. data/lib/supabase/functions/client.rb +174 -0
  28. data/lib/supabase/functions/errors.rb +38 -0
  29. data/lib/supabase/functions/types.rb +37 -0
  30. data/lib/supabase/functions/version.rb +7 -0
  31. data/lib/supabase/functions.rb +11 -0
  32. data/lib/supabase/postgrest/README.md +84 -0
  33. data/lib/supabase/postgrest/async/client.rb +50 -0
  34. data/lib/supabase/postgrest/async.rb +8 -0
  35. data/lib/supabase/postgrest/client.rb +136 -0
  36. data/lib/supabase/postgrest/errors.rb +49 -0
  37. data/lib/supabase/postgrest/request_builder.rb +657 -0
  38. data/lib/supabase/postgrest/types.rb +60 -0
  39. data/lib/supabase/postgrest/utils.rb +24 -0
  40. data/lib/supabase/postgrest/version.rb +7 -0
  41. data/lib/supabase/postgrest.rb +13 -0
  42. data/lib/supabase/realtime/README.md +90 -0
  43. data/lib/supabase/realtime/channel.rb +274 -0
  44. data/lib/supabase/realtime/client.rb +182 -0
  45. data/lib/supabase/realtime/errors.rb +19 -0
  46. data/lib/supabase/realtime/message.rb +38 -0
  47. data/lib/supabase/realtime/presence.rb +136 -0
  48. data/lib/supabase/realtime/push.rb +48 -0
  49. data/lib/supabase/realtime/socket.rb +40 -0
  50. data/lib/supabase/realtime/sockets/async_websocket.rb +175 -0
  51. data/lib/supabase/realtime/sockets/websocket_client_simple.rb +94 -0
  52. data/lib/supabase/realtime/test_socket.rb +65 -0
  53. data/lib/supabase/realtime/transformers.rb +26 -0
  54. data/lib/supabase/realtime/types.rb +70 -0
  55. data/lib/supabase/realtime/version.rb +7 -0
  56. data/lib/supabase/realtime.rb +18 -0
  57. data/lib/supabase/storage/README.md +108 -0
  58. data/lib/supabase/storage/analytics.rb +69 -0
  59. data/lib/supabase/storage/async/client.rb +52 -0
  60. data/lib/supabase/storage/async.rb +8 -0
  61. data/lib/supabase/storage/bucket_api.rb +65 -0
  62. data/lib/supabase/storage/client.rb +80 -0
  63. data/lib/supabase/storage/errors.rb +32 -0
  64. data/lib/supabase/storage/file_api.rb +281 -0
  65. data/lib/supabase/storage/request.rb +63 -0
  66. data/lib/supabase/storage/types.rb +236 -0
  67. data/lib/supabase/storage/utils.rb +35 -0
  68. data/lib/supabase/storage/vectors.rb +189 -0
  69. data/lib/supabase/storage/version.rb +7 -0
  70. data/lib/supabase/storage.rb +17 -0
  71. data/lib/supabase/version.rb +5 -0
  72. data/lib/supabase-auth.rb +3 -0
  73. data/lib/supabase.rb +63 -0
  74. 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