supabase-rb 3.1.0 → 3.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4c6d9216ec9407182b784f962757533f0dcb4bff3be7c7b04a4b2e3ff708878f
4
- data.tar.gz: cd2aa70a2e15fdb624d0891d23ea5fdcd018eb258f3585a22e70b5736b6e03d1
3
+ metadata.gz: 7490edd0417af9cdc03db6da250e89db6117ff7e8b2e0e57981c993a77e11ad8
4
+ data.tar.gz: dfca661ce05dac328aea1aaaec095c8ee1d82365185c6a6ef3e25383361872ba
5
5
  SHA512:
6
- metadata.gz: 595191ce59fb0c18028b0dc32d4b87fcc3c8a83ba4039cd3bd104c78653935c00ec234c6da85dcda2d173856465270262e2ddce8dc1b3cb8af105259b3727a15
7
- data.tar.gz: b494cdfd89a8ac25eddc59434545bc705116ff01ea97d06e279c5c88555e35ecea638ab88ac6f3c3381b1ed9f7a3d6276af85e87b4f67f8fe02ce0c20ee89c1d
6
+ metadata.gz: e8156e656365854825396e52643c1055ca0b5c608b8365c260cdfbe89f4e71923771eb9b1cb0d73eefd8f9e7bf7b350948edfda829cff331ed5972fd467b4293
7
+ data.tar.gz: 042a0c5e2132027ffbb2dab37fda4e93aae88aca605fba711bade4ca4765d5a269366bfc947ab1ad674462b25562920662fe1c053aebc6b55377688e769df9c6
@@ -67,6 +67,11 @@ module Supabase
67
67
  end
68
68
 
69
69
  # Initialize the client, optionally from a URL or from storage.
70
+ # Equivalent to supabase-py's `client.initialize(url=...)` — `initialize`
71
+ # is the Ruby constructor name, so this rb port uses `#init` instead.
72
+ # An alias `bootstrap` is provided for callers who prefer a verb that
73
+ # doesn't read as a constructor.
74
+ #
70
75
  # @param url [String, nil] optional redirect URL to initialize from
71
76
  def init(url: nil)
72
77
  if url && _is_implicit_grant_flow(url)
@@ -75,6 +80,7 @@ module Supabase
75
80
  initialize_from_storage
76
81
  end
77
82
  end
83
+ alias bootstrap init
78
84
 
79
85
  # Recover session from storage and refresh if needed.
80
86
  def initialize_from_storage
@@ -904,51 +910,29 @@ module Supabase
904
910
  end
905
911
 
906
912
  if @persist_session && session.expires_at
907
- session_data = {
908
- access_token: session.access_token,
909
- refresh_token: session.refresh_token,
910
- token_type: session.token_type,
911
- expires_in: session.expires_in,
912
- expires_at: session.expires_at,
913
- provider_token: session.provider_token,
914
- provider_refresh_token: session.provider_refresh_token
915
- }
916
- if session.user
917
- user = session.user
918
- session_data[:user] = {
919
- id: user.id, aud: user.aud, role: user.role,
920
- email: user.email, phone: user.phone,
921
- email_confirmed_at: user.email_confirmed_at&.iso8601,
922
- phone_confirmed_at: user.phone_confirmed_at&.iso8601,
923
- confirmed_at: user.confirmed_at&.iso8601,
924
- last_sign_in_at: user.last_sign_in_at&.iso8601,
925
- app_metadata: user.app_metadata, user_metadata: user.user_metadata,
926
- identities: user.identities&.map { |i|
927
- {
928
- id: i.id, identity_id: i.identity_id, user_id: i.user_id,
929
- identity_data: i.identity_data, provider: i.provider,
930
- last_sign_in_at: i.last_sign_in_at&.iso8601,
931
- created_at: i.created_at&.iso8601, updated_at: i.updated_at&.iso8601
932
- }
933
- },
934
- factors: user.factors&.map { |f|
935
- {
936
- id: f.id, friendly_name: f.friendly_name, factor_type: f.factor_type,
937
- status: f.status,
938
- created_at: f.created_at&.iso8601, updated_at: f.updated_at&.iso8601
939
- }
940
- },
941
- created_at: user.created_at&.iso8601, updated_at: user.updated_at&.iso8601,
942
- new_email: user.new_email, new_phone: user.new_phone,
943
- invited_at: user.invited_at&.iso8601,
944
- is_anonymous: user.is_anonymous,
945
- confirmation_sent_at: user.confirmation_sent_at&.iso8601,
946
- recovery_sent_at: user.recovery_sent_at&.iso8601,
947
- email_change_sent_at: user.email_change_sent_at&.iso8601,
948
- action_link: user.action_link
949
- }
913
+ @storage.set_item(@storage_key, JSON.generate(_serialize_session(session)))
914
+ end
915
+ end
916
+
917
+ # Recursively dump a Session (and nested User / Identity / Factor structs)
918
+ # to a Hash that JSON.generate accepts. Mirrors py's
919
+ # `session.model_dump_json()` — every Struct member is preserved (not
920
+ # just a hand-picked allow-list), so custom upstream fields round-trip
921
+ # through storage without being silently dropped.
922
+ def _serialize_session(value)
923
+ case value
924
+ when Struct
925
+ value.to_h.each_with_object({}) do |(key, member_value), acc|
926
+ acc[key] = _serialize_session(member_value)
950
927
  end
