supabase-rb 3.0.0 → 3.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2589d2d48421b3667b8db24781d8f09d1d3919846e8c08512561c6b9fecd69b7
4
- data.tar.gz: 227b035cb0da6d059a5c43886c8d31801b26ce88b16d3178357c166ceb8c1288
3
+ metadata.gz: 4c6d9216ec9407182b784f962757533f0dcb4bff3be7c7b04a4b2e3ff708878f
4
+ data.tar.gz: cd2aa70a2e15fdb624d0891d23ea5fdcd018eb258f3585a22e70b5736b6e03d1
5
5
  SHA512:
6
- metadata.gz: be23970a53045e60aa4ec5dc8de14731013ddbf08f38c1a0a43694337aefe0d380339c877b773a9e93cbfc4713334abe47da12f4b51948b48f1ea0a0c2355072
7
- data.tar.gz: d533a8d6ad8ef5563ead20e71cf9dfeabeb0d7403a4032330b602135145c440d5c841482ca8c5dba934ed73adb6ccaafdd66c4945c58c30613d12c8051480632
6
+ metadata.gz: 595191ce59fb0c18028b0dc32d4b87fcc3c8a83ba4039cd3bd104c78653935c00ec234c6da85dcda2d173856465270262e2ddce8dc1b3cb8af105259b3727a15
7
+ data.tar.gz: b494cdfd89a8ac25eddc59434545bc705116ff01ea97d06e279c5c88555e35ecea638ab88ac6f3c3381b1ed9f7a3d6276af85e87b4f67f8fe02ce0c20ee89c1d
@@ -1205,7 +1205,7 @@ module Supabase
1205
1205
  payload = decoded[:payload]
1206
1206
 
1207
1207
  aal = payload["aal"]
1208
- amr = payload["amr"] || []
1208
+ amr_entries = (payload["amr"] || []).map { |entry| Types::AMREntry.from_hash(entry) }.compact
1209
1209
 
1210
1210
  verified_factors = (session.user&.factors || []).select { |f| f.status == "verified" }
1211
1211
  next_level = verified_factors.any? ? "aal2" : aal
@@ -1213,7 +1213,7 @@ module Supabase
1213
1213
  Types::AuthMFAGetAuthenticatorAssuranceLevelResponse.new(
1214
1214
  current_level: aal,
1215
1215
  next_level: next_level,
1216
- current_authentication_methods: amr
1216
+ current_authentication_methods: amr_entries
1217
1217
  )
1218
1218
  end
1219
1219
  end
@@ -100,9 +100,11 @@ module Supabase
100
100
  postgrest.rpc(func, params, **opts)
101
101
  end
102
102
 
103
+ # Return a Postgrest client scoped to `name` without mutating self. Matches
104
+ # supabase-py: `client.schema("foo").from_("x")` queries the foo schema but
105
+ # leaves `client.from(...)` (and other call sites) on the default schema.
103
106
  def schema(name)
104
- @postgrest = postgrest.schema(name)
105
- self
107
+ postgrest.schema(name)
106
108
  end
107
109
 
108
110
  # --- Shared auth context -------------------------------------------------
@@ -31,6 +31,24 @@ module Supabase
31
31
  CA_CENTRAL_1, EU_CENTRAL_1, EU_WEST_1, EU_WEST_2, EU_WEST_3,
32
32
  SA_EAST_1, US_EAST_1, US_WEST_1, US_WEST_2
33
33
  ].freeze
34
+
35
+ # PascalCase aliases mirroring supabase-py's FunctionRegion StrEnum, so
36
+ # snippets ported from py (`FunctionRegion.UsEast1`) work without edits.
37
+ Any = ANY
38
+ ApNortheast1 = AP_NORTHEAST_1
39
+ ApNortheast2 = AP_NORTHEAST_2
40
+ ApSouth1 = AP_SOUTH_1
41
+ ApSoutheast1 = AP_SOUTHEAST_1
42
+ ApSoutheast2 = AP_SOUTHEAST_2
43
+ CaCentral1 = CA_CENTRAL_1
44
+ EuCentral1 = EU_CENTRAL_1
45
+ EuWest1 = EU_WEST_1
46
+ EuWest2 = EU_WEST_2
47
+ EuWest3 = EU_WEST_3
48
+ SaEast1 = SA_EAST_1
49
+ UsEast1 = US_EAST_1
50
+ UsWest1 = US_WEST_1
51
+ UsWest2 = US_WEST_2
34
52
  end
35
53
  end
36
54
  end
