functions_framework 0.6.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +35 -0
- data/README.md +5 -5
- data/bin/functions-framework +3 -0
- data/docs/deploying-functions.md +7 -11
- data/docs/overview.md +4 -4
- data/docs/testing-functions.md +50 -0
- data/docs/writing-functions.md +245 -68
- data/lib/functions_framework.rb +11 -8
- data/lib/functions_framework/cli.rb +1 -1
- data/lib/functions_framework/function.rb +190 -48
- data/lib/functions_framework/legacy_event_converter.rb +83 -24
- data/lib/functions_framework/registry.rb +10 -20
- data/lib/functions_framework/server.rb +34 -22
- data/lib/functions_framework/testing.rb +117 -22
- data/lib/functions_framework/version.rb +1 -1
- metadata +28 -16
data/lib/functions_framework.rb
CHANGED
@@ -175,10 +175,8 @@ module FunctionsFramework
|
|
175
175
|
# run only when preparing to run functions. They are not run, for example,
|
176
176
|
# if an app is loaded to verify its integrity during deployment.
|
177
177
|
#
|
178
|
-
# Startup tasks are passed
|
179
|
-
#
|
180
|
-
# {FunctionsFramework::Server::Config} specifying the (frozen) server
|
181
|
-
# configuration. Tasks have no return value.
|
178
|
+
# Startup tasks are passed the {FunctionsFramework::Function} identifying
|
179
|
+
# the function to execute, and have no return value.
|
182
180
|
#
|
183
181
|
# @param block [Proc] The startup task
|
184
182
|
# @return [self]
|
@@ -189,8 +187,9 @@ module FunctionsFramework
|
|
189
187
|
end
|
190
188
|
|
191
189
|
##
|
192
|
-
#
|
193
|
-
#
|
190
|
+
# Run startup tasks, then start the functions framework server in the
|
191
|
+
# background. The startup tasks and target function will be looked up in
|
192
|
+
# the global registry.
|
194
193
|
#
|
195
194
|
# @param target [FunctionsFramework::Function,String] The function to run,
|
196
195
|
# or the name of the function to look up in the global registry.
|
@@ -206,8 +205,12 @@ module FunctionsFramework
|
|
206
205
|
function = global_registry[target]
|
207
206
|
raise ::ArgumentError, "Undefined function: #{target.inspect}" if function.nil?
|
208
207
|
end
|
209
|
-
|
210
|
-
|
208
|
+
globals = function.populate_globals
|
209
|
+
server = Server.new function, globals, &block
|
210
|
+
global_registry.startup_tasks.each do |task|
|
211
|
+
task.call function, globals: globals, logger: server.config.logger
|
212
|
+
end
|
213
|
+
globals.freeze
|
211
214
|
server.respond_to_signals
|
212
215
|
server.start
|
213
216
|
end
|
@@ -74,7 +74,7 @@ module FunctionsFramework
|
|
74
74
|
# @param argv [Array<String>]
|
75
75
|
# @return [self]
|
76
76
|
#
|
77
|
-
def parse_args argv # rubocop:disable Metrics/MethodLength
|
77
|
+
def parse_args argv # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
|
78
78
|
@option_parser = ::OptionParser.new do |op| # rubocop:disable Metrics/BlockLength
|
79
79
|
op.on "-t", "--target TARGET",
|
80
80
|
"Set the name of the function to execute (defaults to #{DEFAULT_TARGET})" do |val|
|
@@ -18,44 +18,91 @@ module FunctionsFramework
|
|
18
18
|
#
|
19
19
|
# A function has a name, a type, and an implementation.
|
20
20
|
#
|
21
|
+
# ## Function implementations
|
22
|
+
#
|
21
23
|
# The implementation in general is an object that responds to the `call`
|
22
|
-
# method.
|
23
|
-
#
|
24
|
-
#
|
25
|
-
#
|
26
|
-
#
|
27
|
-
#
|
28
|
-
#
|
24
|
+
# method.
|
25
|
+
#
|
26
|
+
# * For a function of type `:http`, the `call` method takes a single
|
27
|
+
# `Rack::Request` argument and returns one of various HTTP response
|
28
|
+
# types. See {FunctionsFramework::Registry.add_http}.
|
29
|
+
# * For a function of type `:cloud_event`, the `call` method takes a single
|
30
|
+
# [CloudEvent](https://cloudevents.github.io/sdk-ruby/latest/CloudEvents/Event)
|
31
|
+
# argument, and does not return a value. See
|
32
|
+
# {FunctionsFramework::Registry.add_cloud_event}.
|
33
|
+
# * For a function of type `:startup_task`, the `call` method takes a
|
34
|
+
# single {FunctionsFramework::Function} argument, and does not return a
|
35
|
+
# value. See {FunctionsFramework::Registry.add_startup_task}.
|
29
36
|
#
|
30
|
-
#
|
31
|
-
# every function execution. Note that this means it may be called multiple
|
32
|
-
# times concurrently in separate threads.
|
37
|
+
# The implementation can be specified in one of three ways:
|
33
38
|
#
|
34
|
-
#
|
35
|
-
#
|
36
|
-
#
|
37
|
-
#
|
38
|
-
#
|
39
|
-
#
|
39
|
+
# * A callable object can be passed in the `callable` keyword argument. The
|
40
|
+
# object's `call` method will be invoked for every function execution.
|
41
|
+
# Note that this means it may be called multiple times concurrently in
|
42
|
+
# separate threads.
|
43
|
+
# * A callable _class_ can be passed in the `callable` keyword argument.
|
44
|
+
# This class should subclass {FunctionsFramework::Function::Callable} and
|
45
|
+
# define the `call` method. A separate instance of this class will be
|
46
|
+
# created for each function invocation.
|
47
|
+
# * A block can be provided. It will be used to define the `call` method in
|
48
|
+
# an anonymous subclass of {FunctionsFramework::Function::Callable}.
|
49
|
+
# Thus, providing a block is really just syntactic sugar for providing a
|
50
|
+
# class. (This means, for example, that the `return` keyword will work
|
51
|
+
# as expected within the block because it is treated as a method.)
|
40
52
|
#
|
41
|
-
#
|
42
|
-
#
|
43
|
-
#
|
44
|
-
#
|
45
|
-
#
|
46
|
-
#
|
53
|
+
# When the implementation is provided as a callable class or block, it is
|
54
|
+
# executed in the context of a {FunctionsFramework::Function::Callable}
|
55
|
+
# object. This object provides a convenience accessor for the Logger, and
|
56
|
+
# access to _globals_, which are data defined by the application startup
|
57
|
+
# process and available to each function invocation. Typically, globals are
|
58
|
+
# used for shared global resources such as service connections and clients.
|
47
59
|
#
|
48
60
|
class Function
|
61
|
+
##
|
62
|
+
# Create a new HTTP function definition.
|
63
|
+
#
|
64
|
+
# @param name [String] The function name
|
65
|
+
# @param callable [Class,#call] A callable object or class.
|
66
|
+
# @param block [Proc] The function code as a block.
|
67
|
+
# @return [FunctionsFramework::Function]
|
68
|
+
#
|
69
|
+
def self.http name, callable: nil, &block
|
70
|
+
new name, :http, callable: callable, &block
|
71
|
+
end
|
72
|
+
|
73
|
+
##
|
74
|
+
# Create a new CloudEvents function definition.
|
75
|
+
#
|
76
|
+
# @param name [String] The function name
|
77
|
+
# @param callable [Class,#call] A callable object or class.
|
78
|
+
# @param block [Proc] The function code as a block.
|
79
|
+
# @return [FunctionsFramework::Function]
|
80
|
+
#
|
81
|
+
def self.cloud_event name, callable: nil, &block
|
82
|
+
new name, :cloud_event, callable: callable, &block
|
83
|
+
end
|
84
|
+
|
85
|
+
##
|
86
|
+
# Create a new startup task function definition.
|
87
|
+
#
|
88
|
+
# @param callable [Class,#call] A callable object or class.
|
89
|
+
# @param block [Proc] The function code as a block.
|
90
|
+
# @return [FunctionsFramework::Function]
|
91
|
+
#
|
92
|
+
def self.startup_task callable: nil, &block
|
93
|
+
new nil, :startup_task, callable: callable, &block
|
94
|
+
end
|
95
|
+
|
49
96
|
##
|
50
97
|
# Create a new function definition.
|
51
98
|
#
|
52
99
|
# @param name [String] The function name
|
53
|
-
# @param type [Symbol] The type of function. Valid types are `:http
|
54
|
-
# `:cloud_event`.
|
100
|
+
# @param type [Symbol] The type of function. Valid types are `:http`,
|
101
|
+
# `:cloud_event`, and `:startup_task`.
|
55
102
|
# @param callable [Class,#call] A callable object or class.
|
56
103
|
# @param block [Proc] The function code as a block.
|
57
104
|
#
|
58
|
-
def initialize name, type, callable
|
105
|
+
def initialize name, type, callable: nil, &block
|
59
106
|
@name = name
|
60
107
|
@type = type
|
61
108
|
@callable = @callable_class = nil
|
@@ -64,7 +111,7 @@ module FunctionsFramework
|
|
64
111
|
elsif callable.is_a? ::Class
|
65
112
|
@callable_class = callable
|
66
113
|
elsif block_given?
|
67
|
-
@callable_class = ::Class.new
|
114
|
+
@callable_class = ::Class.new Callable do
|
68
115
|
define_method :call, &block
|
69
116
|
end
|
70
117
|
else
|
@@ -83,18 +130,60 @@ module FunctionsFramework
|
|
83
130
|
attr_reader :type
|
84
131
|
|
85
132
|
##
|
86
|
-
#
|
87
|
-
#
|
88
|
-
#
|
89
|
-
#
|
90
|
-
#
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
133
|
+
# Populate the given globals hash with this function's info.
|
134
|
+
#
|
135
|
+
# @param globals [Hash] Initial globals hash (optional).
|
136
|
+
# @return [Hash] A new globals hash with this function's info included.
|
137
|
+
#
|
138
|
+
def populate_globals globals = nil
|
139
|
+
result = { function_name: name, function_type: type }
|
140
|
+
result.merge! globals if globals
|
141
|
+
result
|
142
|
+
end
|
143
|
+
|
144
|
+
##
|
145
|
+
# Call the function given a set of arguments. Set the given logger and/or
|
146
|
+
# globals in the context if the callable supports it.
|
147
|
+
#
|
148
|
+
# If the given arguments exceeds what the function will accept, the args
|
149
|
+
# are silently truncated. However, if the function requires more arguments
|
150
|
+
# than are provided, an ArgumentError is raised.
|
151
|
+
#
|
152
|
+
# @param args [Array] Argument to pass to the function.
|
153
|
+
# @param logger [Logger] Logger for use by function executions.
|
154
|
+
# @param globals [Hash] Globals for the function execution context
|
155
|
+
# @return [Object] The function return value.
|
156
|
+
#
|
157
|
+
def call *args, globals: nil, logger: nil
|
158
|
+
callable = @callable || @callable_class.new(globals: globals, logger: logger)
|
159
|
+
params = callable.method(:call).parameters.map(&:first)
|
160
|
+
unless params.include? :rest
|
161
|
+
max_params = params.count(:req) + params.count(:opt)
|
162
|
+
args = args.take max_params
|
163
|
+
end
|
164
|
+
callable.call(*args)
|
165
|
+
end
|
166
|
+
|
167
|
+
##
|
168
|
+
# A lazy evaluator for a global
|
169
|
+
# @private
|
170
|
+
#
|
171
|
+
class LazyGlobal
|
172
|
+
def initialize block
|
173
|
+
@block = block
|
174
|
+
@value = nil
|
175
|
+
@mutex = ::Mutex.new
|
176
|
+
end
|
177
|
+
|
178
|
+
def value
|
179
|
+
@mutex.synchronize do
|
180
|
+
if @block
|
181
|
+
@value = @block.call
|
182
|
+
@block = nil
|
183
|
+
end
|
184
|
+
@value
|
185
|
+
end
|
186
|
+
end
|
98
187
|
end
|
99
188
|
|
100
189
|
##
|
@@ -102,29 +191,82 @@ module FunctionsFramework
|
|
102
191
|
#
|
103
192
|
# An object of this class is `self` while a function block is running.
|
104
193
|
#
|
105
|
-
class
|
194
|
+
class Callable
|
106
195
|
##
|
107
196
|
# Create a callable object with the given context.
|
108
197
|
#
|
109
|
-
# @param
|
110
|
-
#
|
111
|
-
# implementations should be prepared to accept any abritrary keys.
|
198
|
+
# @param globals [Hash] A set of globals available to the call.
|
199
|
+
# @param logger [Logger] A logger for use by the function call.
|
112
200
|
#
|
113
|
-
def initialize
|
114
|
-
@
|
201
|
+
def initialize globals: nil, logger: nil
|
202
|
+
@__globals = globals || {}
|
203
|
+
@__logger = logger || FunctionsFramework.logger
|
115
204
|
end
|
116
205
|
|
117
206
|
##
|
118
|
-
#
|
207
|
+
# Get the given named global.
|
208
|
+
#
|
209
|
+
# For most function calls, the following globals will be defined:
|
119
210
|
#
|
120
|
-
# * **:logger** (`Logger`) A logger for use by this function call.
|
121
211
|
# * **:function_name** (`String`) The name of the running function.
|
122
212
|
# * **:function_type** (`Symbol`) The type of the running function,
|
123
213
|
# either `:http` or `:cloud_event`.
|
124
214
|
#
|
125
|
-
#
|
215
|
+
# You can also set additional globals from a startup task.
|
216
|
+
#
|
217
|
+
# @param key [Symbol,String] The name of the global to get.
|
218
|
+
# @return [Object]
|
219
|
+
#
|
220
|
+
def global key
|
221
|
+
value = @__globals[key]
|
222
|
+
value = value.value if LazyGlobal === value
|
223
|
+
value
|
224
|
+
end
|
225
|
+
|
226
|
+
##
|
227
|
+
# Set a global. This can be called from startup tasks, but the globals
|
228
|
+
# are frozen when the server starts, so this call will raise an exception
|
229
|
+
# if called from a normal function.
|
230
|
+
#
|
231
|
+
# You can set a global to a final value, or you can provide a block that
|
232
|
+
# lazily computes the global the first time it is requested.
|
233
|
+
#
|
234
|
+
# @overload set_global(key, value)
|
235
|
+
# Set the given global to the given value. For example:
|
236
|
+
#
|
237
|
+
# set_global(:project_id, "my-project-id")
|
238
|
+
#
|
239
|
+
# @param key [Symbol,String]
|
240
|
+
# @param value [Object]
|
241
|
+
# @return [self]
|
242
|
+
#
|
243
|
+
# @overload set_global(key, &block)
|
244
|
+
# Call the given block to compute the global's value only when the
|
245
|
+
# value is actually requested. This block will be called at most once,
|
246
|
+
# and its result reused for subsequent calls. For example:
|
247
|
+
#
|
248
|
+
# set_global(:connection_pool) do
|
249
|
+
# ExpensiveConnectionPool.new
|
250
|
+
# end
|
126
251
|
#
|
127
|
-
|
252
|
+
# @param key [Symbol,String]
|
253
|
+
# @param block [Proc] A block that lazily computes a value
|
254
|
+
# @yieldreturn [Object] The value
|
255
|
+
# @return [self]
|
256
|
+
#
|
257
|
+
def set_global key, value = nil, &block
|
258
|
+
@__globals[key] = block ? LazyGlobal.new(block) : value
|
259
|
+
self
|
260
|
+
end
|
261
|
+
|
262
|
+
##
|
263
|
+
# A logger for use by this call.
|
264
|
+
#
|
265
|
+
# @return [Logger]
|
266
|
+
#
|
267
|
+
def logger
|
268
|
+
@__logger
|
269
|
+
end
|
128
270
|
end
|
129
271
|
end
|
130
272
|
end
|
@@ -27,20 +27,21 @@ module FunctionsFramework
|
|
27
27
|
# @return [nil] if the event format was not recognized.
|
28
28
|
#
|
29
29
|
def decode_rack_env env
|
30
|
-
content_type = ::CloudEvents::ContentType.new env["CONTENT_TYPE"]
|
30
|
+
content_type = ::CloudEvents::ContentType.new env["CONTENT_TYPE"], default_charset: "utf-8"
|
31
31
|
return nil unless content_type.media_type == "application" && content_type.subtype_base == "json"
|
32
32
|
input = read_input_json env["rack.input"], content_type.charset
|
33
33
|
return nil unless input
|
34
|
+
input = convert_raw_pubsub_event input, env if raw_pubsub_payload? input
|
34
35
|
context = normalized_context input
|
35
36
|
return nil unless context
|
36
|
-
construct_cloud_event context, input["data"]
|
37
|
+
construct_cloud_event context, input["data"]
|
37
38
|
end
|
38
39
|
|
39
40
|
private
|
40
41
|
|
41
42
|
def read_input_json input, charset
|
42
43
|
input = input.read if input.respond_to? :read
|
43
|
-
input
|
44
|
+
input.force_encoding charset if charset
|
44
45
|
content = ::JSON.parse input
|
45
46
|
content = nil unless content.is_a? ::Hash
|
46
47
|
content
|
@@ -48,17 +49,51 @@ module FunctionsFramework
|
|
48
49
|
nil
|
49
50
|
end
|
50
51
|
|
52
|
+
def raw_pubsub_payload? input
|
53
|
+
return false if input.include?("context") || !input.include?("subscription")
|
54
|
+
message = input["message"]
|
55
|
+
message.is_a?(::Hash) && message.include?("data") && message.include?("messageId")
|
56
|
+
end
|
57
|
+
|
58
|
+
def convert_raw_pubsub_event input, env
|
59
|
+
message = input["message"]
|
60
|
+
path = "#{env['SCRIPT_NAME']}#{env['PATH_INFO']}"
|
61
|
+
path_match = %r{projects/[^/?]+/topics/[^/?]+}.match path
|
62
|
+
topic = path_match ? path_match[0] : "UNKNOWN_PUBSUB_TOPIC"
|
63
|
+
timestamp = message["publishTime"] || ::Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S.%6NZ")
|
64
|
+
{
|
65
|
+
"context" => {
|
66
|
+
"eventId" => message["messageId"],
|
67
|
+
"timestamp" => timestamp,
|
68
|
+
"eventType" => "google.pubsub.topic.publish",
|
69
|
+
"resource" => {
|
70
|
+
"service" => "pubsub.googleapis.com",
|
71
|
+
"type" => "type.googleapis.com/google.pubsub.v1.PubsubMessage",
|
72
|
+
"name" => topic
|
73
|
+
}
|
74
|
+
},
|
75
|
+
"data" => {
|
76
|
+
"@type" => "type.googleapis.com/google.pubsub.v1.PubsubMessage",
|
77
|
+
"data" => message["data"],
|
78
|
+
"attributes" => message["attributes"]
|
79
|
+
}
|
80
|
+
}
|
81
|
+
end
|
82
|
+
|
51
83
|
def normalized_context input
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
service, resource = analyze_resource raw_context&.[]("resource") || input["resource"]
|
84
|
+
id = normalized_context_field input, "eventId"
|
85
|
+
timestamp = normalized_context_field input, "timestamp"
|
86
|
+
type = normalized_context_field input, "eventType"
|
87
|
+
service, resource = analyze_resource normalized_context_field input, "resource"
|
57
88
|
service ||= service_from_type type
|
58
89
|
return nil unless id && timestamp && type && service && resource
|
59
90
|
{ id: id, timestamp: timestamp, type: type, service: service, resource: resource }
|
60
91
|
end
|
61
92
|
|
93
|
+
def normalized_context_field input, field
|
94
|
+
input["context"]&.[](field) || input[field]
|
95
|
+
end
|
96
|
+
|
62
97
|
def analyze_resource raw_resource
|
63
98
|
service = resource = nil
|
64
99
|
case raw_resource
|
@@ -78,37 +113,47 @@ module FunctionsFramework
|
|
78
113
|
nil
|
79
114
|
end
|
80
115
|
|
81
|
-
def construct_cloud_event context, data
|
116
|
+
def construct_cloud_event context, data
|
82
117
|
source, subject = convert_source context[:service], context[:resource]
|
83
118
|
type = LEGACY_TYPE_TO_CE_TYPE[context[:type]]
|
84
119
|
return nil unless type && source
|
85
|
-
ce_data = convert_data context[:service], data
|
86
|
-
content_type = "application/json
|
120
|
+
ce_data, data_subject = convert_data context[:service], data
|
121
|
+
content_type = "application/json"
|
87
122
|
::CloudEvents::Event.new id: context[:id],
|
88
123
|
source: source,
|
89
124
|
type: type,
|
90
125
|
spec_version: "1.0",
|
91
126
|
data_content_type: content_type,
|
92
127
|
data: ce_data,
|
93
|
-
subject: subject,
|
128
|
+
subject: subject || data_subject,
|
94
129
|
time: context[:timestamp]
|
95
130
|
end
|
96
131
|
|
97
132
|
def convert_source service, resource
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
["//#{service}/#{resource}", nil]
|
104
|
-
end
|
133
|
+
return ["//#{service}/#{resource}", nil] unless CE_SERVICE_TO_RESOURCE_RE.key? service
|
134
|
+
|
135
|
+
match = CE_SERVICE_TO_RESOURCE_RE[service].match resource
|
136
|
+
return [nil, nil] unless match
|
137
|
+
["//#{service}/#{match[1]}", match[2]]
|
105
138
|
end
|
106
139
|
|
107
140
|
def convert_data service, data
|
108
|
-
|
109
|
-
|
141
|
+
case service
|
142
|
+
when "pubsub.googleapis.com"
|
143
|
+
[{ "message" => data }, nil]
|
144
|
+
when "firebaseauth.googleapis.com"
|
145
|
+
if data.key? "metadata"
|
146
|
+
FIREBASE_AUTH_METADATA_LEGACY_TO_CE.each do |old_key, new_key|
|
147
|
+
if data["metadata"].key? old_key
|
148
|
+
data["metadata"][new_key] = data["metadata"][old_key]
|
149
|
+
data["metadata"].delete old_key
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
subject = "users/#{data['uid']}" if data.key? "uid"
|
154
|
+
[data, subject]
|
110
155
|
else
|
111
|
-
data
|
156
|
+
[data, nil]
|
112
157
|
end
|
113
158
|
end
|
114
159
|
|
@@ -116,8 +161,9 @@ module FunctionsFramework
|
|
116
161
|
%r{^providers/cloud\.firestore/} => "firestore.googleapis.com",
|
117
162
|
%r{^providers/cloud\.pubsub/} => "pubsub.googleapis.com",
|
118
163
|
%r{^providers/cloud\.storage/} => "storage.googleapis.com",
|
119
|
-
%r{^providers/firebase\.auth/} => "
|
120
|
-
%r{^providers/google\.firebase}
|
164
|
+
%r{^providers/firebase\.auth/} => "firebaseauth.googleapis.com",
|
165
|
+
%r{^providers/google\.firebase\.analytics/} => "firebase.googleapis.com",
|
166
|
+
%r{^providers/google\.firebase\.database/} => "firebasedatabase.googleapis.com"
|
121
167
|
}.freeze
|
122
168
|
|
123
169
|
LEGACY_TYPE_TO_CE_TYPE = {
|
@@ -140,5 +186,18 @@ module FunctionsFramework
|
|
140
186
|
"providers/google.firebase.database/eventTypes/ref.delete" => "google.firebase.database.document.v1.deleted",
|
141
187
|
"providers/cloud.storage/eventTypes/object.change" => "google.cloud.storage.object.v1.finalized"
|
142
188
|
}.freeze
|
189
|
+
|
190
|
+
CE_SERVICE_TO_RESOURCE_RE = {
|
191
|
+
"firebase.googleapis.com" => %r{^(projects/[^/]+)/(events/[^/]+)$},
|
192
|
+
"firebasedatabase.googleapis.com" => %r{^(projects/_/instances/[^/]+)/(refs/.+)$},
|
193
|
+
"firestore.googleapis.com" => %r{^(projects/[^/]+/databases/\(default\))/(documents/.+)$},
|
194
|
+
"storage.googleapis.com" => %r{^(projects/[^/]+/buckets/[^/]+)/([^#]+)(?:#.*)?$}
|
195
|
+
}.freeze
|
196
|
+
|
197
|
+
# Map Firebase Auth legacy event metadata field names to their equivalent CloudEvent field names.
|
198
|
+
FIREBASE_AUTH_METADATA_LEGACY_TO_CE = {
|
199
|
+
"createdAt" => "createTime",
|
200
|
+
"lastSignedInAt" => "lastSignInTime"
|
201
|
+
}.freeze
|
143
202
|
end
|
144
203
|
end
|