951
- @storage.set_item(@storage_key, JSON.generate(session_data))
928
+ when Hash
929
+ value.each_with_object({}) { |(k, v), acc| acc[k] = _serialize_session(v) }
930
+ when Array
931
+ value.map { |item| _serialize_session(item) }
932
+ when Time, Date, DateTime
933
+ value.iso8601
934
+ else
935
+ value
952
936
  end
953
937
  end
954
938
 
@@ -30,6 +30,37 @@ module Supabase
30
30
  class Client
31
31
  attr_reader :supabase_url, :supabase_key, :options, :headers
32
32
 
33
+ # Mirrors supabase-py's `Client.create(...)`: builds a client, then — if
34
+ # no explicit Authorization was supplied via options — tries to pull a
35
+ # persisted session via the auth client and applies its access_token as
36
+ # the bearer token. Useful when bootstrapping from a session file the
37
+ # user previously signed into. Any error from get_session is swallowed
38
+ # so the client always returns successfully.
39
+ def self.create(supabase_url:, supabase_key:, options: nil, async: false)
40
+ configured_auth = nil
41
+ if options.is_a?(Supabase::ClientOptions)
42
+ configured_auth = options.headers["Authorization"] || options.headers[:Authorization]
43
+ elsif options.is_a?(Hash)
44
+ global_headers = options[:global]&.dig(:headers) || options.dig("global", "headers") || {}
45
+ configured_auth = global_headers["Authorization"] || global_headers[:Authorization]
46
+ end
47
+
48
+ client = new(supabase_url: supabase_url, supabase_key: supabase_key,
49
+ options: options || {}, async: async)
50
+
51
+ if configured_auth.nil?
52
+ begin
53
+ session = client.auth.get_session
54
+ client.set_auth(session.access_token) if session&.access_token
55
+ rescue StandardError
56
+ # No persisted session, or auth storage unavailable — fall back to
57
+ # the apikey-only bearer that initialize set up.
58
+ end
59
+ end
60
+
61
+ client
62
+ end
63
+
33
64
  def initialize(supabase_url:, supabase_key:, options: {}, async: false)
34
65
  # Use Supabase::SupabaseException once defined; fall back to ArgumentError
35
66
  # during early require cycles. Matches supabase-py's contract.
@@ -63,7 +94,19 @@ module Supabase
63
94
  # --- Sub-clients ---------------------------------------------------------
64
95
 
65
96
  def auth
