Skip to content

Commit 4ebbdb0

Browse files
Merge branch 'main' into fix-date-time-in-schedule_fn
2 parents 225fb60 + c5cb62a commit 4ebbdb0

File tree

4 files changed

+537
-0
lines changed

4 files changed

+537
-0
lines changed

docs/generate.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ PY_MODULES='firebase_functions.core
8888
firebase_functions.alerts.billing_fn
8989
firebase_functions.alerts.crashlytics_fn
9090
firebase_functions.alerts.performance_fn
91+
firebase_functions.dataconnect_fn
9192
firebase_functions.db_fn
9293
firebase_functions.eventarc_fn
9394
firebase_functions.firestore_fn
Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
1+
# Copyright 2022 Google Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
"""
15+
Module for Cloud Functions that are triggered by Firebase Data Connect.
16+
"""
17+
18+
# pylint: disable=protected-access
19+
import dataclasses as _dataclass
20+
import datetime as _dt
21+
import functools as _functools
22+
import typing as _typing
23+
24+
import cloudevents.http as _ce
25+
26+
import firebase_functions.core as _core
27+
import firebase_functions.private.path_pattern as _path_pattern
28+
import firebase_functions.private.util as _util
29+
from firebase_functions.options import DataConnectOptions
30+
31+
_event_type_mutation_executed = "google.firebase.dataconnect.connector.v1.mutationExecuted"
32+
33+
AuthType = _typing.Literal["app_user", "admin", "unknown"]
34+
35+
36+
@_dataclass.dataclass(frozen=True)
37+
class Event(_core.CloudEvent[_core.T]):
38+
"""
39+
A CloudEvent that contains MutationEventData.
40+
"""
41+
42+
location: str
43+
"""
44+
The location of the database.
45+
"""
46+
47+
project: str
48+
"""
49+
The project identifier.
50+
"""
51+
52+
params: dict[str, str]
53+
"""
54+
A dict containing the values of the path patterns.
55+
Only named capture groups are populated - {key}, {key=*}, {key=**}
56+
"""
57+
58+
auth_type: AuthType
59+
"""
60+
The type of principal that triggered the event.
61+
"""
62+
63+
auth_id: str
64+
"""
65+
The unique identifier for the principal.
66+
"""
67+
68+
69+
@_dataclass.dataclass(frozen=True)
70+
class GraphqlErrorExtensions:
71+
"""
72+
GraphqlErrorExtensions contains additional information of `GraphqlError`.
73+
"""
74+
75+
file: str
76+
"""
77+
The source file name where the error occurred.
78+
Included only for `UpdateSchema` and `UpdateConnector`, it corresponds
79+
to `File.path` of the provided `Source`.
80+
"""
81+
82+
code: str
83+
"""
84+
Maps to canonical gRPC codes.
85+
If not specified, it represents `Code.INTERNAL`.
86+
"""
87+
88+
debug_details: str
89+
"""
90+
More detailed error message to assist debugging.
91+
It contains application business logic that are inappropriate to leak
92+
publicly.
93+
94+
In the emulator, Data Connect API always includes it to assist local
95+
development and debugging.
96+
In the backend, ConnectorService always hides it.
97+
GraphqlService without impersonation always include it.
98+
GraphqlService with impersonation includes it only if explicitly opted-in
99+
with `include_debug_details` in `GraphqlRequestExtensions`.
100+
"""
101+
102+
103+
@_dataclass.dataclass(frozen=True)
104+
class SourceLocation:
105+
"""
106+
SourceLocation references a location in a GraphQL source.
107+
"""
108+
109+
line: int
110+
"""
111+
Line number starting at 1.
112+
"""
113+
114+
column: int
115+
"""
116+
Column number starting at 1.
117+
"""
118+
119+
120+
@_dataclass.dataclass(frozen=True)
121+
class GraphQLError:
122+
"""
123+
An error that occurred during the execution of a GraphQL request.
124+
"""
125+
126+
message: str
127+
"""
128+
A string describing the error.
129+
"""
130+
131+
locations: list[dict[str, int]] | None = None
132+
"""
133+
The source locations where the error occurred.
134+
Locations should help developers and toolings identify the source of error
135+
quickly.
136+
137+
Included in admin endpoints (`ExecuteGraphql`, `ExecuteGraphqlRead`,
138+
`UpdateSchema` and `UpdateConnector`) to reference the provided GraphQL
139+
GQL document.
140+
141+
Omitted in `ExecuteMutation` and `ExecuteQuery` since the caller shouldn't
142+
have access access the underlying GQL source.
143+
"""
144+
145+
path: list[str | int] | None = None
146+
"""
147+
The result field which could not be populated due to error.
148+
149+
Clients can use path to identify whether a null result is intentional or
150+
caused by a runtime error.
151+
It should be a list of string or index from the root of GraphQL query
152+
document.
153+
"""
154+
155+
extensions: GraphqlErrorExtensions | None = None
156+
157+
158+
@_dataclass.dataclass(frozen=True)
159+
class Mutation:
160+
"""
161+
An object within Firebase Data Connect.
162+
"""
163+
164+
data: _typing.Any
165+
"""
166+
The result of the execution of the requested operation.
167+
If an error was raised before execution begins, the data entry should not
168+
be present in the result. (a request error:
169+
https://spec.graphql.org/draft/#sec-Errors.Request-Errors) If an error was
170+
raised during the execution that prevented a valid response, the data entry
171+
in the response should be null. (a field error:
172+
https://spec.graphql.org/draft/#sec-Errors.Error-Result-Format)
173+
"""
174+
175+
variables: _typing.Any
176+
"""
177+
Values for GraphQL variables provided in this request.
178+
"""
179+
180+
errors: list[GraphQLError] | None = None
181+
"""
182+
Errors of this response.
183+
If the data entry in the response is not present, the errors entry must be
184+
present.
185+
It conforms to https://spec.graphql.org/draft/#sec-Errors.
186+
"""
187+
188+
189+
@_dataclass.dataclass(frozen=True)
190+
class MutationEventData:
191+
"""
192+
The data within all Mutation events.
193+
"""
194+
195+
payload: Mutation
196+
197+
198+
_E1 = Event[MutationEventData]
199+
_C1 = _typing.Callable[[_E1], None]
200+
201+
202+
def _dataconnect_endpoint_handler(
203+
func: _C1,
204+
event_type: str,
205+
service_pattern: _path_pattern.PathPattern | None,
206+
connector_pattern: _path_pattern.PathPattern | None,
207+
operation_pattern: _path_pattern.PathPattern | None,
208+
raw: _ce.CloudEvent,
209+
) -> None:
210+
# Currently, only mutationExecuted is supported
211+
if event_type != _event_type_mutation_executed:
212+
raise NotImplementedError(f"Unsupported event type: {event_type}. Only {_event_type_mutation_executed} is currently supported.")
213+
214+
event_attributes = raw._get_attributes()
215+
event_data: _typing.Any = raw.get_data()
216+
217+
dataconnect_event_data = event_data
218+
219+
event_service = event_attributes["service"]
220+
event_connector = event_attributes["connector"]
221+
event_operation = event_attributes["operation"]
222+
params: dict[str, str] = {}
223+
224+
if service_pattern:
225+
params = {**params, **service_pattern.extract_matches(event_service)}
226+
if connector_pattern:
227+
params = {
228+
**params,
229+
**connector_pattern.extract_matches(event_connector)
230+
}
231+
if operation_pattern:
232+
params = {
233+
**params,
234+
**operation_pattern.extract_matches(event_operation)
235+
}
236+
237+
event_auth_type = event_attributes["authtype"]
238+
event_auth_id = event_attributes["authid"]
239+
event_time = _util.timestamp_conversion(event_attributes["time"])
240+
241+
dataconnect_event = Event(
242+
specversion=event_attributes["specversion"],
243+
id=event_attributes["id"],
244+
source=event_attributes["source"],
245+
type=event_attributes["type"],
246+
time=event_time,
247+
subject=event_attributes.get("subject"),
248+
location=event_attributes["location"],
249+
project=event_attributes["project"],
250+
params=params,
251+
data=dataconnect_event_data,
252+
auth_type=event_auth_type,
253+
auth_id=event_auth_id,
254+
)
255+
_core._with_init(func)(dataconnect_event)
256+
257+
258+
@_util.copy_func_kwargs(DataConnectOptions)
259+
def on_mutation_executed(**kwargs) -> _typing.Callable[[_C1], _C1]:
260+
"""
261+
Event handler that triggers when a mutation is executed in Firebase Data Connect.
262+
263+
Example:
264+
265+
.. code-block:: python
266+
267+
@on_mutation_executed(
268+
service = "service-id",
269+
connector = "connector-id",
270+
operation = "mutation-name"
271+
)
272+
def mutation_executed_handler(event: Event[MutationEventData]):
273+
pass
274+
275+
:param \\*\\*kwargs: DataConnect options.
276+
:type \\*\\*kwargs: as :exc:`firebase_functions.options.DataConnectOptions`
277+
:rtype: :exc:`typing.Callable`
278+
\\[ \\[ :exc:`firebase_functions.dataconnect_fn.Event` \\[
279+
:exc:`object` \\] \\], `None` \\]
280+
A function that takes a DataConnect event and returns ``None``.
281+
"""
282+
options = DataConnectOptions(**kwargs)
283+
284+
def on_mutation_executed_inner_decorator(func: _C1):
285+
service_pattern = _path_pattern.PathPattern(
286+
options.service) if options.service else None
287+
connector_pattern = _path_pattern.PathPattern(
288+
options.connector) if options.connector else None
289+
operation_pattern = _path_pattern.PathPattern(
290+
options.operation) if options.operation else None
291+
292+
@_functools.wraps(func)
293+
def on_mutation_executed_wrapped(raw: _ce.CloudEvent):
294+
return _dataconnect_endpoint_handler(
295+
func,
296+
_event_type_mutation_executed,
297+
service_pattern,
298+
connector_pattern,
299+
operation_pattern,
300+
raw,
301+
)
302+
303+
_util.set_func_endpoint_attr(
304+
on_mutation_executed_wrapped,
305+
options._endpoint(
306+
event_type=_event_type_mutation_executed,
307+
func_name=func.__name__,
308+
service_pattern=service_pattern,
309+
connector_pattern=connector_pattern,
310+
operation_pattern=operation_pattern,
311+
),
312+
)
313+
return on_mutation_executed_wrapped
314+
315+
return on_mutation_executed_inner_decorator