@@ -64,6 +64,9 @@ module Supabase
64
64
  end
65
65
 
66
66
  alias table from
67
+ # Compat alias for snippets ported from supabase-py where `from` is a
68
+ # reserved keyword and the method is named `from_`.
69
+ alias from_ from
67
70
 
68
71
  # Stored procedure call.
69
72
  # @param func [String] function name
@@ -138,6 +138,34 @@ module Supabase
138
138
  self
139
139
  end
140
140
 
141
+ # Convenience wrappers around the underlying `channel.presence` object,
142
+ # matching supabase-py's `channel.on_presence_sync/join/leave` API. If
143
+ # called after the channel is already joined, the channel resubscribes so
144
+ # the server starts forwarding presence events (presence has to be enabled
145
+ # in the join config — see #default_params).
146
+ def on_presence_sync(&block)
147
+ @presence.on_sync(&block)
148
+ resubscribe_for_presence!
149
+ self
150
+ end
151
+
152
+ def on_presence_join(&block)
153
+ @presence.on_join(&block)
154
+ resubscribe_for_presence!
155
+ self
156
+ end
157
+
158
+ def on_presence_leave(&block)
159
+ @presence.on_leave(&block)
160
+ resubscribe_for_presence!
161
+ self
162
+ end
163
+
164
+ # Shortcut for `channel.presence.state` so callers don't have to drill in.
165
+ def presence_state
166
+ @presence.state
167
+ end
168
+
141
169
  # ----- Outbound -----
142
170
 
143
171
  # Send a custom broadcast message. The server will forward it to other
@@ -214,7 +242,9 @@ module Supabase
214
242
  # Mirrors phoenix.js / supabase-py: every registered on_postgres_changes
215
243
  # listener is serialized into config.postgres_changes on the join payload
216
244
  # so the server filters before sending, instead of shipping every change
217
- # for the topic and forcing the client to drop most of them.
245
+ # for the topic and forcing the client to drop most of them. Also flips
246
+ # config.presence.enabled when any presence callback is attached, so the
247
+ # server starts emitting presence_state/diff frames.
218
248
  def inject_postgres_changes_bindings
219
249
  config = (@join_push.payload["config"] ||= {})
220
250
  config["postgres_changes"] = @postgres_changes_callbacks.map do |binding|
@@ -224,6 +254,21 @@ module Supabase
224
254
  entry["filter"] = binding[:filter] if binding[:filter]
225
255
  entry
226
256
  end
257
+
258
+ presence_cfg = (config["presence"] ||= {})
259
+ presence_cfg["enabled"] = true if @presence.any_callbacks?
260
+ end
261
+
262
+ # If a presence callback is added after the channel is already joined,
263
+ # the server's join config is stale (presence.enabled is still false), so
264
+ # we resubscribe to send a fresh join payload. Matches py's _resubscribe.
265
+ def resubscribe_for_presence!
266
+ return unless joined?
267
+
268
+ unsubscribe
269
+ @joined_once = false
270
+ @join_push.instance_variable_set(:@received_status, nil)
271
+ subscribe(&@subscribe_callback)
227
272
  end
228
273
 
229
274
  def send_push(push, register_pending:)
@@ -88,14 +88,23 @@ module Supabase
88
88
  self
89
89
  end
90
90
 
91
+ # Compat alias mirroring supabase-py's `client.close()`.
92
+ alias close disconnect
93
+
91
94
  def connected?
92
95
  @socket && @socket.connected?
93
96
  end
94
97
 
95
98
  # Get or create a Channel for the given topic. Subsequent calls with the
96
99
  # same topic return the same Channel instance, matching phoenix.js semantics.
100
+ #
101
+ # Topic names are auto-prefixed with `"realtime:"` to match supabase-py:
102
+ # `client.channel("public:users")` reaches the same channel as
103
+ # `client.channel("realtime:public:users")`. Pre-prefixed topics are left
104
+ # alone so existing code keeps working.
97
105
  def channel(topic, params: nil)
98
- @channels[topic] ||= Channel.new(topic, params: params, socket: self)
106
+ full_topic = topic.start_with?("realtime:") ? topic : "realtime:#{topic}"
107
+ @channels[full_topic] ||= Channel.new(full_topic, params: params, socket: self)
99
108
  end
100
109
 
101
110
  def get_channels
@@ -176,6 +185,10 @@ module Supabase
176
185
  end
177
186
  end
178
187
 
