volcano-sdk 0.10.1 → 0.10.2
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/README.md +4 -0
- data/lib/volcano/auth_get_user.rb +6 -2
- data/lib/volcano/auth_oauth.rb +5 -2
- data/lib/volcano/auth_oauth_api.rb +10 -3
- data/lib/volcano/auth_refresh.rb +2 -4
- data/lib/volcano/auth_sessions.rb +4 -0
- data/lib/volcano/auth_sign_out.rb +19 -5
- data/lib/volcano/auth_state.rb +26 -58
- data/lib/volcano/auth_state_notifications.rb +64 -0
- data/lib/volcano/client.rb +4 -0
- data/lib/volcano/database.rb +13 -3
- data/lib/volcano/durable.rb +4 -0
- data/lib/volcano/durable_models.rb +5 -1
- data/lib/volcano/function_resolution.rb +17 -5
- data/lib/volcano/function_response.rb +5 -1
- data/lib/volcano/generated_transport_functions.rb +9 -1
- data/lib/volcano/generated_transport_support.rb +16 -3
- data/lib/volcano/log_responses.rb +5 -1
- data/lib/volcano/models.rb +0 -36
- data/lib/volcano/realtime/postgres_changes.rb +4 -4
- data/lib/volcano/realtime/postgres_database.rb +5 -1
- data/lib/volcano/realtime/postgres_expansion.rb +5 -1
- data/lib/volcano/realtime/protocol.rb +8 -3
- data/lib/volcano/realtime/protocol_recovery.rb +4 -1
- data/lib/volcano/realtime/protocol_recovery_position.rb +9 -0
- data/lib/volcano/realtime.rb +9 -1
- data/lib/volcano/session.rb +9 -4
- data/lib/volcano/storage_models.rb +10 -82
- data/lib/volcano/storage_mutations.rb +5 -3
- data/lib/volcano/storage_upload_models.rb +82 -0
- data/lib/volcano/transport.rb +5 -1
- data/lib/volcano/user.rb +48 -0
- data/lib/volcano/version.rb +1 -1
- data/lib/volcano.rb +2 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0a5a8e6d0c0958a19588efaa73c12721eee47e710bc397ce18a4a2a383d0ba9f
|
|
4
|
+
data.tar.gz: ec32d121d29074aece0ed41d30599f575e90535c66ad93c82b073d1a3d1b8eaf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ed4e6a000be0491dc2edfcb72e6bc03a87a49bdd3eb835c0d29455010852e5a645b6323e65e7740cd3a9c98898726bb0bc7ba7bb68cd5a37992f6f2049661105
|
|
7
|
+
data.tar.gz: b1ef9f3c793ad18bd26a7d347b8b48b85dc792d10de410d88340b945bb9f19c67a25bc3fa612f7889ca88b5962bd89fe20ef2ef4e4d1e2dee273ca5380876cd2
|
data/README.md
CHANGED
|
@@ -907,3 +907,7 @@ Run `npm ci && bin/check-openapi` to verify generated provenance. Use
|
|
|
907
907
|
`bundle exec rubocop -A` to apply safe formatting and lint fixes. Before
|
|
908
908
|
changing the facade, run `bundle exec rubocop --parallel`, `bundle exec rspec`,
|
|
909
909
|
and the shared Cucumber contract suite.
|
|
910
|
+
|
|
911
|
+
## Contributing
|
|
912
|
+
|
|
913
|
+
See [CONTRIBUTING.md](./CONTRIBUTING.md) for cross-language coordination and the full verification workflow.
|
|
@@ -72,8 +72,12 @@ module Volcano
|
|
|
72
72
|
def valid_user?(payload)
|
|
73
73
|
return false unless payload.is_a?(Hash)
|
|
74
74
|
|
|
75
|
-
required_user_fields?(payload) &&
|
|
76
|
-
|
|
75
|
+
required_user_fields?(payload) && valid_optional_user_fields?(payload)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def valid_optional_user_fields?(payload)
|
|
79
|
+
valid_email_confirmation?(payload) && valid_metadata?(payload) &&
|
|
80
|
+
valid_optional_strings?(payload) && valid_timestamps?(payload)
|
|
77
81
|
end
|
|
78
82
|
|
|
79
83
|
def required_user_fields?(payload)
|
data/lib/volcano/auth_oauth.rb
CHANGED
|
@@ -77,13 +77,16 @@ module Volcano
|
|
|
77
77
|
provider = attributes.fetch('provider')
|
|
78
78
|
linked_at = attributes.fetch('linked_at')
|
|
79
79
|
updated_at = attributes.fetch('updated_at')
|
|
80
|
-
valid =
|
|
81
|
-
linked_at.is_a?(Time) && updated_at.is_a?(Time)
|
|
80
|
+
valid = valid_linked_oauth_provider?(provider, linked_at, updated_at)
|
|
82
81
|
raise TypeError, 'Expected complete linked OAuth providers' unless valid
|
|
83
82
|
|
|
84
83
|
LinkedOAuthProvider.new(provider:, linked_at:, updated_at:)
|
|
85
84
|
rescue KeyError
|
|
86
85
|
raise TypeError, 'Expected complete linked OAuth providers'
|
|
87
86
|
end
|
|
87
|
+
|
|
88
|
+
def valid_linked_oauth_provider?(provider, linked_at, updated_at)
|
|
89
|
+
complete_string?(provider) && linked_at.is_a?(Time) && updated_at.is_a?(Time)
|
|
90
|
+
end
|
|
88
91
|
end
|
|
89
92
|
end
|
|
@@ -32,15 +32,22 @@ module Volcano
|
|
|
32
32
|
|
|
33
33
|
def freeze_oauth_api_data(value)
|
|
34
34
|
case value
|
|
35
|
-
when Hash
|
|
36
|
-
value.to_h { |key, item| [freeze_oauth_api_data(key), freeze_oauth_api_data(item)] }.freeze
|
|
35
|
+
when Hash then freeze_oauth_api_data_hash(value)
|
|
37
36
|
when Array
|
|
38
37
|
value.map { |item| freeze_oauth_api_data(item) }.freeze
|
|
39
38
|
when String
|
|
40
39
|
value.dup.freeze
|
|
41
40
|
else
|
|
42
|
-
value
|
|
41
|
+
freeze_oauth_api_data_scalar(value)
|
|
43
42
|
end
|
|
44
43
|
end
|
|
44
|
+
|
|
45
|
+
def freeze_oauth_api_data_scalar(value)
|
|
46
|
+
value.frozen? ? value : value.dup.freeze
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def freeze_oauth_api_data_hash(value)
|
|
50
|
+
value.to_h { |key, item| [freeze_oauth_api_data(key), freeze_oauth_api_data(item)] }.freeze
|
|
51
|
+
end
|
|
45
52
|
end
|
|
46
53
|
end
|
data/lib/volcano/auth_refresh.rb
CHANGED
|
@@ -75,7 +75,7 @@ module Volcano
|
|
|
75
75
|
|
|
76
76
|
def rejected_refresh?(binding, active)
|
|
77
77
|
generation, lineage, = binding
|
|
78
|
-
@
|
|
78
|
+
@client.refresh_rejected?(generation, lineage) &&
|
|
79
79
|
active.first == generation + 1 && active.last.nil?
|
|
80
80
|
end
|
|
81
81
|
|
|
@@ -115,9 +115,7 @@ module Volcano
|
|
|
115
115
|
Transport.body(refresh_response(binding.last.refresh_token), 200)
|
|
116
116
|
rescue Error::AuthenticationError
|
|
117
117
|
generation, owner, = binding
|
|
118
|
-
|
|
119
|
-
@rejected_refresh = [generation, owner]
|
|
120
|
-
end
|
|
118
|
+
@client.reject_refresh_if_current?(generation, notifications: notifications) unless owner.closing?
|
|
121
119
|
raise
|
|
122
120
|
end
|
|
123
121
|
end
|
|
@@ -114,6 +114,10 @@ module Volcano
|
|
|
114
114
|
uncertain = error.nil? || error.is_a?(Error::TransportError)
|
|
115
115
|
return @client.clear_session_if_current?(binding.first, lineage: binding[1]) if deletes_current && uncertain
|
|
116
116
|
|
|
117
|
+
deletion_binding_current?(binding, error)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def deletion_binding_current?(binding, error)
|
|
117
121
|
active = @client.capture_session_binding
|
|
118
122
|
(active.last && active[1] == binding[1]) || (error && rejected_refresh?(binding, active))
|
|
119
123
|
end
|
|
@@ -46,23 +46,37 @@ module Volcano
|
|
|
46
46
|
session_id = access_token_session_id(session.access_token)
|
|
47
47
|
verified = owner.verified_pair?(session)
|
|
48
48
|
return revoke_access_session(session, session_id, refresh_error, joined: joined) if session_id && !verified
|
|
49
|
-
return refresh_error if refresh_error && !verified
|
|
50
|
-
return unless session.refresh_token
|
|
51
49
|
|
|
52
|
-
|
|
53
|
-
nil
|
|
50
|
+
refresh_revocation_error(session, refresh_error, verified: verified)
|
|
54
51
|
rescue Error::VolcanoError => e
|
|
55
52
|
e
|
|
56
53
|
end
|
|
57
54
|
|
|
55
|
+
def refresh_revocation_error(session, refresh_error, verified:)
|
|
56
|
+
return refresh_error if refresh_error && !verified
|
|
57
|
+
|
|
58
|
+
revoke_refresh_session(session.refresh_token)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def revoke_refresh_session(refresh_token)
|
|
62
|
+
return unless refresh_token
|
|
63
|
+
|
|
64
|
+
Transport.body(logout_response(refresh_token), 204)
|
|
65
|
+
nil
|
|
66
|
+
end
|
|
67
|
+
|
|
58
68
|
def revoke_access_session(session, session_id, refresh_error, joined:)
|
|
59
69
|
error = delete_session_error(session.access_token, session_id)
|
|
60
|
-
return error unless error
|
|
70
|
+
return error unless refreshable_revocation?(error, session)
|
|
61
71
|
return refresh_error || error if joined
|
|
62
72
|
|
|
63
73
|
refreshed = parse_refresh_session(Transport.body(refresh_response(session.refresh_token), 200))
|
|
64
74
|
SessionCredentials.validate_refresh(session, refreshed)
|
|
65
75
|
delete_session_error(refreshed.access_token, session_id)
|
|
66
76
|
end
|
|
77
|
+
|
|
78
|
+
def refreshable_revocation?(error, session)
|
|
79
|
+
error&.status == 401 && session.refresh_token
|
|
80
|
+
end
|
|
67
81
|
end
|
|
68
82
|
end
|
data/lib/volcano/auth_state.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'auth_state_notifications'
|
|
4
|
+
|
|
3
5
|
module Volcano
|
|
4
6
|
# Idempotent handle for an authentication-state subscription.
|
|
5
7
|
class AuthSubscription
|
|
@@ -21,14 +23,14 @@ module Volcano
|
|
|
21
23
|
|
|
22
24
|
# Owns synchronized local session state and its subscribers.
|
|
23
25
|
class AuthState
|
|
24
|
-
|
|
25
|
-
private_constant :CALLBACK_FAILURES
|
|
26
|
+
include AuthStateNotifications
|
|
26
27
|
|
|
27
28
|
def initialize(session: nil)
|
|
28
29
|
@mutex = Mutex.new
|
|
29
30
|
@generation = 0
|
|
30
31
|
@lineage = SessionOperations.new
|
|
31
32
|
@session = session
|
|
33
|
+
@rejected_refresh = nil
|
|
32
34
|
@callbacks = {}
|
|
33
35
|
@next_callback_id = 0
|
|
34
36
|
@notifications = []
|
|
@@ -50,17 +52,33 @@ module Volcano
|
|
|
50
52
|
|
|
51
53
|
def store_if_current?(session, generation, event: :signed_in, notifications: nil, lineage: nil)
|
|
52
54
|
dispatch = @mutex.synchronize do
|
|
53
|
-
return false unless
|
|
55
|
+
return false unless current_binding?(generation, lineage)
|
|
54
56
|
return true if session.nil? && @session.nil?
|
|
55
57
|
|
|
56
58
|
enqueue_notification(replace(session, event, verified_pair: session), event, session)
|
|
57
59
|
end
|
|
58
60
|
return true unless dispatch
|
|
59
61
|
|
|
60
|
-
notifications
|
|
62
|
+
dispatch_notifications(notifications)
|
|
63
|
+
true
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def reject_refresh_if_current?(generation, notifications: nil)
|
|
67
|
+
dispatch = @mutex.synchronize do
|
|
68
|
+
return false unless generation == @generation && @session
|
|
69
|
+
|
|
70
|
+
# Publish rejection and credential removal as one synchronized transition.
|
|
71
|
+
@rejected_refresh = [generation, @lineage]
|
|
72
|
+
enqueue_notification(replace(nil, :signed_out), :signed_out, nil)
|
|
73
|
+
end
|
|
74
|
+
dispatch_notifications(notifications) if dispatch
|
|
61
75
|
true
|
|
62
76
|
end
|
|
63
77
|
|
|
78
|
+
def refresh_rejected?(generation, lineage)
|
|
79
|
+
@mutex.synchronize { @rejected_refresh == [generation, lineage] }
|
|
80
|
+
end
|
|
81
|
+
|
|
64
82
|
def update_user_if_current?(user, generation)
|
|
65
83
|
@mutex.synchronize do
|
|
66
84
|
return false unless generation == @generation && @session
|
|
@@ -81,6 +99,10 @@ module Volcano
|
|
|
81
99
|
|
|
82
100
|
private
|
|
83
101
|
|
|
102
|
+
def current_binding?(generation, lineage) = lineage.nil? ? generation == @generation : lineage == @lineage
|
|
103
|
+
|
|
104
|
+
def dispatch_notifications(queue) = queue ? queue.push(method(:drain_notifications)) : drain_notifications
|
|
105
|
+
|
|
84
106
|
def replace(session, event, verified_pair: nil)
|
|
85
107
|
SessionCredentials.validate_refresh(@session, session) if event == :token_refreshed
|
|
86
108
|
@session = session
|
|
@@ -89,59 +111,5 @@ module Volcano
|
|
|
89
111
|
@lineage = SessionOperations.new(verified_pair) if session && event != :token_refreshed
|
|
90
112
|
@callbacks.keys
|
|
91
113
|
end
|
|
92
|
-
|
|
93
|
-
def register(callback)
|
|
94
|
-
@next_callback_id += 1
|
|
95
|
-
@callbacks[@next_callback_id] = callback
|
|
96
|
-
[@next_callback_id, @session]
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
def unsubscribe(callback_id)
|
|
100
|
-
@mutex.synchronize { @callbacks.delete(callback_id) }
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
def enqueue_notification(callbacks, event, session)
|
|
104
|
-
return false if event.nil? || callbacks.empty?
|
|
105
|
-
|
|
106
|
-
@notifications << [callbacks, event, session]
|
|
107
|
-
return false if @dispatching_notifications
|
|
108
|
-
|
|
109
|
-
@dispatching_notifications = true
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
def drain_notifications
|
|
113
|
-
failure = nil
|
|
114
|
-
loop do
|
|
115
|
-
notification = next_notification
|
|
116
|
-
break unless notification
|
|
117
|
-
|
|
118
|
-
current_failure = notify(*notification)
|
|
119
|
-
failure ||= current_failure
|
|
120
|
-
end
|
|
121
|
-
raise failure if failure
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
def next_notification
|
|
125
|
-
@mutex.synchronize do
|
|
126
|
-
if @notifications.empty?
|
|
127
|
-
@dispatching_notifications = false
|
|
128
|
-
return
|
|
129
|
-
end
|
|
130
|
-
@notifications.shift
|
|
131
|
-
end
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
def notify(callback_ids, event, session)
|
|
135
|
-
failure = nil
|
|
136
|
-
callback_ids.each do |callback_id|
|
|
137
|
-
@mutex.synchronize { @callbacks[callback_id] }&.call(event, session)
|
|
138
|
-
rescue StandardError => e
|
|
139
|
-
Warning.warn("Volcano auth-state callback failed (#{e.class})\n")
|
|
140
|
-
rescue *CALLBACK_FAILURES => e
|
|
141
|
-
unsubscribe(callback_id)
|
|
142
|
-
failure ||= e
|
|
143
|
-
end
|
|
144
|
-
failure
|
|
145
|
-
end
|
|
146
114
|
end
|
|
147
115
|
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Volcano
|
|
4
|
+
# Queues auth events under the state mutex and invokes subscribers outside it.
|
|
5
|
+
module AuthStateNotifications
|
|
6
|
+
CALLBACK_FAILURES = [Exception].freeze
|
|
7
|
+
private_constant :CALLBACK_FAILURES
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def register(callback)
|
|
12
|
+
@next_callback_id += 1
|
|
13
|
+
@callbacks[@next_callback_id] = callback
|
|
14
|
+
[@next_callback_id, @session]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def unsubscribe(callback_id) = @mutex.synchronize { @callbacks.delete(callback_id) }
|
|
18
|
+
|
|
19
|
+
def enqueue_notification(callbacks, event, session)
|
|
20
|
+
return false if event.nil? || callbacks.empty?
|
|
21
|
+
|
|
22
|
+
@notifications << [callbacks, event, session]
|
|
23
|
+
return false if @dispatching_notifications
|
|
24
|
+
|
|
25
|
+
@dispatching_notifications = true
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def drain_notifications
|
|
29
|
+
failure = nil
|
|
30
|
+
loop do
|
|
31
|
+
notification = next_notification
|
|
32
|
+
break unless notification
|
|
33
|
+
|
|
34
|
+
current_failure = notify(*notification)
|
|
35
|
+
failure ||= current_failure
|
|
36
|
+
end
|
|
37
|
+
raise failure if failure
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def next_notification
|
|
41
|
+
@mutex.synchronize do
|
|
42
|
+
if @notifications.empty?
|
|
43
|
+
@dispatching_notifications = false
|
|
44
|
+
return
|
|
45
|
+
end
|
|
46
|
+
@notifications.shift
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def notify(callback_ids, event, session)
|
|
51
|
+
failure = nil
|
|
52
|
+
callback_ids.each do |callback_id|
|
|
53
|
+
@mutex.synchronize { @callbacks[callback_id] }&.call(event, session)
|
|
54
|
+
rescue StandardError => e
|
|
55
|
+
Warning.warn("Volcano auth-state callback failed (#{e.class})\n")
|
|
56
|
+
rescue *CALLBACK_FAILURES => e
|
|
57
|
+
unsubscribe(callback_id)
|
|
58
|
+
failure ||= e
|
|
59
|
+
end
|
|
60
|
+
failure
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
private_constant :AuthStateNotifications
|
|
64
|
+
end
|
data/lib/volcano/client.rb
CHANGED
|
@@ -86,6 +86,10 @@ module Volcano
|
|
|
86
86
|
@auth_state.store_if_current?(nil, generation, event: event, notifications: notifications, lineage: lineage)
|
|
87
87
|
end
|
|
88
88
|
|
|
89
|
+
def reject_refresh_if_current?(...) = @auth_state.reject_refresh_if_current?(...)
|
|
90
|
+
|
|
91
|
+
def refresh_rejected?(...) = @auth_state.refresh_rejected?(...)
|
|
92
|
+
|
|
89
93
|
def subscribe_auth_state_change(...)
|
|
90
94
|
@auth_state.subscribe(...)
|
|
91
95
|
end
|
data/lib/volcano/database.rb
CHANGED
|
@@ -9,10 +9,14 @@ module Volcano
|
|
|
9
9
|
case value
|
|
10
10
|
when String then value.dup.freeze
|
|
11
11
|
when Array then value.map { |item| capture(item) }.freeze
|
|
12
|
-
when Hash then value
|
|
12
|
+
when Hash then capture_hash(value)
|
|
13
13
|
else value
|
|
14
14
|
end
|
|
15
15
|
end
|
|
16
|
+
|
|
17
|
+
def capture_hash(value)
|
|
18
|
+
value.to_h { |key, item| [capture(key), capture(item)] }.freeze
|
|
19
|
+
end
|
|
16
20
|
end
|
|
17
21
|
private_constant :ImmutableQueryValue
|
|
18
22
|
|
|
@@ -146,11 +150,17 @@ module Volcano
|
|
|
146
150
|
|
|
147
151
|
def query_body
|
|
148
152
|
{ 'table' => @table }.tap do |body|
|
|
153
|
+
add_query_selection(body)
|
|
154
|
+
body['limit'] = @limit unless @limit.nil?
|
|
155
|
+
body['offset'] = @offset unless @offset.nil?
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def add_query_selection(body)
|
|
160
|
+
body.tap do
|
|
149
161
|
body['select'] = @columns unless @columns.empty? || @columns == ['*']
|
|
150
162
|
body['filters'] = @filters unless @filters.empty?
|
|
151
163
|
body['order'] = @order unless @order.empty?
|
|
152
|
-
body['limit'] = @limit unless @limit.nil?
|
|
153
|
-
body['offset'] = @offset unless @offset.nil?
|
|
154
164
|
end
|
|
155
165
|
end
|
|
156
166
|
end
|
data/lib/volcano/durable.rb
CHANGED
|
@@ -124,6 +124,10 @@ module Volcano
|
|
|
124
124
|
def validate_paging(page, limit)
|
|
125
125
|
raise ArgumentError, 'page must be a positive Integer' unless page.nil? || positive_integer?(page)
|
|
126
126
|
|
|
127
|
+
validate_page_limit(limit)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def validate_page_limit(limit)
|
|
127
131
|
return if limit.nil? || (positive_integer?(limit) && limit <= MAX_PAGE_SIZE)
|
|
128
132
|
|
|
129
133
|
raise ArgumentError, "limit must be an Integer between 1 and #{MAX_PAGE_SIZE}"
|
|
@@ -51,12 +51,16 @@ module Volcano
|
|
|
51
51
|
# `result` is the function's own JSON, so it is frozen all the way down.
|
|
52
52
|
def immutable_value(value)
|
|
53
53
|
case value
|
|
54
|
-
when Hash then value
|
|
54
|
+
when Hash then immutable_value_hash(value)
|
|
55
55
|
when Array then value.map { |item| immutable_value(item) }.freeze
|
|
56
56
|
when Time, String then value.dup.freeze
|
|
57
57
|
else value
|
|
58
58
|
end
|
|
59
59
|
end
|
|
60
|
+
|
|
61
|
+
def immutable_value_hash(value)
|
|
62
|
+
value.to_h { |key, item| [immutable_value(key), immutable_value(item)] }.freeze
|
|
63
|
+
end
|
|
60
64
|
end
|
|
61
65
|
|
|
62
66
|
# One page of a durable function's executions, most recent first.
|
|
@@ -51,11 +51,10 @@ module Volcano
|
|
|
51
51
|
# when the API itself is plaintext, so a resolve response cannot
|
|
52
52
|
# downgrade a credential that is otherwise protected in transit.
|
|
53
53
|
def valid_invoke_url(value, api_url)
|
|
54
|
-
return nil unless
|
|
54
|
+
return nil unless nonempty_url?(value)
|
|
55
55
|
|
|
56
56
|
uri = URI.parse(value)
|
|
57
|
-
|
|
58
|
-
return nil if uri.host.to_s.empty? || !uri.port.to_i.between?(1, 65_535)
|
|
57
|
+
return nil unless usable_address?(uri)
|
|
59
58
|
|
|
60
59
|
usable_scheme?(uri.scheme, api_url) ? value : nil
|
|
61
60
|
rescue URI::InvalidURIError
|
|
@@ -107,6 +106,15 @@ module Volcano
|
|
|
107
106
|
|
|
108
107
|
private
|
|
109
108
|
|
|
109
|
+
def nonempty_url?(value)
|
|
110
|
+
value.is_a?(String) && !value.empty?
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def usable_address?(uri)
|
|
114
|
+
# URI accepts a port outside the range a connection can use.
|
|
115
|
+
!uri.host.to_s.empty? && uri.port.to_i.between?(1, 65_535)
|
|
116
|
+
end
|
|
117
|
+
|
|
110
118
|
def usable_scheme?(scheme, api_url)
|
|
111
119
|
case scheme&.downcase
|
|
112
120
|
when 'https' then true
|
|
@@ -120,10 +128,14 @@ module Volcano
|
|
|
120
128
|
@entries[key] = Entry.new(outcome: outcome, expires_at: now + ttl_seconds)
|
|
121
129
|
next if @entries.size <= MAX_ENTRIES
|
|
122
130
|
|
|
123
|
-
|
|
124
|
-
@entries.delete(@entries.min_by { |_, entry| entry.expires_at }&.first) while @entries.size > MAX_ENTRIES
|
|
131
|
+
evict_expired_entries
|
|
125
132
|
end
|
|
126
133
|
end
|
|
134
|
+
|
|
135
|
+
def evict_expired_entries
|
|
136
|
+
@entries.delete_if { |_, entry| entry.expires_at <= now }
|
|
137
|
+
@entries.delete(@entries.min_by { |_, entry| entry.expires_at }.first) while @entries.size > MAX_ENTRIES
|
|
138
|
+
end
|
|
127
139
|
end
|
|
128
140
|
end
|
|
129
141
|
end
|
|
@@ -16,11 +16,15 @@ module Volcano
|
|
|
16
16
|
|
|
17
17
|
def immutable_json(value)
|
|
18
18
|
case value
|
|
19
|
-
when Hash then value
|
|
19
|
+
when Hash then immutable_json_hash(value)
|
|
20
20
|
when Array then value.map { |item| immutable_json(item) }.freeze
|
|
21
21
|
when String then value.dup.freeze
|
|
22
22
|
else value
|
|
23
23
|
end
|
|
24
24
|
end
|
|
25
|
+
|
|
26
|
+
def immutable_json_hash(value)
|
|
27
|
+
value.to_h { |key, item| [immutable_json(key), immutable_json(item)] }.freeze
|
|
28
|
+
end
|
|
25
29
|
end
|
|
26
30
|
end
|
|
@@ -82,6 +82,10 @@ module Volcano
|
|
|
82
82
|
return nil if text.empty?
|
|
83
83
|
return text unless function_json?(text, headers)
|
|
84
84
|
|
|
85
|
+
parse_function_json(text)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def parse_function_json(text)
|
|
85
89
|
parsed = JSON.parse(text)
|
|
86
90
|
valid_function_encoding?(parsed) ? parsed : text
|
|
87
91
|
rescue JSON::ParserError
|
|
@@ -90,13 +94,17 @@ module Volcano
|
|
|
90
94
|
|
|
91
95
|
def valid_function_encoding?(value)
|
|
92
96
|
case value
|
|
93
|
-
when Hash then
|
|
97
|
+
when Hash then valid_function_hash_encoding?(value)
|
|
94
98
|
when Array then value.all? { |item| valid_function_encoding?(item) }
|
|
95
99
|
when String then value.valid_encoding?
|
|
96
100
|
else true
|
|
97
101
|
end
|
|
98
102
|
end
|
|
99
103
|
|
|
104
|
+
def valid_function_hash_encoding?(value)
|
|
105
|
+
value.all? { |key, item| valid_function_encoding?(key) && valid_function_encoding?(item) }
|
|
106
|
+
end
|
|
107
|
+
|
|
100
108
|
def function_json?(text, headers)
|
|
101
109
|
return true if text.start_with?('{', '[')
|
|
102
110
|
|
|
@@ -10,7 +10,12 @@ module Volcano
|
|
|
10
10
|
rescue NameError => e
|
|
11
11
|
raise unless e.name == :Generated && e.message.include?('private constant Volcano::Generated')
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
deserialize_private_model(Generated.const_get(type), value)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def deserialize_private_model(klass, value)
|
|
14
19
|
if klass.respond_to?(:openapi_any_of) || klass.respond_to?(:openapi_one_of)
|
|
15
20
|
klass.build(value)
|
|
16
21
|
else
|
|
@@ -139,12 +144,16 @@ module Volcano
|
|
|
139
144
|
def plain_value(value)
|
|
140
145
|
value = value.to_hash if value.respond_to?(:to_hash)
|
|
141
146
|
case value
|
|
142
|
-
when Hash then value
|
|
147
|
+
when Hash then plain_hash(value)
|
|
143
148
|
when Array then value.map { |item| plain_value(item) }
|
|
144
149
|
else value
|
|
145
150
|
end
|
|
146
151
|
end
|
|
147
152
|
|
|
153
|
+
def plain_hash(value)
|
|
154
|
+
value.to_h { |key, item| [key.to_s, plain_value(item)] }
|
|
155
|
+
end
|
|
156
|
+
|
|
148
157
|
def deep_symbolize(value)
|
|
149
158
|
case value
|
|
150
159
|
when Hash then value.to_h { |key, item| [key.to_sym, deep_symbolize(item)] }
|
|
@@ -163,10 +172,14 @@ module Volcano
|
|
|
163
172
|
end
|
|
164
173
|
|
|
165
174
|
def prepare_stream(value)
|
|
166
|
-
|
|
175
|
+
reopen_stream(value)
|
|
167
176
|
value.binmode if value.respond_to?(:binmode)
|
|
168
177
|
value.rewind if value.respond_to?(:rewind)
|
|
169
178
|
end
|
|
179
|
+
|
|
180
|
+
def reopen_stream(value)
|
|
181
|
+
value.open if value.respond_to?(:closed?) && value.closed? && value.respond_to?(:open)
|
|
182
|
+
end
|
|
170
183
|
end
|
|
171
184
|
|
|
172
185
|
# Builds generated API wrappers with consistent authentication and timeouts.
|
|
@@ -7,12 +7,16 @@ module Volcano
|
|
|
7
7
|
|
|
8
8
|
def immutable_json(value)
|
|
9
9
|
case value
|
|
10
|
-
when Hash then value
|
|
10
|
+
when Hash then immutable_json_hash(value)
|
|
11
11
|
when Array then value.map { |item| immutable_json(item) }.freeze
|
|
12
12
|
when Time, String then value.dup.freeze
|
|
13
13
|
else value
|
|
14
14
|
end
|
|
15
15
|
end
|
|
16
|
+
|
|
17
|
+
def immutable_json_hash(value)
|
|
18
|
+
value.to_h { |key, item| [immutable_json(key), immutable_json(item)] }.freeze
|
|
19
|
+
end
|
|
16
20
|
end
|
|
17
21
|
private_constant :ImmutableLogJSON
|
|
18
22
|
|
data/lib/volcano/models.rb
CHANGED
|
@@ -59,42 +59,6 @@ module Volcano
|
|
|
59
59
|
end
|
|
60
60
|
end
|
|
61
61
|
EmailChangeResult = Data.define(:message, :new_email)
|
|
62
|
-
USER_OPTIONAL_ATTRIBUTES = %i[
|
|
63
|
-
project_id email_confirmed user_metadata app_metadata avatar_url banned_until
|
|
64
|
-
last_sign_in_at created_at updated_at
|
|
65
|
-
].freeze
|
|
66
|
-
private_constant :USER_OPTIONAL_ATTRIBUTES
|
|
67
|
-
|
|
68
|
-
User = Data.define(
|
|
69
|
-
:id, :project_id, :email, :email_confirmed, :user_metadata, :app_metadata,
|
|
70
|
-
:avatar_url, :status, :banned_until, :last_sign_in_at, :created_at, :updated_at
|
|
71
|
-
) do
|
|
72
|
-
def initialize(id:, email:, status:, **attributes)
|
|
73
|
-
validate_optional_attributes(attributes)
|
|
74
|
-
super(
|
|
75
|
-
id: freeze_value(id),
|
|
76
|
-
email: freeze_value(email),
|
|
77
|
-
status: freeze_value(status),
|
|
78
|
-
**USER_OPTIONAL_ATTRIBUTES.to_h { |name| [name, freeze_value(attributes[name])] }
|
|
79
|
-
)
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
private
|
|
83
|
-
|
|
84
|
-
def validate_optional_attributes(attributes)
|
|
85
|
-
unknown = attributes.keys - USER_OPTIONAL_ATTRIBUTES
|
|
86
|
-
raise ArgumentError, "unknown keywords: #{unknown.join(', ')}" unless unknown.empty?
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
def freeze_value(value)
|
|
90
|
-
case value
|
|
91
|
-
when Hash then value.to_h { |key, item| [freeze_value(key), freeze_value(item)] }.freeze
|
|
92
|
-
when Array then value.map { |item| freeze_value(item) }.freeze
|
|
93
|
-
when String then value.dup.freeze
|
|
94
|
-
else value.frozen? ? value : value.dup.freeze
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
end
|
|
98
62
|
LockLease = Data.define(:key, :token, :expires_at, :fencing_token) do
|
|
99
63
|
def initialize(key:, token:, expires_at:, fencing_token:)
|
|
100
64
|
super(
|
|
@@ -11,10 +11,10 @@ module Volcano
|
|
|
11
11
|
)
|
|
12
12
|
super(
|
|
13
13
|
type: Immutable.call(type.to_s), schema: Immutable.call(schema.to_s),
|
|
14
|
-
table: Immutable.call(table.to_s), record:
|
|
15
|
-
old_record:
|
|
16
|
-
columns:
|
|
17
|
-
id:
|
|
14
|
+
table: Immutable.call(table.to_s), record: Immutable.optional(record),
|
|
15
|
+
old_record: Immutable.optional(old_record),
|
|
16
|
+
columns: Immutable.optional(columns), timestamp: Immutable.call(timestamp.to_s),
|
|
17
|
+
id: Immutable.optional(id), mode: mode && Immutable.call(mode.to_s)
|
|
18
18
|
)
|
|
19
19
|
end
|
|
20
20
|
end
|
|
@@ -11,7 +11,7 @@ module Volcano
|
|
|
11
11
|
attr_reader :database_name
|
|
12
12
|
|
|
13
13
|
def database_name=(name)
|
|
14
|
-
unless
|
|
14
|
+
unless valid_database_name?(name)
|
|
15
15
|
raise ArgumentError,
|
|
16
16
|
'database name must match ^[a-z0-9_]+$ and contain at most 64 characters'
|
|
17
17
|
end
|
|
@@ -20,6 +20,10 @@ module Volcano
|
|
|
20
20
|
|
|
21
21
|
private
|
|
22
22
|
|
|
23
|
+
def valid_database_name?(name)
|
|
24
|
+
name.nil? || (name.is_a?(String) && DATABASE_NAME.match?(name))
|
|
25
|
+
end
|
|
26
|
+
|
|
23
27
|
def initialize_postgres_database
|
|
24
28
|
@database_name = nil
|
|
25
29
|
@postgres_fetch_semaphore = nil
|
|
@@ -7,7 +7,7 @@ module Volcano
|
|
|
7
7
|
private
|
|
8
8
|
|
|
9
9
|
def expanded_postgres_changes(requests)
|
|
10
|
-
return requests
|
|
10
|
+
return local_postgres_changes(requests) unless requests.first.database_name
|
|
11
11
|
|
|
12
12
|
records = fetch_postgres_records(requests)
|
|
13
13
|
requests.map { |request| expanded_postgres_record(request, records) }
|
|
@@ -16,6 +16,10 @@ module Volcano
|
|
|
16
16
|
requests.map(&:change)
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
+
def local_postgres_changes(requests)
|
|
20
|
+
requests.map { |request| local_postgres_change(request.change) }
|
|
21
|
+
end
|
|
22
|
+
|
|
19
23
|
def lightweight_delete?(change)
|
|
20
24
|
change.mode == 'lightweight' && change.type == 'DELETE'
|
|
21
25
|
end
|
|
@@ -48,9 +48,7 @@ module Volcano
|
|
|
48
48
|
def self.subscribe(id:, channel:, recoverable: false, join_leave: false, recovery: nil)
|
|
49
49
|
options = { 'channel' => channel }
|
|
50
50
|
if recovery
|
|
51
|
-
options
|
|
52
|
-
options['epoch'] = recovery[:epoch] if recovery.key?(:epoch)
|
|
53
|
-
options['offset'] = recovery[:offset] if recovery.key?(:offset)
|
|
51
|
+
add_recovery_options(options, recovery)
|
|
54
52
|
elsif recoverable
|
|
55
53
|
options['recoverable'] = true
|
|
56
54
|
end
|
|
@@ -58,6 +56,13 @@ module Volcano
|
|
|
58
56
|
{ 'id' => id, 'subscribe' => options }
|
|
59
57
|
end
|
|
60
58
|
|
|
59
|
+
def self.add_recovery_options(options, recovery)
|
|
60
|
+
options.merge!('recover' => true, 'positioned' => true, 'recoverable' => true)
|
|
61
|
+
options['epoch'] = recovery[:epoch] if recovery.key?(:epoch)
|
|
62
|
+
options['offset'] = recovery[:offset] if recovery.key?(:offset)
|
|
63
|
+
end
|
|
64
|
+
private_class_method :add_recovery_options
|
|
65
|
+
|
|
61
66
|
def self.publish(id:, channel:, data:) = { 'id' => id, 'publish' => { 'channel' => channel, 'data' => data } }
|
|
62
67
|
|
|
63
68
|
def self.unsubscribe(id:, channel:) = { 'id' => id, 'unsubscribe' => { 'channel' => channel } }
|
|
@@ -59,7 +59,10 @@ module Volcano
|
|
|
59
59
|
result_position = recovery_position(result['epoch'], result['offset'])
|
|
60
60
|
return [nil, nil] unless result_position
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
validate_recovery_publications(result.fetch('publications', []), result_position)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def validate_recovery_publications(publications, result_position)
|
|
63
66
|
return [nil, nil] unless publications.is_a?(Array)
|
|
64
67
|
|
|
65
68
|
validated = publications.map { |publication| validated_recovery_publication(publication, result_position) }
|
|
@@ -26,6 +26,11 @@ module Volcano
|
|
|
26
26
|
|
|
27
27
|
completed = publication_position(publication, current)
|
|
28
28
|
return unless completed
|
|
29
|
+
|
|
30
|
+
advance_publication_position(channel, completed, current)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def advance_publication_position(channel, completed, current)
|
|
29
34
|
return mark_unknown_gap(channel) unless same_epoch?(completed, current)
|
|
30
35
|
return if stale_position?(completed, current)
|
|
31
36
|
return mark_unknown_gap(channel) unless next_position?(completed, current)
|
|
@@ -57,6 +62,10 @@ module Volcano
|
|
|
57
62
|
return @position_gaps[channel] = true unless same_epoch?(gap, current)
|
|
58
63
|
return if gap.fetch(:offset) <= current.fetch(:offset)
|
|
59
64
|
|
|
65
|
+
store_earliest_gap(channel, gap)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def store_earliest_gap(channel, gap)
|
|
60
69
|
existing = @position_gaps[channel]
|
|
61
70
|
return if existing == true
|
|
62
71
|
return if existing && existing.fetch(:offset) <= gap.fetch(:offset)
|
data/lib/volcano/realtime.rb
CHANGED
|
@@ -34,14 +34,22 @@ module Volcano
|
|
|
34
34
|
module Immutable
|
|
35
35
|
module_function
|
|
36
36
|
|
|
37
|
+
def optional(value)
|
|
38
|
+
value && call(value)
|
|
39
|
+
end
|
|
40
|
+
|
|
37
41
|
def call(value)
|
|
38
42
|
case value
|
|
39
|
-
when Hash then value
|
|
43
|
+
when Hash then call_hash(value)
|
|
40
44
|
when Array then value.map { |child| call(child) }.freeze
|
|
41
45
|
when String then value.dup.freeze
|
|
42
46
|
else value.freeze
|
|
43
47
|
end
|
|
44
48
|
end
|
|
49
|
+
|
|
50
|
+
def call_hash(value)
|
|
51
|
+
value.to_h { |key, item| [call(key), call(item)] }.freeze
|
|
52
|
+
end
|
|
45
53
|
end
|
|
46
54
|
private_constant :Immutable
|
|
47
55
|
|
data/lib/volcano/session.rb
CHANGED
|
@@ -37,15 +37,20 @@ module Volcano
|
|
|
37
37
|
def self.validate_refresh(current, refreshed)
|
|
38
38
|
return unless current
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
if expected && expected != session_id(refreshed&.access_token)
|
|
42
|
-
raise Error::AuthenticationError, 'Refreshed credentials belong to a different server session'
|
|
43
|
-
end
|
|
40
|
+
validate_refresh_session_id(current, refreshed)
|
|
44
41
|
return unless current.user_id && current.user_id != refreshed&.user_id
|
|
45
42
|
|
|
46
43
|
raise Error::AuthenticationError, 'Refreshed session belongs to a different user'
|
|
47
44
|
end
|
|
48
45
|
|
|
46
|
+
def self.validate_refresh_session_id(current, refreshed)
|
|
47
|
+
expected = session_id(current.access_token)
|
|
48
|
+
return unless expected && expected != session_id(refreshed&.access_token)
|
|
49
|
+
|
|
50
|
+
raise Error::AuthenticationError, 'Refreshed credentials belong to a different server session'
|
|
51
|
+
end
|
|
52
|
+
private_class_method :validate_refresh_session_id
|
|
53
|
+
|
|
49
54
|
# Untrusted continuity constraint, never authenticated user identity.
|
|
50
55
|
def self.session_id(access_token)
|
|
51
56
|
payload = token_payload(access_token)
|
|
@@ -24,98 +24,26 @@ module Volcano
|
|
|
24
24
|
|
|
25
25
|
def immutable_value(value)
|
|
26
26
|
case value
|
|
27
|
-
when Hash
|
|
28
|
-
value.to_h { |key, item| [immutable_value(key), immutable_value(item)] }.freeze
|
|
27
|
+
when Hash then immutable_value_hash(value)
|
|
29
28
|
when Array then value.map { |item| immutable_value(item) }.freeze
|
|
30
29
|
when String then value.dup.freeze
|
|
31
|
-
else value
|
|
30
|
+
else immutable_value_scalar(value)
|
|
32
31
|
end
|
|
33
32
|
end
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
StoragePage = Data.define(:objects, :next_cursor) do
|
|
37
|
-
def initialize(objects:, next_cursor: nil)
|
|
38
|
-
cursor = next_cursor.nil? ? nil : next_cursor.dup.freeze
|
|
39
|
-
super(objects: objects.to_a.dup.freeze, next_cursor: cursor)
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
UploadSessionRequest = Data.define(:path, :content_type, :total_size, :part_size) do
|
|
44
|
-
def initialize(path:, content_type:, total_size:, part_size: nil)
|
|
45
|
-
super(
|
|
46
|
-
path: path.dup.freeze,
|
|
47
|
-
content_type: content_type.dup.freeze,
|
|
48
|
-
total_size: total_size,
|
|
49
|
-
part_size: part_size
|
|
50
|
-
)
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
private_constant :UploadSessionRequest
|
|
54
|
-
|
|
55
|
-
UploadPartRequest = Data.define(:path, :session_id, :part_number, :data) do
|
|
56
|
-
def initialize(path:, session_id:, part_number:, data:)
|
|
57
|
-
super(
|
|
58
|
-
path: path.dup.freeze,
|
|
59
|
-
session_id: session_id.dup.freeze,
|
|
60
|
-
part_number: part_number,
|
|
61
|
-
data: data.dup.freeze
|
|
62
|
-
)
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
private_constant :UploadPartRequest
|
|
66
|
-
|
|
67
|
-
UploadSessionReference = Data.define(:path, :session_id) do
|
|
68
|
-
def initialize(path:, session_id:)
|
|
69
|
-
super(path: path.dup.freeze, session_id: session_id.dup.freeze)
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
private_constant :UploadSessionReference
|
|
73
33
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
super(
|
|
77
|
-
session_id: session_id.dup.freeze,
|
|
78
|
-
part_size: part_size,
|
|
79
|
-
total_parts: total_parts,
|
|
80
|
-
expires_at: expires_at.dup.freeze
|
|
81
|
-
)
|
|
34
|
+
def immutable_value_scalar(value)
|
|
35
|
+
value.frozen? ? value : value.dup.freeze
|
|
82
36
|
end
|
|
83
|
-
end
|
|
84
37
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
super(part_number: part_number, etag: etag.dup.freeze, size: size)
|
|
38
|
+
def immutable_value_hash(value)
|
|
39
|
+
value.to_h { |key, item| [immutable_value(key), immutable_value(item)] }.freeze
|
|
88
40
|
end
|
|
89
41
|
end
|
|
90
42
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
private_constant :UPLOAD_SESSION_STATUS_ATTRIBUTES
|
|
96
|
-
|
|
97
|
-
UploadSessionStatus = Data.define(*UPLOAD_SESSION_STATUS_ATTRIBUTES) do
|
|
98
|
-
def initialize(**attributes)
|
|
99
|
-
unknown = attributes.keys - UPLOAD_SESSION_STATUS_ATTRIBUTES
|
|
100
|
-
raise ArgumentError, "unknown keywords: #{unknown.join(', ')}" unless unknown.empty?
|
|
101
|
-
|
|
102
|
-
missing = UPLOAD_SESSION_STATUS_ATTRIBUTES - attributes.keys
|
|
103
|
-
raise ArgumentError, "missing keywords: #{missing.join(', ')}" unless missing.empty?
|
|
104
|
-
|
|
105
|
-
values = UPLOAD_SESSION_STATUS_ATTRIBUTES.to_h do |name|
|
|
106
|
-
[name, immutable_value(attributes.fetch(name))]
|
|
107
|
-
end
|
|
108
|
-
super(**values)
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
private
|
|
112
|
-
|
|
113
|
-
def immutable_value(value)
|
|
114
|
-
case value
|
|
115
|
-
when Array then value.to_a.dup.freeze
|
|
116
|
-
when String then value.dup.freeze
|
|
117
|
-
else value.frozen? ? value : value.dup.freeze
|
|
118
|
-
end
|
|
43
|
+
StoragePage = Data.define(:objects, :next_cursor) do
|
|
44
|
+
def initialize(objects:, next_cursor: nil)
|
|
45
|
+
cursor = next_cursor.nil? ? nil : next_cursor.dup.freeze
|
|
46
|
+
super(objects: objects.to_a.dup.freeze, next_cursor: cursor)
|
|
119
47
|
end
|
|
120
48
|
end
|
|
121
49
|
end
|
|
@@ -54,13 +54,15 @@ module Volcano
|
|
|
54
54
|
|
|
55
55
|
def storage_paths(paths)
|
|
56
56
|
path_list = paths.is_a?(String) ? [paths] : paths.to_a
|
|
57
|
-
|
|
58
|
-
raise ArgumentError, 'storage paths must be non-empty strings'
|
|
59
|
-
end
|
|
57
|
+
raise ArgumentError, 'storage paths must be non-empty strings' unless valid_storage_paths?(path_list)
|
|
60
58
|
|
|
61
59
|
path_list.map { |path| path.dup.freeze }.freeze
|
|
62
60
|
end
|
|
63
61
|
|
|
62
|
+
def valid_storage_paths?(paths)
|
|
63
|
+
!paths.empty? && paths.all? { |path| path.is_a?(String) && !path.empty? }
|
|
64
|
+
end
|
|
65
|
+
|
|
64
66
|
def delete_path(path, binding)
|
|
65
67
|
response = storage_request(binding: binding) do |token|
|
|
66
68
|
@transport.delete_storage_object(
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Volcano
|
|
4
|
+
UploadSessionRequest = Data.define(:path, :content_type, :total_size, :part_size) do
|
|
5
|
+
def initialize(path:, content_type:, total_size:, part_size: nil)
|
|
6
|
+
super(
|
|
7
|
+
path: path.dup.freeze,
|
|
8
|
+
content_type: content_type.dup.freeze,
|
|
9
|
+
total_size: total_size,
|
|
10
|
+
part_size: part_size
|
|
11
|
+
)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
private_constant :UploadSessionRequest
|
|
15
|
+
|
|
16
|
+
UploadPartRequest = Data.define(:path, :session_id, :part_number, :data) do
|
|
17
|
+
def initialize(path:, session_id:, part_number:, data:)
|
|
18
|
+
super(
|
|
19
|
+
path: path.dup.freeze,
|
|
20
|
+
session_id: session_id.dup.freeze,
|
|
21
|
+
part_number: part_number,
|
|
22
|
+
data: data.dup.freeze
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
private_constant :UploadPartRequest
|
|
27
|
+
|
|
28
|
+
UploadSessionReference = Data.define(:path, :session_id) do
|
|
29
|
+
def initialize(path:, session_id:)
|
|
30
|
+
super(path: path.dup.freeze, session_id: session_id.dup.freeze)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
private_constant :UploadSessionReference
|
|
34
|
+
|
|
35
|
+
UploadSession = Data.define(:session_id, :part_size, :total_parts, :expires_at) do
|
|
36
|
+
def initialize(session_id:, part_size:, total_parts:, expires_at:)
|
|
37
|
+
super(
|
|
38
|
+
session_id: session_id.dup.freeze,
|
|
39
|
+
part_size: part_size,
|
|
40
|
+
total_parts: total_parts,
|
|
41
|
+
expires_at: expires_at.dup.freeze
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
UploadPart = Data.define(:part_number, :etag, :size) do
|
|
47
|
+
def initialize(part_number:, etag:, size:)
|
|
48
|
+
super(part_number: part_number, etag: etag.dup.freeze, size: size)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
UPLOAD_SESSION_STATUS_ATTRIBUTES = %i[
|
|
53
|
+
session_id status path content_type total_size part_size total_parts parts_uploaded
|
|
54
|
+
bytes_uploaded parts expires_at created_at
|
|
55
|
+
].freeze
|
|
56
|
+
private_constant :UPLOAD_SESSION_STATUS_ATTRIBUTES
|
|
57
|
+
|
|
58
|
+
UploadSessionStatus = Data.define(*UPLOAD_SESSION_STATUS_ATTRIBUTES) do
|
|
59
|
+
def initialize(**attributes)
|
|
60
|
+
unknown = attributes.keys - UPLOAD_SESSION_STATUS_ATTRIBUTES
|
|
61
|
+
raise ArgumentError, "unknown keywords: #{unknown.join(', ')}" unless unknown.empty?
|
|
62
|
+
|
|
63
|
+
missing = UPLOAD_SESSION_STATUS_ATTRIBUTES - attributes.keys
|
|
64
|
+
raise ArgumentError, "missing keywords: #{missing.join(', ')}" unless missing.empty?
|
|
65
|
+
|
|
66
|
+
values = UPLOAD_SESSION_STATUS_ATTRIBUTES.to_h do |name|
|
|
67
|
+
[name, immutable_value(attributes.fetch(name))]
|
|
68
|
+
end
|
|
69
|
+
super(**values)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def immutable_value(value)
|
|
75
|
+
case value
|
|
76
|
+
when Array then value.to_a.dup.freeze
|
|
77
|
+
when String then value.dup.freeze
|
|
78
|
+
else value.frozen? ? value : value.dup.freeze
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
data/lib/volcano/transport.rb
CHANGED
|
@@ -38,13 +38,17 @@ module Volcano
|
|
|
38
38
|
payload = response.body.is_a?(Hash) ? response.body : {}
|
|
39
39
|
retry_after = integer_header(response.headers, 'Retry-After') if response.status == 429
|
|
40
40
|
error_type(response.status).new(
|
|
41
|
-
payload
|
|
41
|
+
error_message(payload),
|
|
42
42
|
status: response.status,
|
|
43
43
|
code: payload['code']&.to_s,
|
|
44
44
|
retry_after: retry_after
|
|
45
45
|
)
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
+
def error_message(payload)
|
|
49
|
+
payload['error'] || payload['message'] || 'Volcano request failed'
|
|
50
|
+
end
|
|
51
|
+
|
|
48
52
|
def error_type(status)
|
|
49
53
|
ERROR_TYPES.fetch(status) do
|
|
50
54
|
status.between?(500, 599) ? Error::ServerError : Error::VolcanoError
|
data/lib/volcano/user.rb
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Volcano
|
|
4
|
+
USER_OPTIONAL_ATTRIBUTES = %i[
|
|
5
|
+
project_id email_confirmed user_metadata app_metadata avatar_url banned_until
|
|
6
|
+
last_sign_in_at created_at updated_at
|
|
7
|
+
].freeze
|
|
8
|
+
private_constant :USER_OPTIONAL_ATTRIBUTES
|
|
9
|
+
|
|
10
|
+
User = Data.define(
|
|
11
|
+
:id, :project_id, :email, :email_confirmed, :user_metadata, :app_metadata,
|
|
12
|
+
:avatar_url, :status, :banned_until, :last_sign_in_at, :created_at, :updated_at
|
|
13
|
+
) do
|
|
14
|
+
def initialize(id:, email:, status:, **attributes)
|
|
15
|
+
validate_optional_attributes(attributes)
|
|
16
|
+
super(
|
|
17
|
+
id: freeze_value(id),
|
|
18
|
+
email: freeze_value(email),
|
|
19
|
+
status: freeze_value(status),
|
|
20
|
+
**USER_OPTIONAL_ATTRIBUTES.to_h { |name| [name, freeze_value(attributes[name])] }
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def validate_optional_attributes(attributes)
|
|
27
|
+
unknown = attributes.keys - USER_OPTIONAL_ATTRIBUTES
|
|
28
|
+
raise ArgumentError, "unknown keywords: #{unknown.join(', ')}" unless unknown.empty?
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def freeze_value(value)
|
|
32
|
+
case value
|
|
33
|
+
when Hash then freeze_value_hash(value)
|
|
34
|
+
when Array then value.map { |item| freeze_value(item) }.freeze
|
|
35
|
+
when String then value.dup.freeze
|
|
36
|
+
else freeze_value_scalar(value)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def freeze_value_scalar(value)
|
|
41
|
+
value.frozen? ? value : value.dup.freeze
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def freeze_value_hash(value)
|
|
45
|
+
value.to_h { |key, item| [freeze_value(key), freeze_value(item)] }.freeze
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
data/lib/volcano/version.rb
CHANGED
data/lib/volcano.rb
CHANGED
|
@@ -3,11 +3,13 @@
|
|
|
3
3
|
require_relative 'volcano/version'
|
|
4
4
|
require_relative 'volcano/connection_string'
|
|
5
5
|
require_relative 'volcano/models'
|
|
6
|
+
require_relative 'volcano/user'
|
|
6
7
|
require_relative 'volcano/session'
|
|
7
8
|
require_relative 'volcano/function_response'
|
|
8
9
|
require_relative 'volcano/durable_models'
|
|
9
10
|
require_relative 'volcano/log_responses'
|
|
10
11
|
require_relative 'volcano/storage_models'
|
|
12
|
+
require_relative 'volcano/storage_upload_models'
|
|
11
13
|
require_relative 'volcano/session_operations'
|
|
12
14
|
require_relative 'volcano/auth_state'
|
|
13
15
|
require_relative 'volcano/errors'
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: volcano-sdk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.10.
|
|
4
|
+
version: 0.10.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kong
|
|
@@ -148,6 +148,7 @@ files:
|
|
|
148
148
|
- lib/volcano/auth_sign_out.rb
|
|
149
149
|
- lib/volcano/auth_sign_up.rb
|
|
150
150
|
- lib/volcano/auth_state.rb
|
|
151
|
+
- lib/volcano/auth_state_notifications.rb
|
|
151
152
|
- lib/volcano/auth_update_user.rb
|
|
152
153
|
- lib/volcano/client.rb
|
|
153
154
|
- lib/volcano/connection_string.rb
|
|
@@ -597,8 +598,10 @@ files:
|
|
|
597
598
|
- lib/volcano/storage_mutations.rb
|
|
598
599
|
- lib/volcano/storage_public_url.rb
|
|
599
600
|
- lib/volcano/storage_resumable_upload.rb
|
|
601
|
+
- lib/volcano/storage_upload_models.rb
|
|
600
602
|
- lib/volcano/storage_upload_sessions.rb
|
|
601
603
|
- lib/volcano/transport.rb
|
|
604
|
+
- lib/volcano/user.rb
|
|
602
605
|
- lib/volcano/version.rb
|
|
603
606
|
homepage: https://github.com/Kong/volcano-sdk-ruby
|
|
604
607
|
licenses:
|