volcano-sdk 0.32.0 → 0.32.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 +13 -7
- data/lib/volcano/database.rb +28 -37
- data/lib/volcano/durable.rb +6 -6
- data/lib/volcano/durable_models.rb +65 -22
- data/lib/volcano/function_auth.rb +9 -2
- data/lib/volcano/functions.rb +18 -15
- data/lib/volcano/immutable_request_value.rb +38 -0
- data/lib/volcano/logs.rb +14 -9
- data/lib/volcano/models.rb +55 -22
- data/lib/volcano/realtime/channel_callbacks.rb +4 -1
- data/lib/volcano/realtime/postgres_changes.rb +2 -0
- data/lib/volcano/session.rb +14 -3
- data/lib/volcano/transport.rb +53 -11
- data/lib/volcano/user.rb +12 -7
- data/lib/volcano/version.rb +1 -1
- data/lib/volcano.rb +1 -0
- data/sig/auth_credentials.rbs +3 -1
- data/sig/auth_session_records.rbs +1 -1
- data/sig/client.rbs +15 -0
- data/sig/core_facades.rbs +81 -0
- data/sig/durable_records.rbs +1 -1
- data/sig/locks.rbs +10 -0
- data/sig/realtime.rbs +8 -9
- data/sig/user.rbs +1 -1
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ea2aa2e1bb816dd0f4feef5c80d3dab616b24d56b54e81394fb63d5774bddae0
|
|
4
|
+
data.tar.gz: 7ce5d0819d1a808fb4ee54c63c830a2a5173b0d15449174a007ac4b03236f0ac
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '037199a369e46cb09775fb9e85b1396ff373d3a39b88411670ccae2b9a3494985755016b1af78f41d84cbe674ff5703c3f1cef4c49b7c8f05a3e9e7ba46b38ed'
|
|
7
|
+
data.tar.gz: d178936e2018bcad91947371b2ee56878ef5f06f42df97e5a273035ae9395a2219062efd80bad18c2d8b4f13abd5eb8eb4908d17d8e46561faa1f4a36ac77471
|
data/README.md
CHANGED
|
@@ -505,26 +505,32 @@ handle = client.durable.start(
|
|
|
505
505
|
execution_name: "order-9"
|
|
506
506
|
)
|
|
507
507
|
|
|
508
|
-
|
|
508
|
+
owner_client = Volcano::Client.new(
|
|
509
|
+
api_url: "https://api.volcano.dev",
|
|
510
|
+
anon_key: ENV.fetch("VOLCANO_ANON_KEY"),
|
|
511
|
+
access_token: ENV.fetch("VOLCANO_PLATFORM_TOKEN")
|
|
512
|
+
)
|
|
513
|
+
execution = owner_client.durable.get(project_id, "charge-order", handle.id)
|
|
509
514
|
puts [execution.status, execution.terminal?, execution.result]
|
|
510
515
|
|
|
511
|
-
page =
|
|
516
|
+
page = owner_client.durable.list(project_id, "charge-order", status: "running")
|
|
512
517
|
puts [page.total, page.has_more]
|
|
513
518
|
|
|
514
|
-
|
|
519
|
+
owner_client.durable.stop(project_id, "charge-order", handle.id)
|
|
515
520
|
```
|
|
516
521
|
|
|
517
522
|
`start` takes a durable function's name or its id and returns a handle, never a
|
|
518
|
-
result: an execution can run for
|
|
523
|
+
result: an execution can run for up to 366 days, so its result is read back with `get`.
|
|
519
524
|
It uses the same credential `invoke` does — the active user session, then a
|
|
520
525
|
configured service key, then the anonymous key — and is the only durable
|
|
521
526
|
operation an application credential may perform. An `execution_name` makes the
|
|
522
527
|
start idempotent: starting again under the same name returns the execution that
|
|
523
528
|
already exists rather than beginning a second one.
|
|
524
529
|
|
|
525
|
-
`get`, `list`, and `stop` are owner-scoped and require
|
|
526
|
-
|
|
527
|
-
|
|
530
|
+
`get`, `list`, and `stop` are owner-scoped and require the project owner's
|
|
531
|
+
platform user token, because an execution is addressed by its id alone.
|
|
532
|
+
Auth-user sessions, anonymous keys, service keys, and project access tokens are
|
|
533
|
+
not accepted. `get` carries the deeply
|
|
528
534
|
frozen `result` once the execution has succeeded, and `error` when it failed;
|
|
529
535
|
`result_expired` separates a result the platform has discarded from a function
|
|
530
536
|
that returned nothing. `terminal?` reports whether the execution has stopped
|
data/lib/volcano/database.rb
CHANGED
|
@@ -1,33 +1,21 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# Ruby SDK runtime and immutable database builders.
|
|
3
4
|
module Volcano
|
|
4
|
-
# Captures caller-owned query values before storing them in immutable builders.
|
|
5
|
-
module ImmutableQueryValue
|
|
6
|
-
module_function
|
|
7
|
-
|
|
8
|
-
def capture(value)
|
|
9
|
-
case value
|
|
10
|
-
when String then value.dup.freeze
|
|
11
|
-
when Array then value.map { |item| capture(item) }.freeze
|
|
12
|
-
when Hash then capture_hash(value)
|
|
13
|
-
else value
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def capture_hash(value)
|
|
18
|
-
value.to_h { |key, item| [capture(key), capture(item)] }.freeze
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
private_constant :ImmutableQueryValue
|
|
22
|
-
|
|
23
5
|
QueryContext = Data.define(:client, :transport, :database_name)
|
|
24
6
|
private_constant :QueryContext
|
|
25
7
|
QueryState = Data.define(:columns, :filters, :order, :limit, :offset)
|
|
26
8
|
private_constant :QueryState
|
|
9
|
+
# @type var empty_columns: Array[String]
|
|
10
|
+
empty_columns = []
|
|
11
|
+
empty_columns.freeze
|
|
12
|
+
# @type var empty_conditions: Array[query_condition]
|
|
13
|
+
empty_conditions = []
|
|
14
|
+
empty_conditions.freeze
|
|
27
15
|
EMPTY_QUERY_STATE = QueryState.new(
|
|
28
|
-
columns:
|
|
29
|
-
filters:
|
|
30
|
-
order:
|
|
16
|
+
columns: empty_columns,
|
|
17
|
+
filters: empty_conditions,
|
|
18
|
+
order: empty_conditions,
|
|
31
19
|
limit: nil,
|
|
32
20
|
offset: nil
|
|
33
21
|
)
|
|
@@ -35,6 +23,7 @@ module Volcano
|
|
|
35
23
|
|
|
36
24
|
# Adds the shared immutable filter vocabulary to database builders.
|
|
37
25
|
module FilterMethods
|
|
26
|
+
# @dynamic copy
|
|
38
27
|
def eq(column, value) = add_filter(column, 'eq', value)
|
|
39
28
|
def neq(column, value) = add_filter(column, 'neq', value)
|
|
40
29
|
def gt(column, value) = add_filter(column, 'gt', value)
|
|
@@ -50,9 +39,9 @@ module Volcano
|
|
|
50
39
|
|
|
51
40
|
def add_filter(column, operator, value)
|
|
52
41
|
condition = {
|
|
53
|
-
'column' =>
|
|
42
|
+
'column' => ImmutableRequestValue.capture(column),
|
|
54
43
|
'operator' => operator,
|
|
55
|
-
'value' =>
|
|
44
|
+
'value' => ImmutableRequestValue.capture(value)
|
|
56
45
|
}.freeze
|
|
57
46
|
copy(filters: [*@filters, condition])
|
|
58
47
|
end
|
|
@@ -62,7 +51,7 @@ module Volcano
|
|
|
62
51
|
# Creates immutable queries scoped to one project database.
|
|
63
52
|
class Database
|
|
64
53
|
def initialize(client, transport, name)
|
|
65
|
-
database_name =
|
|
54
|
+
database_name = ImmutableRequestValue.capture(name)
|
|
66
55
|
@context = QueryContext.new(client:, transport:, database_name:)
|
|
67
56
|
end
|
|
68
57
|
|
|
@@ -77,7 +66,7 @@ module Volcano
|
|
|
77
66
|
|
|
78
67
|
def initialize(context, table, state: EMPTY_QUERY_STATE)
|
|
79
68
|
@context = context
|
|
80
|
-
@table =
|
|
69
|
+
@table = ImmutableRequestValue.capture(table)
|
|
81
70
|
@columns = state.columns
|
|
82
71
|
@filters = state.filters
|
|
83
72
|
@order = state.order
|
|
@@ -87,13 +76,13 @@ module Volcano
|
|
|
87
76
|
end
|
|
88
77
|
|
|
89
78
|
def select(*columns)
|
|
90
|
-
copy(columns:
|
|
79
|
+
copy(columns: ImmutableRequestValue.capture(columns))
|
|
91
80
|
end
|
|
92
81
|
|
|
93
|
-
def insert(values) = InsertBuilder.new(@context, @table,
|
|
82
|
+
def insert(values) = InsertBuilder.new(@context, @table, ImmutableRequestValue.capture(values))
|
|
94
83
|
|
|
95
84
|
def update(values)
|
|
96
|
-
UpdateBuilder.new(@context, @table,
|
|
85
|
+
UpdateBuilder.new(@context, @table, ImmutableRequestValue.capture(values), filters: @filters)
|
|
97
86
|
end
|
|
98
87
|
|
|
99
88
|
def delete
|
|
@@ -101,7 +90,7 @@ module Volcano
|
|
|
101
90
|
end
|
|
102
91
|
|
|
103
92
|
def order(column, ascending: true)
|
|
104
|
-
clause = { 'column' =>
|
|
93
|
+
clause = { 'column' => ImmutableRequestValue.capture(column), 'ascending' => ascending }.freeze
|
|
105
94
|
copy(order: [*@order, clause])
|
|
106
95
|
end
|
|
107
96
|
|
|
@@ -123,7 +112,7 @@ module Volcano
|
|
|
123
112
|
)
|
|
124
113
|
end
|
|
125
114
|
end
|
|
126
|
-
Transport.body(response, 200)
|
|
115
|
+
Transport.json_rows(Transport.body(response, 200))
|
|
127
116
|
end
|
|
128
117
|
|
|
129
118
|
private
|
|
@@ -149,7 +138,9 @@ module Volcano
|
|
|
149
138
|
end
|
|
150
139
|
|
|
151
140
|
def query_body
|
|
152
|
-
|
|
141
|
+
# @type var body: sdk_payload
|
|
142
|
+
body = { 'table' => @table }
|
|
143
|
+
body.tap do |body|
|
|
153
144
|
add_query_selection(body)
|
|
154
145
|
body['limit'] = @limit unless @limit.nil?
|
|
155
146
|
body['offset'] = @offset unless @offset.nil?
|
|
@@ -184,7 +175,7 @@ module Volcano
|
|
|
184
175
|
)
|
|
185
176
|
end
|
|
186
177
|
end
|
|
187
|
-
Transport.body(response, 200)
|
|
178
|
+
Transport.json_rows(Transport.body(response, 200))
|
|
188
179
|
end
|
|
189
180
|
end
|
|
190
181
|
|
|
@@ -192,7 +183,7 @@ module Volcano
|
|
|
192
183
|
class UpdateBuilder
|
|
193
184
|
include FilterMethods
|
|
194
185
|
|
|
195
|
-
def initialize(context, table, values, filters:
|
|
186
|
+
def initialize(context, table, values, filters: EMPTY_QUERY_STATE.filters)
|
|
196
187
|
@context = context
|
|
197
188
|
@table = table
|
|
198
189
|
@values = values
|
|
@@ -210,7 +201,7 @@ module Volcano
|
|
|
210
201
|
)
|
|
211
202
|
end
|
|
212
203
|
end
|
|
213
|
-
Transport.body(response, 200)
|
|
204
|
+
Transport.json_rows(Transport.body(response, 200))
|
|
214
205
|
end
|
|
215
206
|
|
|
216
207
|
private
|
|
@@ -224,7 +215,7 @@ module Volcano
|
|
|
224
215
|
class DeleteBuilder
|
|
225
216
|
include FilterMethods
|
|
226
217
|
|
|
227
|
-
def initialize(context, table, filters:
|
|
218
|
+
def initialize(context, table, filters: EMPTY_QUERY_STATE.filters)
|
|
228
219
|
@context = context
|
|
229
220
|
@table = table
|
|
230
221
|
@filters = filters
|
|
@@ -241,7 +232,7 @@ module Volcano
|
|
|
241
232
|
)
|
|
242
233
|
end
|
|
243
234
|
end
|
|
244
|
-
Transport.body(response, 200)
|
|
235
|
+
Transport.json_rows(Transport.body(response, 200))
|
|
245
236
|
end
|
|
246
237
|
|
|
247
238
|
private
|
data/lib/volcano/durable.rb
CHANGED
|
@@ -40,10 +40,10 @@ module Volcano
|
|
|
40
40
|
|
|
41
41
|
# Reads an execution, including its result once it has succeeded.
|
|
42
42
|
#
|
|
43
|
-
# Owner-scoped: an execution is addressed by its id alone
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
#
|
|
43
|
+
# Owner-scoped: an execution is addressed by its id alone, so this takes
|
|
44
|
+
# the project id and the project owner's platform user token. Auth-user
|
|
45
|
+
# sessions, anon keys, service keys, and project access tokens are not
|
|
46
|
+
# accepted. Poll it from a backend, not a browser.
|
|
47
47
|
def get(project_id, function_name, execution_id)
|
|
48
48
|
project, function_id, execution = execution_path(project_id, function_name, execution_id)
|
|
49
49
|
response = Transport.invoke do
|
|
@@ -100,7 +100,7 @@ module Volcano
|
|
|
100
100
|
# An empty path segment would address the collection instead of the
|
|
101
101
|
# execution, which is a different request rather than a failed one.
|
|
102
102
|
def identifier(value, field)
|
|
103
|
-
raise ArgumentError, "#{field} must be a non-empty String" unless present_string?(value)
|
|
103
|
+
raise ArgumentError, "#{field} must be a non-empty String" unless value.is_a?(String) && present_string?(value)
|
|
104
104
|
|
|
105
105
|
value.strip.freeze
|
|
106
106
|
end
|
|
@@ -128,7 +128,7 @@ module Volcano
|
|
|
128
128
|
end
|
|
129
129
|
|
|
130
130
|
def validate_page_limit(limit)
|
|
131
|
-
return if limit.nil? || (positive_integer?(limit) && limit <= MAX_PAGE_SIZE)
|
|
131
|
+
return if limit.nil? || (limit.is_a?(Integer) && positive_integer?(limit) && limit <= MAX_PAGE_SIZE)
|
|
132
132
|
|
|
133
133
|
raise ArgumentError, "limit must be an Integer between 1 and #{MAX_PAGE_SIZE}"
|
|
134
134
|
end
|
|
@@ -15,9 +15,16 @@ module Volcano
|
|
|
15
15
|
private_constant :DURABLE_TERMINAL_STATUSES
|
|
16
16
|
|
|
17
17
|
# Why a failed or timed-out execution ended.
|
|
18
|
-
DurableExecutionError = Data.define(:type, :message)
|
|
18
|
+
DurableExecutionError = Data.define(:type, :message)
|
|
19
|
+
|
|
20
|
+
# Reopens the generated record for checked initialization.
|
|
21
|
+
class DurableExecutionError
|
|
22
|
+
# @dynamic type, message, members, with, to_h, deconstruct, deconstruct_keys
|
|
23
|
+
# @dynamic self.[], self.members
|
|
19
24
|
def initialize(type: nil, message: nil)
|
|
20
|
-
|
|
25
|
+
type = type&.dup&.freeze
|
|
26
|
+
message = message&.dup&.freeze
|
|
27
|
+
super
|
|
21
28
|
end
|
|
22
29
|
end
|
|
23
30
|
|
|
@@ -27,7 +34,13 @@ module Volcano
|
|
|
27
34
|
# once its retention lapses. That is not the same as a function that returned
|
|
28
35
|
# nothing, so read `result_expired` before concluding anything from a missing
|
|
29
36
|
# result.
|
|
30
|
-
DurableExecution = Data.define(*DURABLE_EXECUTION_ATTRIBUTES)
|
|
37
|
+
DurableExecution = Data.define(*DURABLE_EXECUTION_ATTRIBUTES)
|
|
38
|
+
|
|
39
|
+
# Reopens the generated record for checked initialization.
|
|
40
|
+
class DurableExecution
|
|
41
|
+
# @dynamic id, function_id, name, status, region, created_at, result, result_expired
|
|
42
|
+
# @dynamic error, completed_at, members, with, to_h, deconstruct, deconstruct_keys
|
|
43
|
+
# @dynamic self.[], self.members
|
|
31
44
|
def initialize(**attributes)
|
|
32
45
|
unknown = attributes.keys - DURABLE_EXECUTION_ATTRIBUTES
|
|
33
46
|
raise ArgumentError, "unknown keywords: #{unknown.join(', ')}" unless unknown.empty?
|
|
@@ -38,7 +51,8 @@ module Volcano
|
|
|
38
51
|
values = DURABLE_EXECUTION_ATTRIBUTES.to_h do |name|
|
|
39
52
|
[name, immutable_value(attributes[name])]
|
|
40
53
|
end
|
|
41
|
-
|
|
54
|
+
attributes = values
|
|
55
|
+
super
|
|
42
56
|
end
|
|
43
57
|
|
|
44
58
|
# Reports whether the execution has stopped changing.
|
|
@@ -64,12 +78,15 @@ module Volcano
|
|
|
64
78
|
end
|
|
65
79
|
|
|
66
80
|
# One page of a durable function's executions, most recent first.
|
|
67
|
-
DurableExecutionPage = Data.define(:executions, :page, :limit, :total, :has_more)
|
|
81
|
+
DurableExecutionPage = Data.define(:executions, :page, :limit, :total, :has_more)
|
|
82
|
+
|
|
83
|
+
# Reopens the generated record for checked initialization.
|
|
84
|
+
class DurableExecutionPage
|
|
85
|
+
# @dynamic executions, page, limit, total, has_more, members, with, to_h
|
|
86
|
+
# @dynamic deconstruct, deconstruct_keys, self.[], self.members
|
|
68
87
|
def initialize(executions:, page:, limit:, total:, has_more:)
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
page: page, limit: limit, total: total, has_more: has_more
|
|
72
|
-
)
|
|
88
|
+
executions = executions.to_a.dup.freeze
|
|
89
|
+
super
|
|
73
90
|
end
|
|
74
91
|
end
|
|
75
92
|
|
|
@@ -77,18 +94,17 @@ module Volcano
|
|
|
77
94
|
module DurableResponses
|
|
78
95
|
INCOMPLETE_EXECUTION = 'Expected a complete durable execution'
|
|
79
96
|
INCOMPLETE_PAGE = 'Expected a complete durable execution page'
|
|
80
|
-
|
|
81
|
-
private_constant :INCOMPLETE_EXECUTION, :INCOMPLETE_PAGE, :EXECUTION_FIELDS
|
|
97
|
+
private_constant :INCOMPLETE_EXECUTION, :INCOMPLETE_PAGE
|
|
82
98
|
|
|
83
99
|
private
|
|
84
100
|
|
|
85
101
|
def durable_execution(payload)
|
|
86
102
|
values = execution_payload(payload)
|
|
87
103
|
DurableExecution.new(
|
|
88
|
-
id: values
|
|
89
|
-
name: values
|
|
90
|
-
region: values
|
|
91
|
-
result: values['result'], result_expired: values['result_expired'],
|
|
104
|
+
id: execution_text(values, 'id'), function_id: execution_text(values, 'function_id'),
|
|
105
|
+
name: execution_text(values, 'name'), status: execution_text(values, 'status'),
|
|
106
|
+
region: execution_text(values, 'region'), created_at: execution_time(values),
|
|
107
|
+
result: execution_result(values['result']), result_expired: execution_expired(values['result_expired']),
|
|
92
108
|
error: execution_error(values['error']), completed_at: parse_time(values['completed_at'])
|
|
93
109
|
)
|
|
94
110
|
end
|
|
@@ -96,12 +112,36 @@ module Volcano
|
|
|
96
112
|
def execution_payload(payload)
|
|
97
113
|
raise TypeError, INCOMPLETE_EXECUTION unless payload.is_a?(Hash)
|
|
98
114
|
|
|
99
|
-
complete = EXECUTION_FIELDS.all? { |field| present_string?(payload[field]) }
|
|
100
|
-
raise TypeError, INCOMPLETE_EXECUTION unless complete && payload['created_at']
|
|
101
|
-
|
|
102
115
|
payload
|
|
103
116
|
end
|
|
104
117
|
|
|
118
|
+
def execution_text(values, field)
|
|
119
|
+
value = values[field]
|
|
120
|
+
raise TypeError, INCOMPLETE_EXECUTION unless value.is_a?(String) && !value.strip.empty?
|
|
121
|
+
|
|
122
|
+
value
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def execution_time(values)
|
|
126
|
+
case (value = values['created_at'])
|
|
127
|
+
when String then Time.iso8601(value)
|
|
128
|
+
when Time then value
|
|
129
|
+
else raise TypeError, INCOMPLETE_EXECUTION
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def execution_expired(value)
|
|
134
|
+
return value if value.nil? || value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
|
135
|
+
|
|
136
|
+
raise TypeError, INCOMPLETE_EXECUTION
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def execution_result(value)
|
|
140
|
+
Transport.json_value(value)
|
|
141
|
+
rescue TypeError
|
|
142
|
+
raise TypeError, INCOMPLETE_EXECUTION, cause: nil
|
|
143
|
+
end
|
|
144
|
+
|
|
105
145
|
def execution_error(payload)
|
|
106
146
|
return if payload.nil?
|
|
107
147
|
raise TypeError, INCOMPLETE_EXECUTION unless payload.is_a?(Hash)
|
|
@@ -110,10 +150,10 @@ module Volcano
|
|
|
110
150
|
end
|
|
111
151
|
|
|
112
152
|
def durable_execution_page(payload)
|
|
113
|
-
data, has_more = page_payload(payload)
|
|
153
|
+
values, data, has_more = page_payload(payload)
|
|
114
154
|
DurableExecutionPage.new(
|
|
115
155
|
executions: data.map { |entry| durable_execution(entry) }, has_more: has_more,
|
|
116
|
-
page: count(
|
|
156
|
+
page: count(values['page']), limit: count(values['limit']), total: count(values['total'])
|
|
117
157
|
)
|
|
118
158
|
end
|
|
119
159
|
|
|
@@ -124,7 +164,7 @@ module Volcano
|
|
|
124
164
|
has_more = payload['has_more']
|
|
125
165
|
raise TypeError, INCOMPLETE_PAGE unless data.is_a?(Array) && [true, false].include?(has_more)
|
|
126
166
|
|
|
127
|
-
[data, has_more]
|
|
167
|
+
[payload, data, has_more]
|
|
128
168
|
end
|
|
129
169
|
|
|
130
170
|
def count(value)
|
|
@@ -138,7 +178,10 @@ module Volcano
|
|
|
138
178
|
end
|
|
139
179
|
|
|
140
180
|
def parse_time(value)
|
|
141
|
-
|
|
181
|
+
return Time.iso8601(value) if value.is_a?(String)
|
|
182
|
+
return value if value.nil? || value.is_a?(Time)
|
|
183
|
+
|
|
184
|
+
raise TypeError, INCOMPLETE_EXECUTION
|
|
142
185
|
end
|
|
143
186
|
end
|
|
144
187
|
private_constant :DurableResponses
|
|
@@ -32,7 +32,7 @@ module Volcano
|
|
|
32
32
|
|
|
33
33
|
def refresh(original)
|
|
34
34
|
# Resolution has released its cache lock before refresh callbacks run.
|
|
35
|
-
@client.auth.__send__(:refresh_captured_session,
|
|
35
|
+
@client.auth.__send__(:refresh_captured_session, active_binding)
|
|
36
36
|
rescue Error::SessionChangedError
|
|
37
37
|
raise
|
|
38
38
|
rescue Error::VolcanoError
|
|
@@ -41,11 +41,18 @@ module Volcano
|
|
|
41
41
|
|
|
42
42
|
def validate
|
|
43
43
|
if @binding.last
|
|
44
|
-
@client.auth.__send__(:validate_read_failure,
|
|
44
|
+
@client.auth.__send__(:validate_read_failure, active_binding)
|
|
45
45
|
elsif @client.capture_session_binding[1] != @binding[1]
|
|
46
46
|
raise Error::SessionChangedError
|
|
47
47
|
end
|
|
48
48
|
end
|
|
49
|
+
|
|
50
|
+
def active_binding
|
|
51
|
+
generation, owner, session = @binding
|
|
52
|
+
raise Error::SessionChangedError unless session
|
|
53
|
+
|
|
54
|
+
[generation, owner, session]
|
|
55
|
+
end
|
|
49
56
|
end
|
|
50
57
|
private_constant :FunctionAuth
|
|
51
58
|
end
|
data/lib/volcano/functions.rb
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
require_relative 'function_auth'
|
|
4
4
|
|
|
5
5
|
module Volcano
|
|
6
|
+
INVALID_FUNCTION_NAME = 'Function name must be DNS-safe: lowercase letters, numbers, and hyphens; 1-63 characters'
|
|
7
|
+
private_constant :INVALID_FUNCTION_NAME
|
|
8
|
+
|
|
6
9
|
# Invokes deployed Volcano functions by name.
|
|
7
10
|
class Functions
|
|
8
11
|
FUNCTION_NAME = /\A[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\z/
|
|
@@ -21,7 +24,7 @@ module Volcano
|
|
|
21
24
|
def invoke(name, payload = {})
|
|
22
25
|
auth = FunctionAuth.new(@client)
|
|
23
26
|
validate_invocation(name, payload)
|
|
24
|
-
invoke_owned(auth, name.dup.freeze,
|
|
27
|
+
invoke_owned(auth, name.dup.freeze, ImmutableRequestValue.capture(payload))
|
|
25
28
|
end
|
|
26
29
|
|
|
27
30
|
private
|
|
@@ -53,9 +56,7 @@ module Volcano
|
|
|
53
56
|
# X-Volcano-Function-Invoked only after dispatch, so its absence is what
|
|
54
57
|
# separates the two. X-Volcano-Version cannot: the server stamps it on every
|
|
55
58
|
# response, including errors raised before the function is reached.
|
|
56
|
-
def stale_mapping?(response)
|
|
57
|
-
response.status == 404 && header(response.headers, FUNCTION_INVOKED_HEADER).nil?
|
|
58
|
-
end
|
|
59
|
+
def stale_mapping?(response) = response.status == 404 && header(response.headers, FUNCTION_INVOKED_HEADER).nil?
|
|
59
60
|
|
|
60
61
|
def invoke_resolved(authorization, resolution, payload)
|
|
61
62
|
if resolution.invoke_url.nil?
|
|
@@ -77,8 +78,8 @@ module Volcano
|
|
|
77
78
|
# Hold the name's lock across the round trip so concurrent callers wait
|
|
78
79
|
# for one resolve instead of each opening their own.
|
|
79
80
|
FunctionResolution.resolve_lock(@api_url, authorization, name).synchronize do
|
|
80
|
-
|
|
81
|
-
next cached_resolution(
|
|
81
|
+
cached_under_lock = FunctionResolution.lookup(@api_url, authorization, name)
|
|
82
|
+
next cached_resolution(cached_under_lock) if cached_under_lock
|
|
82
83
|
|
|
83
84
|
resolve_uncached(authorization, name)
|
|
84
85
|
end
|
|
@@ -88,7 +89,7 @@ module Volcano
|
|
|
88
89
|
resolved = Transport.invoke do
|
|
89
90
|
@transport.resolve_function_for_invocation(authorization: authorization, name: name)
|
|
90
91
|
end
|
|
91
|
-
payload = Transport.body(resolved, 200)
|
|
92
|
+
payload = Transport.json_object(Transport.body(resolved, 200), message: 'Expected a complete function response')
|
|
92
93
|
resolution = resolved_resolution(payload)
|
|
93
94
|
FunctionResolution.store(@api_url, authorization, name, resolution, cache_ttl(payload))
|
|
94
95
|
resolution
|
|
@@ -98,24 +99,26 @@ module Volcano
|
|
|
98
99
|
end
|
|
99
100
|
|
|
100
101
|
def cached_resolution(cached)
|
|
101
|
-
cached.resolution
|
|
102
|
+
return cached.resolution if cached.resolution
|
|
103
|
+
|
|
104
|
+
failure = cached.failure || raise(TypeError, 'Expected a cached function resolution')
|
|
105
|
+
raise FunctionResolution.error_for(failure)
|
|
102
106
|
end
|
|
103
107
|
|
|
104
108
|
def validate_invocation(name, payload)
|
|
105
|
-
unless name.is_a?(String) && FUNCTION_NAME.match?(name)
|
|
106
|
-
raise ArgumentError,
|
|
107
|
-
'Function name must be DNS-safe: lowercase letters, numbers, and hyphens; 1-63 characters'
|
|
108
|
-
end
|
|
109
|
+
raise ArgumentError, INVALID_FUNCTION_NAME unless name.is_a?(String) && FUNCTION_NAME.match?(name)
|
|
109
110
|
raise TypeError, 'Function payload must be a Hash' unless payload.is_a?(Hash)
|
|
110
111
|
end
|
|
111
112
|
|
|
112
113
|
def resolved_resolution(payload)
|
|
113
|
-
|
|
114
|
+
payload = Transport.json_object(payload)
|
|
115
|
+
function_id = payload['function_id']
|
|
114
116
|
raise TypeError, 'Expected a complete function response' unless function_id.is_a?(String) && !function_id.empty?
|
|
115
117
|
|
|
116
118
|
# Absent when the deployment serves no public invocation domain, as in
|
|
117
119
|
# local development; the function is reached through the API instead.
|
|
118
|
-
|
|
120
|
+
candidate = payload['invoke_url']
|
|
121
|
+
invoke_url = FunctionResolution.valid_invoke_url(candidate.is_a?(String) ? candidate : nil, @api_url)
|
|
119
122
|
FunctionResolution::Resolution.new(function_id: function_id, invoke_url: invoke_url)
|
|
120
123
|
end
|
|
121
124
|
|
|
@@ -136,7 +139,7 @@ module Volcano
|
|
|
136
139
|
Transport.body(response, 200) unless response.status.between?(200, 299) || dispatched
|
|
137
140
|
|
|
138
141
|
FunctionResponse.new(
|
|
139
|
-
data: response.body, status: response.status,
|
|
142
|
+
data: Transport.json_value(response.body), status: response.status,
|
|
140
143
|
headers: response.headers || {}, version: version
|
|
141
144
|
)
|
|
142
145
|
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Ruby SDK runtime and immutable caller-owned request values.
|
|
4
|
+
module Volcano
|
|
5
|
+
# Copies JSON-like request values before storing them in builders or sending them.
|
|
6
|
+
module ImmutableRequestValue
|
|
7
|
+
def self.capture(value)
|
|
8
|
+
# @type var active: Hash[Integer, bool]
|
|
9
|
+
active = {}
|
|
10
|
+
capture_value(value, active)
|
|
11
|
+
rescue SystemStackError
|
|
12
|
+
raise TypeError, 'Request value nesting is too deep'
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.capture_value(value, active)
|
|
16
|
+
case value
|
|
17
|
+
when String, Time then value.dup.freeze
|
|
18
|
+
when Array, Hash then capture_container(value, active)
|
|
19
|
+
else value
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.capture_container(value, active)
|
|
24
|
+
id = value.object_id
|
|
25
|
+
raise TypeError, 'Request value contains a cycle' if active.key?(id)
|
|
26
|
+
|
|
27
|
+
active[id] = true
|
|
28
|
+
copy = value.is_a?(Array) ? value.map { |item| capture_value(item, active) } : capture_hash(value, active)
|
|
29
|
+
active.delete(id)
|
|
30
|
+
copy.freeze
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.capture_hash(value, active)
|
|
34
|
+
value.to_h { |key, item| [capture_value(key, active), capture_value(item, active)] }
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
private_constant :ImmutableRequestValue
|
|
38
|
+
end
|
data/lib/volcano/logs.rb
CHANGED
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
module Volcano
|
|
4
4
|
# Searches retained project logs and activity.
|
|
5
5
|
class Logs
|
|
6
|
-
include ImmutableLogJSON
|
|
7
|
-
|
|
8
6
|
def initialize(client, transport)
|
|
9
7
|
@client = client
|
|
10
8
|
@transport = transport
|
|
@@ -46,26 +44,27 @@ module Volcano
|
|
|
46
44
|
end
|
|
47
45
|
raise TypeError, 'Log request must be a Hash' unless request.is_a?(Hash)
|
|
48
46
|
|
|
49
|
-
[project_id.dup.freeze,
|
|
47
|
+
[project_id.dup.freeze, ImmutableRequestValue.capture(request)]
|
|
50
48
|
end
|
|
51
49
|
|
|
52
50
|
def search_response(body)
|
|
53
|
-
data = response_data(body)
|
|
51
|
+
body, data = response_data(body)
|
|
54
52
|
limit = body['limit']
|
|
55
53
|
has_more = body['has_more']
|
|
56
54
|
next_cursor = body['next_cursor']
|
|
57
|
-
unless limit.is_a?(Integer) &&
|
|
58
|
-
(next_cursor.nil? || next_cursor.is_a?(String))
|
|
55
|
+
unless limit.is_a?(Integer) && boolean?(has_more) && (next_cursor.nil? || next_cursor.is_a?(String))
|
|
59
56
|
raise TypeError, 'Expected a complete log response'
|
|
60
57
|
end
|
|
61
58
|
|
|
59
|
+
has_more = has_more == true
|
|
60
|
+
|
|
62
61
|
LogSearchResponse.new(
|
|
63
62
|
data: data, limit: limit, has_more: has_more, next_cursor: next_cursor
|
|
64
63
|
)
|
|
65
64
|
end
|
|
66
65
|
|
|
67
66
|
def activity_response(body)
|
|
68
|
-
data = response_data(body)
|
|
67
|
+
body, data = response_data(body)
|
|
69
68
|
total = body['total']
|
|
70
69
|
raise TypeError, 'Expected a complete log response' unless total.is_a?(Integer)
|
|
71
70
|
|
|
@@ -73,10 +72,16 @@ module Volcano
|
|
|
73
72
|
end
|
|
74
73
|
|
|
75
74
|
def response_data(body)
|
|
76
|
-
|
|
75
|
+
raise TypeError, 'Expected a complete log response' unless body.is_a?(Hash)
|
|
76
|
+
|
|
77
|
+
data = body['data']
|
|
77
78
|
raise TypeError, 'Expected a complete log response' unless data.is_a?(Array) && data.all?(Hash)
|
|
78
79
|
|
|
79
|
-
data
|
|
80
|
+
[body, data]
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def boolean?(value)
|
|
84
|
+
value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
|
80
85
|
end
|
|
81
86
|
end
|
|
82
87
|
end
|
data/lib/volcano/models.rb
CHANGED
|
@@ -7,7 +7,14 @@ module Volcano
|
|
|
7
7
|
].freeze
|
|
8
8
|
private_constant :AUTH_SESSION_ATTRIBUTES
|
|
9
9
|
|
|
10
|
-
AuthSession = Data.define(*AUTH_SESSION_ATTRIBUTES)
|
|
10
|
+
AuthSession = Data.define(*AUTH_SESSION_ATTRIBUTES)
|
|
11
|
+
|
|
12
|
+
# Reopens the generated record for checked initialization.
|
|
13
|
+
class AuthSession
|
|
14
|
+
# @dynamic id, user_id, provider, expires_at, is_active, is_current, user_agent
|
|
15
|
+
# @dynamic ip_address, last_ip_address, last_activity_at, session_started_at
|
|
16
|
+
# @dynamic created_at, updated_at, members, with, to_h, deconstruct
|
|
17
|
+
# @dynamic deconstruct_keys, self.[], self.members
|
|
11
18
|
def initialize(**attributes)
|
|
12
19
|
unknown = attributes.keys - AUTH_SESSION_ATTRIBUTES
|
|
13
20
|
raise ArgumentError, "unknown keywords: #{unknown.join(', ')}" unless unknown.empty?
|
|
@@ -18,7 +25,8 @@ module Volcano
|
|
|
18
25
|
values = AUTH_SESSION_ATTRIBUTES.to_h do |name|
|
|
19
26
|
[name, freeze_session_value(attributes[name])]
|
|
20
27
|
end
|
|
21
|
-
|
|
28
|
+
attributes = values
|
|
29
|
+
super
|
|
22
30
|
end
|
|
23
31
|
|
|
24
32
|
private
|
|
@@ -28,19 +36,28 @@ module Volcano
|
|
|
28
36
|
end
|
|
29
37
|
end
|
|
30
38
|
|
|
31
|
-
SessionPage = Data.define(:sessions, :total, :page, :limit, :total_pages)
|
|
39
|
+
SessionPage = Data.define(:sessions, :total, :page, :limit, :total_pages)
|
|
40
|
+
|
|
41
|
+
# Reopens the generated record for checked initialization.
|
|
42
|
+
class SessionPage
|
|
43
|
+
# @dynamic sessions, total, page, limit, total_pages, members, with, to_h
|
|
44
|
+
# @dynamic deconstruct, deconstruct_keys, self.[], self.members
|
|
32
45
|
def initialize(sessions:, total:, page:, limit:, total_pages:)
|
|
33
|
-
|
|
34
|
-
|
|
46
|
+
sessions = sessions.to_a.dup.freeze
|
|
47
|
+
super
|
|
35
48
|
end
|
|
36
49
|
end
|
|
37
|
-
LinkedOAuthProvider = Data.define(:provider, :linked_at, :updated_at)
|
|
50
|
+
LinkedOAuthProvider = Data.define(:provider, :linked_at, :updated_at)
|
|
51
|
+
|
|
52
|
+
# Reopens the generated record for checked initialization.
|
|
53
|
+
class LinkedOAuthProvider
|
|
54
|
+
# @dynamic provider, linked_at, updated_at, members, with, to_h, deconstruct
|
|
55
|
+
# @dynamic deconstruct_keys, self.[], self.members
|
|
38
56
|
def initialize(provider:, linked_at:, updated_at:)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
)
|
|
57
|
+
provider = immutable_value(provider)
|
|
58
|
+
linked_at = immutable_value(linked_at)
|
|
59
|
+
updated_at = immutable_value(updated_at)
|
|
60
|
+
super
|
|
44
61
|
end
|
|
45
62
|
|
|
46
63
|
private
|
|
@@ -49,30 +66,46 @@ module Volcano
|
|
|
49
66
|
value.frozen? ? value : value.dup.freeze
|
|
50
67
|
end
|
|
51
68
|
end
|
|
52
|
-
OAuthProviderTokenStatus = Data.define(:message, :provider, :expires_in)
|
|
69
|
+
OAuthProviderTokenStatus = Data.define(:message, :provider, :expires_in)
|
|
70
|
+
|
|
71
|
+
# Reopens the generated record for checked initialization.
|
|
72
|
+
class OAuthProviderTokenStatus
|
|
73
|
+
# @dynamic message, provider, expires_in, members, with, to_h, deconstruct
|
|
74
|
+
# @dynamic deconstruct_keys, self.[], self.members
|
|
53
75
|
def initialize(message:, provider:, expires_in:)
|
|
54
76
|
values = [message, provider]
|
|
55
77
|
valid = values.all? { |value| value.is_a?(String) && !value.strip.empty? }
|
|
56
78
|
raise TypeError, 'Expected complete OAuth provider token status' unless valid && expires_in.is_a?(Integer)
|
|
57
79
|
|
|
58
|
-
|
|
80
|
+
message = message.dup.freeze
|
|
81
|
+
provider = provider.dup.freeze
|
|
82
|
+
super
|
|
59
83
|
end
|
|
60
84
|
end
|
|
61
85
|
EmailChangeResult = Data.define(:message, :new_email)
|
|
62
|
-
LockLease = Data.define(:key, :token, :expires_at, :fencing_token)
|
|
86
|
+
LockLease = Data.define(:key, :token, :expires_at, :fencing_token)
|
|
87
|
+
|
|
88
|
+
# Reopens the generated record for checked initialization.
|
|
89
|
+
class LockLease
|
|
90
|
+
# @dynamic key, token, expires_at, fencing_token, members, with, to_h
|
|
91
|
+
# @dynamic deconstruct, deconstruct_keys, self.[], self.members
|
|
63
92
|
def initialize(key:, token:, expires_at:, fencing_token:)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
fencing_token: fencing_token
|
|
69
|
-
)
|
|
93
|
+
key = key.dup.freeze
|
|
94
|
+
token = token.dup.freeze
|
|
95
|
+
expires_at = expires_at&.dup&.freeze
|
|
96
|
+
super
|
|
70
97
|
end
|
|
71
98
|
end
|
|
72
|
-
LockState = Data.define(:held, :expires_at, :fencing_token)
|
|
99
|
+
LockState = Data.define(:held, :expires_at, :fencing_token)
|
|
100
|
+
|
|
101
|
+
# Reopens the generated record for checked initialization.
|
|
102
|
+
class LockState
|
|
103
|
+
# @dynamic held, expires_at, fencing_token, members, with, to_h, deconstruct
|
|
104
|
+
# @dynamic deconstruct_keys, self.[], self.members
|
|
73
105
|
def initialize(held:, expires_at:, fencing_token:)
|
|
74
106
|
immutable_expiry = expires_at&.then { |value| value.frozen? ? value : value.dup.freeze }
|
|
75
|
-
|
|
107
|
+
expires_at = immutable_expiry
|
|
108
|
+
super
|
|
76
109
|
end
|
|
77
110
|
end
|
|
78
111
|
end
|
|
@@ -10,7 +10,10 @@ module Volcano
|
|
|
10
10
|
def on(event, callback = nil, &block)
|
|
11
11
|
raise ArgumentError, "unsupported realtime event: #{event}" unless allowed_event?(event)
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
handler = callback || block || raise(ArgumentError, 'callback or block is required')
|
|
14
|
+
raise ArgumentError, 'callback must respond to call' unless handler.respond_to?(:call)
|
|
15
|
+
|
|
16
|
+
@callbacks[event] << handler
|
|
14
17
|
self
|
|
15
18
|
end
|
|
16
19
|
|
|
@@ -39,6 +39,8 @@ module Volcano
|
|
|
39
39
|
ensure_postgres!
|
|
40
40
|
event = postgres_event(event)
|
|
41
41
|
handler = callback || block || raise(ArgumentError, 'callback or block is required')
|
|
42
|
+
raise ArgumentError, 'callback must respond to call' unless handler.respond_to?(:call)
|
|
43
|
+
|
|
42
44
|
register_postgres_listener(event, schema.to_s, table.to_s, handler)
|
|
43
45
|
end
|
|
44
46
|
|
data/lib/volcano/session.rb
CHANGED
|
@@ -10,15 +10,26 @@ module Volcano
|
|
|
10
10
|
private_constant :SESSION_USER_CODER
|
|
11
11
|
|
|
12
12
|
# Local credentials; refresh credentials and user identity may be unknown.
|
|
13
|
-
Session = Data.define(:access_token, :refresh_token, :user_id, :user)
|
|
13
|
+
Session = Data.define(:access_token, :refresh_token, :user_id, :user)
|
|
14
|
+
|
|
15
|
+
# Reopens the generated record for checked initialization.
|
|
16
|
+
class Session
|
|
17
|
+
# @dynamic access_token, refresh_token, user_id, user, members, with, to_h
|
|
18
|
+
# @dynamic deconstruct, deconstruct_keys, self.[], self.members
|
|
14
19
|
def initialize(access_token:, refresh_token: nil, user_id: nil, user: nil)
|
|
15
|
-
|
|
20
|
+
user = immutable_user(user)
|
|
21
|
+
super
|
|
16
22
|
end
|
|
17
23
|
|
|
18
24
|
private
|
|
19
25
|
|
|
20
26
|
def immutable_user(value)
|
|
21
|
-
|
|
27
|
+
return if value.nil?
|
|
28
|
+
|
|
29
|
+
snapshot = SESSION_USER_CODER.load(SESSION_USER_CODER.dump(value))
|
|
30
|
+
raise TypeError, 'Session user snapshot must be a Hash' unless snapshot.is_a?(Hash)
|
|
31
|
+
|
|
32
|
+
snapshot
|
|
22
33
|
rescue JSON::GeneratorError, JSON::NestingError
|
|
23
34
|
raise TypeError, 'Session user snapshot must contain JSON values or timestamps', cause: nil
|
|
24
35
|
end
|
data/lib/volcano/transport.rb
CHANGED
|
@@ -20,21 +20,20 @@ module Volcano
|
|
|
20
20
|
|
|
21
21
|
NETWORK_ERRORS = [IOError, SystemCallError, SocketError, Timeout::Error].freeze
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def invoke
|
|
23
|
+
def self.invoke
|
|
26
24
|
yield
|
|
27
|
-
rescue
|
|
25
|
+
rescue IOError, SystemCallError, SocketError, Timeout::Error => e
|
|
28
26
|
raise Error::TransportError, e.message, cause: e
|
|
29
27
|
end
|
|
30
28
|
|
|
31
|
-
def body(response, expected_status)
|
|
29
|
+
def self.body(response, expected_status)
|
|
32
30
|
return response.body if response.status == expected_status
|
|
33
31
|
|
|
34
32
|
raise response_error(response)
|
|
35
33
|
end
|
|
36
34
|
|
|
37
|
-
def response_error(response)
|
|
35
|
+
def self.response_error(response)
|
|
36
|
+
# @type var payload: Hash[String, Object?]
|
|
38
37
|
payload = response.body.is_a?(Hash) ? response.body : {}
|
|
39
38
|
retry_after = integer_header(response.headers, 'Retry-After') if response.status == 429
|
|
40
39
|
error_type(response.status).new(
|
|
@@ -45,20 +44,63 @@ module Volcano
|
|
|
45
44
|
)
|
|
46
45
|
end
|
|
47
46
|
|
|
48
|
-
def error_message(payload)
|
|
49
|
-
|
|
47
|
+
def self.error_message(payload)
|
|
48
|
+
error = payload['error']
|
|
49
|
+
return error if error.is_a?(String)
|
|
50
|
+
|
|
51
|
+
message = payload['message']
|
|
52
|
+
message.is_a?(String) ? message : 'Volcano request failed'
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def self.json_value(value)
|
|
56
|
+
case value
|
|
57
|
+
when Array then value.map { |entry| json_value(entry) }
|
|
58
|
+
when Hash then json_object(value)
|
|
59
|
+
else json_scalar(value)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def self.json_object(value, message: 'Expected a JSON object')
|
|
64
|
+
raise TypeError, message unless value.is_a?(Hash)
|
|
65
|
+
|
|
66
|
+
value.to_h do |key, item|
|
|
67
|
+
raise TypeError, message unless key.is_a?(String)
|
|
68
|
+
|
|
69
|
+
[key, json_value(item)]
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def self.json_rows(value)
|
|
74
|
+
rows = json_object(value)['data']
|
|
75
|
+
raise TypeError, 'Expected database rows' unless rows.is_a?(Array)
|
|
76
|
+
|
|
77
|
+
rows.map { |row| json_object(row) }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def self.finite_float(value)
|
|
81
|
+
raise TypeError, 'Expected a finite JSON number' unless value.finite?
|
|
82
|
+
|
|
83
|
+
value
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def self.json_scalar(value)
|
|
87
|
+
case value
|
|
88
|
+
when NilClass, TrueClass, FalseClass, Integer, String then value
|
|
89
|
+
when Float then finite_float(value)
|
|
90
|
+
else raise TypeError, 'Expected a JSON response'
|
|
91
|
+
end
|
|
50
92
|
end
|
|
51
93
|
|
|
52
|
-
def error_type(status)
|
|
94
|
+
def self.error_type(status)
|
|
53
95
|
ERROR_TYPES.fetch(status) do
|
|
54
96
|
status.between?(500, 599) ? Error::ServerError : Error::VolcanoError
|
|
55
97
|
end
|
|
56
98
|
end
|
|
57
99
|
|
|
58
|
-
def integer_header(headers, name)
|
|
100
|
+
def self.integer_header(headers, name)
|
|
59
101
|
value = headers&.find { |key, _| key.casecmp?(name) }&.last
|
|
60
102
|
Integer(value, exception: false)
|
|
61
103
|
end
|
|
62
|
-
private_class_method :error_type, :integer_header, :response_error
|
|
104
|
+
private_class_method :error_type, :integer_header, :response_error, :finite_float, :json_scalar
|
|
63
105
|
end
|
|
64
106
|
end
|
data/lib/volcano/user.rb
CHANGED
|
@@ -10,15 +10,20 @@ module Volcano
|
|
|
10
10
|
User = Data.define(
|
|
11
11
|
:id, :project_id, :email, :email_confirmed, :user_metadata, :app_metadata,
|
|
12
12
|
:avatar_url, :status, :banned_until, :last_sign_in_at, :created_at, :updated_at
|
|
13
|
-
)
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
# Reopens the generated record for checked initialization.
|
|
16
|
+
class User
|
|
17
|
+
# @dynamic id, project_id, email, email_confirmed, user_metadata, app_metadata
|
|
18
|
+
# @dynamic avatar_url, status, banned_until, last_sign_in_at, created_at, updated_at
|
|
19
|
+
# @dynamic members, with, to_h, deconstruct, deconstruct_keys, self.[], self.members
|
|
14
20
|
def initialize(id:, email:, status:, **attributes)
|
|
15
21
|
validate_optional_attributes(attributes)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
)
|
|
22
|
+
id = freeze_value(id)
|
|
23
|
+
email = freeze_value(email)
|
|
24
|
+
status = freeze_value(status)
|
|
25
|
+
attributes = USER_OPTIONAL_ATTRIBUTES.to_h { |name| [name, freeze_value(attributes[name])] }
|
|
26
|
+
super
|
|
22
27
|
end
|
|
23
28
|
|
|
24
29
|
private
|
data/lib/volcano/version.rb
CHANGED
data/lib/volcano.rb
CHANGED
|
@@ -16,6 +16,7 @@ require_relative 'volcano/auth_state'
|
|
|
16
16
|
require_relative 'volcano/errors'
|
|
17
17
|
require_relative 'volcano/redaction'
|
|
18
18
|
require_relative 'volcano/transport'
|
|
19
|
+
require_relative 'volcano/immutable_request_value'
|
|
19
20
|
require_relative 'volcano/generated_transport'
|
|
20
21
|
require_relative 'volcano/auth_notification_dispatch'
|
|
21
22
|
require_relative 'volcano/auth_session_binding'
|
data/sig/auth_credentials.rbs
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
module Volcano
|
|
2
|
+
type auth_state_event = :initial_session | :signed_in | :signed_out | :token_refreshed
|
|
3
|
+
|
|
2
4
|
class Auth
|
|
3
5
|
def current_session: () -> Session?
|
|
4
6
|
def current_session=: (Session session) -> void
|
|
5
|
-
def on_auth_state_change: () { (
|
|
7
|
+
def on_auth_state_change: () { (auth_state_event, Session?) -> void } -> AuthSubscription
|
|
6
8
|
def sign_in: (email: String, password: String) -> Session
|
|
7
9
|
def sign_up: (email: String, password: String, ?metadata: session_user_input, ?sign_in_when_allowed: bool) -> SignUpResult
|
|
8
10
|
def sign_in_anonymously: (?metadata: session_user_input) -> Session
|
|
@@ -20,7 +20,7 @@ module Volcano
|
|
|
20
20
|
| (id: String, user_id: String, provider: String, expires_at: Time, is_active: bool, is_current: bool, ?user_agent: String?, ?ip_address: String?, ?last_ip_address: String?, ?last_activity_at: Time?, ?session_started_at: Time?, ?created_at: Time?, ?updated_at: Time?) -> AuthSession
|
|
21
21
|
def self.members: () -> [:id, :user_id, :provider, :expires_at, :is_active, :is_current, :user_agent, :ip_address, :last_ip_address, :last_activity_at, :session_started_at, :created_at, :updated_at]
|
|
22
22
|
|
|
23
|
-
def initialize: (
|
|
23
|
+
private def initialize: (**Object? attributes) -> void
|
|
24
24
|
def members: () -> [:id, :user_id, :provider, :expires_at, :is_active, :is_current, :user_agent, :ip_address, :last_ip_address, :last_activity_at, :session_started_at, :created_at, :updated_at]
|
|
25
25
|
def with: (?id: String, ?user_id: String, ?provider: String, ?expires_at: Time, ?is_active: bool, ?is_current: bool, ?user_agent: String?, ?ip_address: String?, ?last_ip_address: String?, ?last_activity_at: Time?, ?session_started_at: Time?, ?created_at: Time?, ?updated_at: Time?) -> AuthSession
|
|
26
26
|
def to_h: () -> { id: String, user_id: String, provider: String, expires_at: Time, is_active: bool, is_current: bool, user_agent: String?, ip_address: String?, last_ip_address: String?, last_activity_at: Time?, session_started_at: Time?, created_at: Time?, updated_at: Time? }
|
data/sig/client.rbs
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module Volcano
|
|
2
|
+
class Client
|
|
3
|
+
attr_reader auth: Auth
|
|
4
|
+
attr_reader functions: Functions
|
|
5
|
+
attr_reader durable: Durable
|
|
6
|
+
attr_reader logs: Logs
|
|
7
|
+
attr_reader storage: Storage
|
|
8
|
+
attr_reader locks: Locks
|
|
9
|
+
attr_reader realtime: Realtime
|
|
10
|
+
|
|
11
|
+
def initialize: (anon_key: String, ?api_url: String, ?service_key: String?, ?timeout: (Integer | Float), ?access_token: String?, ?refresh_token: String?) -> void
|
|
12
|
+
def current_session: () -> Session?
|
|
13
|
+
def database: (String name) -> Database
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
module Volcano
|
|
2
|
+
type sdk_json = nil | bool | Integer | Float | String | Time | Array[sdk_json] | Hash[String | Symbol, sdk_json]
|
|
3
|
+
type sdk_payload = Hash[String | Symbol, sdk_json]
|
|
4
|
+
type database_rows = Array[Hash[String, json_value]]
|
|
5
|
+
|
|
6
|
+
class Database
|
|
7
|
+
def from: (String table) -> QueryBuilder
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
class QueryBuilder
|
|
11
|
+
def select: (*String columns) -> QueryBuilder
|
|
12
|
+
def insert: (sdk_payload values) -> InsertBuilder
|
|
13
|
+
def update: (sdk_payload values) -> UpdateBuilder
|
|
14
|
+
def delete: () -> DeleteBuilder
|
|
15
|
+
def order: (String column, ?ascending: bool) -> QueryBuilder
|
|
16
|
+
def limit: (Integer count) -> QueryBuilder
|
|
17
|
+
def offset: (Integer count) -> QueryBuilder
|
|
18
|
+
def execute: () -> database_rows
|
|
19
|
+
|
|
20
|
+
def eq: (String column, sdk_json value) -> QueryBuilder
|
|
21
|
+
def neq: (String column, sdk_json value) -> QueryBuilder
|
|
22
|
+
def gt: (String column, sdk_json value) -> QueryBuilder
|
|
23
|
+
def gte: (String column, sdk_json value) -> QueryBuilder
|
|
24
|
+
def lt: (String column, sdk_json value) -> QueryBuilder
|
|
25
|
+
def lte: (String column, sdk_json value) -> QueryBuilder
|
|
26
|
+
def like: (String column, String pattern) -> QueryBuilder
|
|
27
|
+
def ilike: (String column, String pattern) -> QueryBuilder
|
|
28
|
+
def is: (String column, sdk_json value) -> QueryBuilder
|
|
29
|
+
def in: (String column, Array[sdk_json] values) -> QueryBuilder
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class InsertBuilder
|
|
33
|
+
def execute: () -> database_rows
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class UpdateBuilder
|
|
37
|
+
def execute: () -> database_rows
|
|
38
|
+
|
|
39
|
+
def eq: (String column, sdk_json value) -> UpdateBuilder
|
|
40
|
+
def neq: (String column, sdk_json value) -> UpdateBuilder
|
|
41
|
+
def gt: (String column, sdk_json value) -> UpdateBuilder
|
|
42
|
+
def gte: (String column, sdk_json value) -> UpdateBuilder
|
|
43
|
+
def lt: (String column, sdk_json value) -> UpdateBuilder
|
|
44
|
+
def lte: (String column, sdk_json value) -> UpdateBuilder
|
|
45
|
+
def like: (String column, String pattern) -> UpdateBuilder
|
|
46
|
+
def ilike: (String column, String pattern) -> UpdateBuilder
|
|
47
|
+
def is: (String column, sdk_json value) -> UpdateBuilder
|
|
48
|
+
def in: (String column, Array[sdk_json] values) -> UpdateBuilder
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
class DeleteBuilder
|
|
52
|
+
def execute: () -> database_rows
|
|
53
|
+
|
|
54
|
+
def eq: (String column, sdk_json value) -> DeleteBuilder
|
|
55
|
+
def neq: (String column, sdk_json value) -> DeleteBuilder
|
|
56
|
+
def gt: (String column, sdk_json value) -> DeleteBuilder
|
|
57
|
+
def gte: (String column, sdk_json value) -> DeleteBuilder
|
|
58
|
+
def lt: (String column, sdk_json value) -> DeleteBuilder
|
|
59
|
+
def lte: (String column, sdk_json value) -> DeleteBuilder
|
|
60
|
+
def like: (String column, String pattern) -> DeleteBuilder
|
|
61
|
+
def ilike: (String column, String pattern) -> DeleteBuilder
|
|
62
|
+
def is: (String column, sdk_json value) -> DeleteBuilder
|
|
63
|
+
def in: (String column, Array[sdk_json] values) -> DeleteBuilder
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
class Functions
|
|
67
|
+
def invoke: (String name, ?sdk_payload payload) -> FunctionResponse
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
class Durable
|
|
71
|
+
def start: (String function_name, ?sdk_payload payload, ?execution_name: String?) -> DurableExecution
|
|
72
|
+
def get: (String project_id, String function_name, String execution_id) -> DurableExecution
|
|
73
|
+
def list: (String project_id, String function_name, ?status: String?, ?page: Integer?, ?limit: Integer?) -> DurableExecutionPage
|
|
74
|
+
def stop: (String project_id, String function_name, String execution_id) -> DurableExecution
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
class Logs
|
|
78
|
+
def search: (String project_id, sdk_payload request) -> LogSearchResponse
|
|
79
|
+
def activity: (String project_id, sdk_payload request) -> LogActivityResponse
|
|
80
|
+
end
|
|
81
|
+
end
|
data/sig/durable_records.rbs
CHANGED
|
@@ -36,7 +36,7 @@ module Volcano
|
|
|
36
36
|
| (id: String, function_id: String, name: String, status: String, region: String, created_at: Time, ?result: json_value, ?result_expired: bool?, ?error: DurableExecutionError?, ?completed_at: Time?) -> DurableExecution
|
|
37
37
|
def self.members: () -> [:id, :function_id, :name, :status, :region, :created_at, :result, :result_expired, :error, :completed_at]
|
|
38
38
|
|
|
39
|
-
def initialize: (
|
|
39
|
+
private def initialize: (**(json_value | Time | DurableExecutionError) attributes) -> void
|
|
40
40
|
def members: () -> [:id, :function_id, :name, :status, :region, :created_at, :result, :result_expired, :error, :completed_at]
|
|
41
41
|
def with: (?id: String, ?function_id: String, ?name: String, ?status: String, ?region: String, ?created_at: Time, ?result: json_value, ?result_expired: bool?, ?error: DurableExecutionError?, ?completed_at: Time?) -> DurableExecution
|
|
42
42
|
def to_h: () -> { id: String, function_id: String, name: String, status: String, region: String, created_at: Time, result: json_value, result_expired: bool?, error: DurableExecutionError?, completed_at: Time? }
|
data/sig/locks.rbs
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
module Volcano
|
|
2
|
+
class Locks
|
|
3
|
+
def get: (String key, ?request_id: String?) -> LockState
|
|
4
|
+
def acquire: (String key, ttl: Integer, ?token: String?, ?request_id: String?) -> LockLease
|
|
5
|
+
def renew: (String key, LockLease lease, ttl: Integer, ?request_id: String?) -> LockLease
|
|
6
|
+
def release: (String key, LockLease lease, ?request_id: String?) -> nil
|
|
7
|
+
def force_release: (String key, ?request_id: String?) -> nil
|
|
8
|
+
def with_lock: [T] (String key, ttl: Integer, ?token: String?, ?request_id: String?) { (LockGuard) -> T } -> T
|
|
9
|
+
end
|
|
10
|
+
end
|
data/sig/realtime.rbs
CHANGED
|
@@ -1,11 +1,4 @@
|
|
|
1
1
|
module Volcano
|
|
2
|
-
class Client
|
|
3
|
-
attr_reader auth: Auth
|
|
4
|
-
attr_reader realtime: Realtime
|
|
5
|
-
|
|
6
|
-
def initialize: (anon_key: String, ?api_url: String, ?service_key: String?, ?timeout: Integer, ?access_token: String?, ?refresh_token: String?) -> void
|
|
7
|
-
end
|
|
8
|
-
|
|
9
2
|
class Realtime
|
|
10
3
|
def channel: (String name, ?type: String | Symbol, ?auto_fetch: bool, ?fetch_batch_window_ms: Integer, ?fetch_max_batch_size: Integer) -> Channel
|
|
11
4
|
def connected?: () -> bool
|
|
@@ -30,8 +23,14 @@ module Volcano
|
|
|
30
23
|
def unsubscribe: () -> nil
|
|
31
24
|
def send: (event: String | Symbol, **json_value payload) -> nil
|
|
32
25
|
|
|
33
|
-
def on: (
|
|
34
|
-
| (
|
|
26
|
+
def on: ("message" event, ^(Hash[String, json_value]) -> void callback) -> self
|
|
27
|
+
| ("message" event) { (Hash[String, json_value]) -> void } -> self
|
|
28
|
+
| ("join" | "leave" event, ^(PresenceInfo) -> void callback) -> self
|
|
29
|
+
| ("join" | "leave" event) { (PresenceInfo) -> void } -> self
|
|
30
|
+
| ("presence_sync" event, ^(Hash[String, PresenceInfo]) -> void callback) -> self
|
|
31
|
+
| ("presence_sync" event) { (Hash[String, PresenceInfo]) -> void } -> self
|
|
32
|
+
| ("INSERT" | "UPDATE" | "DELETE" | "*" event, ^(PostgresChange) -> void callback) -> self
|
|
33
|
+
| ("INSERT" | "UPDATE" | "DELETE" | "*" event) { (PostgresChange) -> void } -> self
|
|
35
34
|
def on_presence_sync: (^(Hash[String, PresenceInfo]) -> void callback) -> self
|
|
36
35
|
| () { (Hash[String, PresenceInfo]) -> void } -> self
|
|
37
36
|
def on_postgres_changes: (String | Symbol event, schema: String, table: String, callback: ^(PostgresChange) -> void) -> self
|
data/sig/user.rbs
CHANGED
|
@@ -19,7 +19,7 @@ module Volcano
|
|
|
19
19
|
| (id: String, email: String, status: String, ?project_id: String?, ?email_confirmed: bool?, ?user_metadata: Hash[String, json_value]?, ?app_metadata: Hash[String, json_value]?, ?avatar_url: String?, ?banned_until: Time?, ?last_sign_in_at: Time?, ?created_at: Time?, ?updated_at: Time?) -> User
|
|
20
20
|
def self.members: () -> [:id, :project_id, :email, :email_confirmed, :user_metadata, :app_metadata, :avatar_url, :status, :banned_until, :last_sign_in_at, :created_at, :updated_at]
|
|
21
21
|
|
|
22
|
-
def initialize: (id: String, email: String, status: String, ?
|
|
22
|
+
private def initialize: (id: String, email: String, status: String, **Object? attributes) -> void
|
|
23
23
|
def members: () -> [:id, :project_id, :email, :email_confirmed, :user_metadata, :app_metadata, :avatar_url, :status, :banned_until, :last_sign_in_at, :created_at, :updated_at]
|
|
24
24
|
def with: (?id: String, ?project_id: String?, ?email: String, ?email_confirmed: bool?, ?user_metadata: Hash[String, json_value]?, ?app_metadata: Hash[String, json_value]?, ?avatar_url: String?, ?status: String, ?banned_until: Time?, ?last_sign_in_at: Time?, ?created_at: Time?, ?updated_at: Time?) -> User
|
|
25
25
|
def to_h: () -> { id: String, project_id: String?, email: String, email_confirmed: bool?, user_metadata: Hash[String, json_value]?, app_metadata: Hash[String, json_value]?, avatar_url: String?, status: String, banned_until: Time?, last_sign_in_at: Time?, created_at: Time?, updated_at: Time? }
|
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.32.
|
|
4
|
+
version: 0.32.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kong
|
|
@@ -560,6 +560,7 @@ files:
|
|
|
560
560
|
- lib/volcano/generated_transport_support.rb
|
|
561
561
|
- lib/volcano/generated_transport_upload_session_api.rb
|
|
562
562
|
- lib/volcano/generated_transport_upload_sessions.rb
|
|
563
|
+
- lib/volcano/immutable_request_value.rb
|
|
563
564
|
- lib/volcano/lock_auto_renewal.rb
|
|
564
565
|
- lib/volcano/lock_guard.rb
|
|
565
566
|
- lib/volcano/lock_lease_clock.rb
|
|
@@ -620,13 +621,16 @@ files:
|
|
|
620
621
|
- sig/auth_session_records.rbs
|
|
621
622
|
- sig/auth_sessions.rbs
|
|
622
623
|
- sig/auth_subscription.rbs
|
|
624
|
+
- sig/client.rbs
|
|
623
625
|
- sig/connection_string.rbs
|
|
626
|
+
- sig/core_facades.rbs
|
|
624
627
|
- sig/durable_records.rbs
|
|
625
628
|
- sig/email_change_result.rbs
|
|
626
629
|
- sig/errors.rbs
|
|
627
630
|
- sig/function_response.rbs
|
|
628
631
|
- sig/lock_guard.rbs
|
|
629
632
|
- sig/lock_records.rbs
|
|
633
|
+
- sig/locks.rbs
|
|
630
634
|
- sig/log_responses.rbs
|
|
631
635
|
- sig/oauth_records.rbs
|
|
632
636
|
- sig/realtime.rbs
|