src/firebase_functions/options.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1152,6 +1152,73 @@ def _endpoint(
11521152
return _manifest.ManifestEndpoint(**_typing.cast(dict, kwargs_merged))
11531153

11541154

1155+
@_dataclasses.dataclass(frozen=True, kw_only=True)
1156+
class DataConnectOptions(RuntimeOptions):
1157+
"""
1158+
Options specific to Firebase Data Connect function types.
1159+
Internal use only.
1160+
"""
1161+
1162+
service: str | None = None
1163+
"""
1164+
The Firebase Data Connect service ID.
1165+
"""
1166+
1167+
connector: str | None = None
1168+
"""
1169+
The Firebase Data Connect connector ID.
1170+
"""
1171+
1172+
operation: str | None = None
1173+
"""
1174+
Name of the operation.
1175+
"""
1176+
1177+
def _endpoint(
1178+
self,
1179+
**kwargs,
1180+
) -> _manifest.ManifestEndpoint:
1181+
assert kwargs["event_type"] is not None
1182+
1183+
service_pattern: _path_pattern.PathPattern = kwargs["service_pattern"]
1184+
connector_pattern: _path_pattern.PathPattern = kwargs["connector_pattern"]
1185+
operation_pattern: _path_pattern.PathPattern = kwargs["operation_pattern"]
1186+
1187+
event_filters: _typing.Any = {}
1188+
event_filters_path_patterns: _typing.Any = {}
1189+
1190+
if self.service:
1191+
if service_pattern.has_wildcards:
1192+
event_filters_path_patterns["service"] = service_pattern.value
1193+
else:
1194+
event_filters["service"] = service_pattern.value
1195+
1196+
if self.connector:
1197+
if connector_pattern.has_wildcards:
1198+
event_filters_path_patterns["connector"] = connector_pattern.value
1199+
else:
1200+
event_filters["connector"] = connector_pattern.value
1201+
1202+
if self.operation:
1203+
if operation_pattern.has_wildcards:
1204+
event_filters_path_patterns["operation"] = operation_pattern.value
1205+
else:
1206+
event_filters["operation"] = operation_pattern.value
1207+
1208+
event_trigger = _manifest.EventTrigger(
1209+
eventType=kwargs["event_type"],
1210+
retry=False,
1211+
eventFilters=event_filters,
1212+
eventFilterPathPatterns=event_filters_path_patterns,
1213+
)
1214+
1215+
kwargs_merged = {
1216+
**_dataclasses.asdict(super()._endpoint(**kwargs)),
1217+
"eventTrigger": event_trigger,
1218+
}
1219+
return _manifest.ManifestEndpoint(**_typing.cast(dict, kwargs_merged))
1220+
1221+
11551222
_GLOBAL_OPTIONS = RuntimeOptions()
11561223
"""The current default options for all functions. Internal use only."""
11571224

0 commit comments

Comments
 (0)