66
- @auth ||= auth_class.new(url: rest_url_for("auth/v1"), headers: @headers, **sub_options(:auth))
97
+ return @auth if @auth
98
+
99
+ @auth = auth_class.new(url: rest_url_for("auth/v1"), headers: @headers, **sub_options(:auth))
100
+ # Mirror supabase-py's `self.auth.on_auth_state_change(self._listen_to_auth_events)`:
101
+ # when the auth client emits SIGNED_IN / TOKEN_REFRESHED / SIGNED_OUT,
102
+ # propagate the new token to every other sub-client.
103
+ @auth.on_auth_state_change do |event, session|
104
+ next unless %w[SIGNED_IN TOKEN_REFRESHED SIGNED_OUT].include?(event)
105
+
106
+ token = session&.access_token || @supabase_key
107
+ propagate_auth(token)
108
+ end
109
+ @auth
67
110
  end
68
111
 
69
112
  def storage
@@ -126,6 +169,17 @@ module Supabase
126
169
 
127
170
  private
128
171
 
172
+ # Refresh the Authorization header (used by every sub-client other than
173
+ # auth itself, which manages its own headers) and reset the memoized
174
+ # sub-clients so they pick up the new token on next access.
175
+ def propagate_auth(token)
176
+ @headers["Authorization"] = "Bearer #{token}"
177
+ @storage = nil
178
+ @functions = nil
179
+ @postgrest = nil
180
+ @realtime&.set_auth(token)
181
+ end
182
+
129
183
  def auth_class
130
184
  @async ? require_async_class("auth", "Async::Client") : Auth::Client
131
185
  end
@@ -45,6 +45,54 @@ module Supabase
45
45
  @timeout = timeout
46
46
  end
47
47
 
48
+ # Set the Authorization header to either Bearer (token) or Basic
49
+ # (username/password) authentication. Bearer wins if both are supplied.
50
+ # Mirrors supabase-py's BasePostgrestClient.auth().
51
+ #
52
+ # @param token [String, nil]
53
+ # @param username [String, nil]
54
+ # @param password [String]
55
+ # @return [Client] self, so callers can chain.
56
+ def auth(token, username: nil, password: "")
57
+ if token && !token.empty?
58
+ @headers["Authorization"] = "Bearer #{token}"
59
+ elsif username
60
+ credentials = ["#{username}:#{password}"].pack("m0")
61
+ @headers["Authorization"] = "Basic #{credentials}"
62
+ else
63
+ raise ArgumentError, "Neither bearer token nor basic authentication credentials were provided"
64
+ end
65
+ self
66
+ end
67
+
68
+ # Release the underlying HTTP connection. After close, subsequent requests
69
+ # will lazily rebuild the connection on the next call. Mirrors
70
+ # supabase-py's BasePostgrestClient.aclose() / __exit__ hook — Ruby
71
+ # callers use it via `client.close` or `Postgrest::Client.new(...) { |c|
72
+ # ... }` (see ::open below).
73
+ def close
74
+ # Faraday connections own a sub-connection per host through their
75
+ # adapter. We can't force a hard close on most adapters, but dropping
76
+ # our reference frees the connection for GC and ensures the next call
77
+ # rebuilds.
78
+ @session = nil
79
+ @http_client = nil
80
+ self
81
+ end
82
+
83
+ # Block form: yields the client, then closes it. Use to scope a client
84
+ # to a discrete unit of work (matches py's `with SyncPostgrestClient(...)`).
85
+ def self.open(**kwargs)
86
+ client = new(**kwargs)
87
+ return client unless block_given?
88
+
89
+ begin
90
+ yield client
91
+ ensure
92
+ client.close
93
+ end
94
+ end
95
+
48
96
  # Switch schemas. Returns a new client that points at a different postgres schema.
49
97
  # @param name [String]
50
98
  # @return [Client]
@@ -195,6 +195,22 @@ module Supabase
195
195
  self
196
196
  end
197
197
 
