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 +4 -4
- data/lib/supabase/auth/client.rb +28 -44
- data/lib/supabase/client.rb +55 -1
- data/lib/supabase/postgrest/client.rb +48 -0
- data/lib/supabase/realtime/channel.rb +16 -0
- data/lib/supabase/realtime/client.rb +7 -0
- data/lib/supabase/realtime/errors.rb +10 -0
- data/lib/supabase/realtime/transformers.rb +10 -0
- data/lib/supabase/storage/types.rb +4 -0
- data/lib/supabase/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7490edd0417af9cdc03db6da250e89db6117ff7e8b2e0e57981c993a77e11ad8
|
|
4
|
+
data.tar.gz: dfca661ce05dac328aea1aaaec095c8ee1d82365185c6a6ef3e25383361872ba
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e8156e656365854825396e52643c1055ca0b5c608b8365c260cdfbe89f4e71923771eb9b1cb0d73eefd8f9e7bf7b350948edfda829cff331ed5972fd467b4293
|
|
7
|
+
data.tar.gz: 042a0c5e2132027ffbb2dab37fda4e93aae88aca605fba711bade4ca4765d5a269366bfc947ab1ad674462b25562920662fe1c053aebc6b55377688e769df9c6
|
data/lib/supabase/auth/client.rb
CHANGED
|
@@ -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
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
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
|
-
|
|
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
|
|
data/lib/supabase/client.rb
CHANGED
|
@@ -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
|
|
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 -----------------------------------------------------------
|
data/lib/supabase/version.rb
CHANGED