188
+ # NOTE: supabase-py exposes this as `client.send(message)`. In Ruby that
189
+ # name would shadow Object#send and break reflective `obj.send(:method)`
190
+ # calls, so the rb port uses `push` instead.
191
+
179
192
  private
180
193
 
181
194
  def attach_socket
@@ -218,7 +231,9 @@ module Supabase
218
231
  return if @heartbeat_interval.nil? || @heartbeat_interval <= 0
219
232
  return if @heartbeat_thread&.alive?
220
233
 
221
- interval = @heartbeat_interval
234
+ # Mirror supabase-py: clamp to a 15s floor so an overeager caller can't
235
+ # hammer the server with sub-15s heartbeats.
236
+ interval = [@heartbeat_interval, 15].max
222
237
  @heartbeat_thread = Thread.new do
223
238
  Thread.current.report_on_exception = false
224
239
  loop do
@@ -50,6 +50,8 @@ module Supabase
50
50
  end
51
51
 
52
52
  alias bucket from
53
+ # Compat alias for snippets ported from supabase-py (`storage.from_(id)`).
54
+ alias from_ from
53
55
 
54
56
  # Iceberg / analytics bucket management. Mirrors storage3's
55
57
  # `SyncStorageClient#analytics`.
@@ -53,9 +53,20 @@ module Supabase
53
53
 
54
54
  # ----- Download -----
55
55
 
56
- def download(path)
56
+ # When `transform:` is provided, the request is routed through the image
57
+ # rendering endpoint (`render/image/authenticated`) and the transform opts
58
+ # are passed as query params. Mirrors supabase-py's DownloadOptions /
59
+ # TransformOptions split.
60
+ def download(path, transform: nil)
61
+ render_path = transform ? %w[render image authenticated] : %w[object]
62
+ query = if transform
63
+ transform.transform_keys(&:to_s).transform_values(&:to_s)
64
+ else
65
+ {}
66
+ end
67
+
57
68
  parts = Utils.relative_path_to_parts(path)
58
- response = _request(:get, ["object", @id, *parts], raw_response: true)
69
+ response = _request(:get, [*render_path, @id, *parts], raw_response: true, query: query)
59
70
  response.body
60
71
  end
61
72
 
@@ -196,7 +207,8 @@ module Supabase
196
207
  send_multipart(:put, ["object", "upload", "sign", @id, *parts],
197
208
  file: file, filename: parts.last, content_type: content_type,
198
209
  cache_control: cache_control, upsert: nil, metadata: metadata,
199
- extra_headers: headers, query: { "token" => token })
210
+ extra_headers: headers, query: { "token" => token },
211
+ relative_path: parts.join("/"))
200
212
  end
201
213
 
202
214
  private
@@ -207,10 +219,11 @@ module Supabase
207
219
  file: file, filename: parts.last,
208
220
  content_type: content_type, cache_control: cache_control,
209
221
  upsert: omit_upsert ? nil : upsert,
210
- metadata: metadata, extra_headers: headers)
222
+ metadata: metadata, extra_headers: headers,
223
+ relative_path: parts.join("/"))
211
224
  end
212
225
 
213
- def send_multipart(method, segments, file:, filename:, content_type:, cache_control:, upsert:, metadata:, extra_headers:, query: nil)
226
+ def send_multipart(method, segments, file:, filename:, content_type:, cache_control:, upsert:, metadata:, extra_headers:, query: nil, relative_path: nil)
214
227
  request_headers = {}
215
228
  request_headers["cache-control"] = "max-age=#{cache_control}" if cache_control
216
229
  request_headers["x-upsert"] = upsert.to_s unless upsert.nil?
@@ -240,7 +253,11 @@ module Supabase
240
253
  response = @session.run_request(method, url, form, merged_headers)
241
254
  raise_for_status(response)
242
255
  parsed = parse_json(response.body) || {}
243
- Types::UploadResponse.from_hash(path: segments[2..].join("/"), key: parsed["Key"])
256
+ # Caller passes the user-facing relative path explicitly: index slicing
257
+ # off `segments` would yield "sign/<bucket>/<path>" for upload_to_signed_url
258
+ # (segments: ["object","upload","sign",@id,*parts]) instead of just <path>.
259
+ upload_path = relative_path || segments[2..].join("/")
260
+ Types::UploadResponse.from_hash(path: upload_path, key: parsed["Key"])
244
261
  end
245
262
 
246
263
  def build_upload_io(file, filename, content_type)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Supabase
4
- VERSION = "3.0.0"
4
+ VERSION = "3.1.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: supabase-rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.0
4
+ version: 3.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Supabase