198
+ # Public low-level push for arbitrary Phoenix events. Mirrors
199
+ # `supabase-py`'s `channel.push(event, payload, timeout)`. Returns the
200
+ # {Push} instance so callers can attach receive() handlers and observe the
201
+ # reply / timeout. Raises if called before {#subscribe}.
202
+ def push_event(event, payload = {}, timeout: nil)
203
+ unless @joined_once
204
+ raise Errors::RealtimeError,
205
+ "tried to push '#{event}' to '#{@topic}' before joining. Call subscribe() first."
206
+ end
207
+
208
+ ref = @socket&.next_ref
209
+ push = Push.new(self, event, payload, ref: ref, timeout: timeout || Types::DEFAULT_TIMEOUT_SECONDS)
210
+ send_push(push, register_pending: true)
211
+ push
212
+ end
213
+
198
214
  # ----- Inbound dispatch (called by Client) -----
199
215
 
200
216
  # Route a parsed Message to the appropriate listeners. Returns true if the
@@ -4,7 +4,9 @@ require "json"
4
4
  require "uri"
5
5
 
6
6
  require_relative "channel"
7
+ require_relative "errors"
7
8
  require_relative "message"
9
+ require_relative "transformers"
8
10
  require_relative "types"
9
11
  require_relative "version"
10
12
 
@@ -43,6 +45,11 @@ module Supabase
43
45
  def initialize(url:, params: {}, socket: nil, timeout: Types::DEFAULT_TIMEOUT_SECONDS,
44
46
  heartbeat_interval: Types::DEFAULT_HEARTBEAT_INTERVAL_SECONDS,
45
47
  auto_reconnect: true, max_retries: 5, initial_backoff: 1.0)
48
+ unless Transformers.is_ws_url(url)
49
+ raise ArgumentError,
50
+ "Invalid Realtime URL #{url.inspect}: expected ws://, wss://, http://, or https://"
51
+ end
52
+
46
53
  @url = normalize_url(url, params)
47
54
  @params = params
48
55
  @access_token = params[:access_token] || params["access_token"]
@@ -14,6 +14,16 @@ module Supabase
14
14
 
15
15
  # Raised when a non-JSON or malformed frame arrives on the WebSocket.
16
16
  class ProtocolError < RealtimeError; end
17
+
18
+ # Raised when an operation requires an active WebSocket connection but
19
+ # the client hasn't connected (or has been closed). Mirrors py
20
+ # NotConnectedError so call sites can rescue the same class name.
21
+ class NotConnectedError < RealtimeError; end
22
+
23
+ # Raised when the server rejects a join push for authentication reasons
24
+ # (typical case: missing/invalid apikey or access_token). Mirrors py
25
+ # AuthorizationError.
26
+ class AuthorizationError < RealtimeError; end
17
27
  end
18
28
  end
19
29
  end
@@ -21,6 +21,16 @@ module Supabase
21
21
  url = url.sub(%r{(/socket/websocket|/socket|/websocket)/?\z}i, "")
22
22
  url.sub(%r{/+\z}, "")
23
23
  end
24
+
25
+ # Mirrors supabase-py's utils.is_ws_url: accepts ws/wss and http/https
26
+ # (which Client#normalize_url upgrades to ws/wss). Any other scheme — or
27
+ # an unparseable string — returns false.
28
+ def is_ws_url(url)
29
+ scheme = URI.parse(url.to_s).scheme&.downcase
30
+ %w[ws wss http https].include?(scheme)
31
+ rescue URI::InvalidURIError
32
+ false
33
+ end
24
34
  end
25
35
  end
26
36
  end
@@ -55,6 +55,10 @@ module Supabase
55
55
  # Returned by create_signed_upload_url.
56
56
  SignedUploadURL = Struct.new(:signed_url, :token, :path, keyword_init: true) do
57
57
  alias_method :signedUrl, :signed_url # rubocop:disable Naming/MethodName
58
+ # supabase-py exposes this key as `signedURL` (all-caps URL) in its
59
+ # TypedDict. Keep the alias so dictionary-style indexing from
60
+ # py-ported code (`result[:signedURL]`) works.
61
+ alias_method :signedURL, :signed_url # rubocop:disable Naming/MethodName
58
62
  end
59
63
 
60
64
  # --- list_v2 -----------------------------------------------------------
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Supabase
4
- VERSION = "3.1.0"
4
+ VERSION = "3.1.1"
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.1.0
4
+ version: 3.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Supabase