Skip to content

Commit d8d394c

Browse files
authored
feat: add remote config triggers (#37)
1 parent a8922d6 commit d8d394c

File tree

13 files changed

+813
-9
lines changed

13 files changed

+813
-9
lines changed

example/basic/lib/main.dart

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,21 @@ void main(List<String> args) async {
405405
return null;
406406
});
407407

408+
// ==========================================================================
409+
// Remote Config trigger examples
410+
// ==========================================================================
411+
412+
// Remote Config update trigger
413+
firebase.remoteConfig.onConfigUpdated((event) async {
414+
final data = event.data;
415+
print('Remote Config updated:');
416+
print(' Version: ${data?.versionNumber}');
417+
print(' Description: ${data?.description}');
418+
print(' Update Origin: ${data?.updateOrigin.value}');
419+
print(' Update Type: ${data?.updateType.value}');
420+
print(' Updated By: ${data?.updateUser.email}');
421+
});
422+
408423
// ==========================================================================
409424
// Scheduler trigger examples
410425
// ==========================================================================

example/nodejs_reference/index.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const { onPlanUpdatePublished } = require("firebase-functions/v2/alerts/billing"
1313
const { onThresholdAlertPublished } = require("firebase-functions/v2/alerts/performance");
1414
const { beforeUserCreated, beforeUserSignedIn, beforeOperation } = require("firebase-functions/v2/identity");
1515
const { onSchedule } = require("firebase-functions/v2/scheduler");
16+
const { onConfigUpdated } = require("firebase-functions/v2/remoteConfig");
1617
const { defineString, defineInt, defineBoolean } = require("firebase-functions/params");
1718

1819
// =============================================================================
@@ -338,6 +339,22 @@ exports.beforeSendSms = beforeOperation(
338339
}
339340
);
340341

342+
// =============================================================================
343+
// Remote Config trigger examples
344+
// =============================================================================
345+
346+
// Remote Config update trigger
347+
exports.onConfigUpdated = onConfigUpdated(
348+
(event) => {
349+
console.log("Remote Config updated:");
350+
console.log(" Version:", event.data?.versionNumber);
351+
console.log(" Description:", event.data?.description);
352+
console.log(" Update Origin:", event.data?.updateOrigin);
353+
console.log(" Update Type:", event.data?.updateType);
354+
console.log(" Updated By:", event.data?.updateUser?.email);
355+
}
356+
);
357+
341358
// =============================================================================
342359
// Scheduler trigger examples
343360
// =============================================================================

lib/builder.dart

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,11 @@ class _FirebaseFunctionsVisitor extends RecursiveAstVisitor<void> {
171171
'beforeSmsSent',
172172
],
173173
),
174+
_Namespace(
175+
_extractRemoteConfigFunction,
176+
'$_pkg/src/remote_config/remote_config_namespace.dart#RemoteConfigNamespace',
177+
['onConfigUpdated'],
178+
),
174179
_Namespace(
175180
// Adapter: _extractSchedulerFunction only takes node, not the second String arg
176181
(node, _) => _extractSchedulerFunction(node),
@@ -506,6 +511,20 @@ class _FirebaseFunctionsVisitor extends RecursiveAstVisitor<void> {
506511
_extractAlertEndpoint(node, alertType);
507512
}
508513

514+
/// Extracts a Remote Config function declaration.
515+
void _extractRemoteConfigFunction(MethodInvocation node, String methodName) {
516+
// Remote Config has a single event type and no filters,
517+
// so the function name is always 'onConfigUpdated'.
518+
const functionName = 'onConfigUpdated';
519+
520+
endpoints[functionName] = EndpointSpec(
521+
name: functionName,
522+
type: 'remoteConfig',
523+
options: node.findOptionsArg(),
524+
variableToParamName: _variableToParamName,
525+
);
526+
}
527+
509528
/// Helper to extract alert endpoint from a method invocation.
510529
void _extractAlertEndpoint(MethodInvocation node, String alertType) {
511530
// Extract appId from options if present

lib/firebase_functions.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ export 'src/https/https.dart';
8181
export 'src/identity/identity.dart';
8282
// Pub/Sub triggers
8383
export 'src/pubsub/pubsub.dart';
84+
// Remote Config triggers
85+
export 'src/remote_config/remote_config.dart';
8486
// Scheduler triggers
8587
export 'src/scheduler/scheduler.dart';
8688
// Core runtime

lib/src/builder/manifest.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,13 @@ void _addTrigger(
274274
'options': triggerOptions,
275275
};
276276

277+
case 'remoteConfig':
278+
map['eventTrigger'] = <String, dynamic>{
279+
'eventType': 'google.firebase.remoteconfig.remoteConfig.v1.updated',
280+
'eventFilters': <String, dynamic>{},
281+
'retry': false,
282+
};
283+
277284
case 'scheduler' when endpoint.schedule != null:
278285
final trigger = <String, dynamic>{'schedule': endpoint.schedule};
279286
if (endpoint.timeZone != null) {

lib/src/firebase.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import 'firestore/firestore_namespace.dart';
1111
import 'https/https_namespace.dart';
1212
import 'identity/identity_namespace.dart';
1313
import 'pubsub/pubsub_namespace.dart';
14+
import 'remote_config/remote_config_namespace.dart';
1415
import 'scheduler/scheduler_namespace.dart';
1516

1617
/// Main Firebase Functions instance.
@@ -93,6 +94,9 @@ class Firebase {
9394
/// Identity Platform namespace.
9495
IdentityNamespace get identity => IdentityNamespace(this);
9596

97+
/// Remote Config namespace.
98+
RemoteConfigNamespace get remoteConfig => RemoteConfigNamespace(this);
99+
96100
/// Scheduler namespace.
97101
SchedulerNamespace get scheduler => SchedulerNamespace(this);
98102
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/// The data within Firebase Remote Config update events.
2+
class ConfigUpdateData {
3+
const ConfigUpdateData({
4+
required this.versionNumber,
5+
required this.updateTime,
6+
required this.updateUser,
7+
required this.description,
8+
required this.updateOrigin,
9+
required this.updateType,
10+
this.rollbackSource,
11+
});
12+
13+
/// Parses a ConfigUpdateData from JSON (CloudEvent data format).
14+
factory ConfigUpdateData.fromJson(Map<String, dynamic> json) {
15+
return ConfigUpdateData(
16+
versionNumber: json['versionNumber'] as num,
17+
updateTime: DateTime.parse(json['updateTime'] as String),
18+
updateUser: ConfigUser.fromJson(
19+
json['updateUser'] as Map<String, dynamic>,
20+
),
21+
description: json['description'] as String? ?? '',
22+
updateOrigin: ConfigUpdateOrigin.fromValue(
23+
json['updateOrigin'] as String,
24+
),
25+
updateType: ConfigUpdateType.fromValue(json['updateType'] as String),
26+
rollbackSource: json['rollbackSource'] as int?,
27+
);
28+
}
29+
30+
/// The version number of the version's corresponding Remote Config template.
31+
final num versionNumber;
32+
33+
/// When the Remote Config template was written to the Remote Config server.
34+
final DateTime updateTime;
35+
36+
/// Aggregation of all metadata fields about the account that performed
37+
/// the update.
38+
final ConfigUser updateUser;
39+
40+
/// The user-provided description of the corresponding Remote Config template.
41+
final String description;
42+
43+
/// Where the update action originated.
44+
final ConfigUpdateOrigin updateOrigin;
45+
46+
/// What type of update was made.
47+
final ConfigUpdateType updateType;
48+
49+
/// Only present if this version is the result of a rollback, and will be
50+
/// the version number of the Remote Config template that was rolled-back to.
51+
final int? rollbackSource;
52+
53+
/// Converts this data to JSON.
54+
Map<String, dynamic> toJson() => <String, dynamic>{
55+
'versionNumber': versionNumber,
56+
'updateTime': updateTime.toIso8601String(),
57+
'updateUser': updateUser.toJson(),
58+
'description': description,
59+
'updateOrigin': updateOrigin.value,
60+
'updateType': updateType.value,
61+
if (rollbackSource != null) 'rollbackSource': rollbackSource,
62+
};
63+
}
64+
65+
/// The person/service account that wrote a Remote Config template.
66+
class ConfigUser {
67+
const ConfigUser({
68+
required this.name,
69+
required this.email,
70+
required this.imageUrl,
71+
});
72+
73+
/// Parses a ConfigUser from JSON.
74+
factory ConfigUser.fromJson(Map<String, dynamic> json) {
75+
return ConfigUser(
76+
name: json['name'] as String? ?? '',
77+
email: json['email'] as String? ?? '',
78+
imageUrl: json['imageUrl'] as String? ?? '',
79+
);
80+
}
81+
82+
/// Display name.
83+
final String name;
84+
85+
/// Email address.
86+
final String email;
87+
88+
/// Image URL.
89+
final String imageUrl;
90+
91+
/// Converts this user to JSON.
92+
Map<String, dynamic> toJson() => <String, dynamic>{
93+
'name': name,
94+
'email': email,
95+
'imageUrl': imageUrl,
96+
};
97+
}
98+
99+
/// What type of update origin was associated with the Remote Config template
100+
/// version.
101+
enum ConfigUpdateOrigin {
102+
remoteConfigUpdateOriginUnspecified(
103+
'REMOTE_CONFIG_UPDATE_ORIGIN_UNSPECIFIED',
104+
),
105+
console('CONSOLE'),
106+
restApi('REST_API'),
107+
adminSdkNode('ADMIN_SDK_NODE');
108+
109+
const ConfigUpdateOrigin(this.value);
110+
111+
/// The string value as sent in CloudEvents.
112+
final String value;
113+
114+
/// Parses a ConfigUpdateOrigin from its string value.
115+
static ConfigUpdateOrigin fromValue(String value) {
116+
return ConfigUpdateOrigin.values.firstWhere(
117+
(e) => e.value == value,
118+
orElse: () => ConfigUpdateOrigin.remoteConfigUpdateOriginUnspecified,
119+
);
120+
}
121+
}
122+
123+
/// What type of update was associated with the Remote Config template version.
124+
enum ConfigUpdateType {
125+
remoteConfigUpdateTypeUnspecified('REMOTE_CONFIG_UPDATE_TYPE_UNSPECIFIED'),
126+
incrementalUpdate('INCREMENTAL_UPDATE'),
127+
forcedUpdate('FORCED_UPDATE'),
128+
rollback('ROLLBACK');
129+
130+
const ConfigUpdateType(this.value);
131+
132+
/// The string value as sent in CloudEvents.
133+
final String value;
134+
135+
/// Parses a ConfigUpdateType from its string value.
136+
static ConfigUpdateType fromValue(String value) {
137+
return ConfigUpdateType.values.firstWhere(
138+
(e) => e.value == value,
139+
orElse: () => ConfigUpdateType.remoteConfigUpdateTypeUnspecified,
140+
);
141+
}
142+
}

lib/src/remote_config/options.dart

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import '../common/options.dart';
2+
3+
/// Options for Remote Config event handlers.
4+
class RemoteConfigOptions extends GlobalOptions {
5+
const RemoteConfigOptions({
6+
super.concurrency,
7+
super.cpu,
8+
super.ingressSettings,
9+
super.invoker,
10+
super.labels,
11+
super.minInstances,
12+
super.maxInstances,
13+
super.memory,
14+
super.omit,
15+
super.preserveExternalChanges,
16+
super.region,
17+
super.secrets,
18+
super.serviceAccount,
19+
super.timeoutSeconds,
20+
super.vpcConnector,
21+
super.vpcConnectorEgressSettings,
22+
});
23+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export 'config_update_data.dart';
2+
export 'options.dart';
3+
export 'remote_config_namespace.dart';
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import 'dart:async';
2+
3+
import 'package:meta/meta.dart';
4+
import 'package:shelf/shelf.dart';
5+
6+
import '../common/cloud_event.dart';
7+
import '../firebase.dart';
8+
import 'config_update_data.dart';
9+
import 'options.dart';
10+
11+
/// Remote Config triggers namespace.
12+
///
13+
/// Provides methods to define Remote Config-triggered Cloud Functions.
14+
class RemoteConfigNamespace extends FunctionsNamespace {
15+
const RemoteConfigNamespace(super.firebase);
16+
17+
/// Creates a function triggered by Remote Config updates.
18+
///
19+
/// The handler receives a [CloudEvent] containing the [ConfigUpdateData].
20+
///
21+
/// Example:
22+
/// ```dart
23+
/// firebase.remoteConfig.onConfigUpdated(
24+
/// (event) async {
25+
/// final data = event.data;
26+
/// print('Config updated: version ${data?.versionNumber}');
27+
/// print('Updated by: ${data?.updateUser.email}');
28+
/// },
29+
/// );
30+
/// ```
31+
void onConfigUpdated(
32+
Future<void> Function(CloudEvent<ConfigUpdateData> event) handler, {
33+
// ignore: experimental_member_use
34+
@mustBeConst RemoteConfigOptions? options = const RemoteConfigOptions(),
35+
}) {
36+
const functionName = 'onConfigUpdated';
37+
38+
firebase.registerFunction(functionName, (request) async {
39+
try {
40+
// Read and parse CloudEvent
41+
final json = await parseAndValidateCloudEvent(request);
42+
43+
// Verify it's a Remote Config event
44+
if (!_isRemoteConfigEvent(json['type'] as String)) {
45+
return Response(
46+
400,
47+
body: 'Invalid event type for Remote Config: ${json['type']}',
48+
);
49+
}
50+
51+
// Parse CloudEvent with ConfigUpdateData
52+
final event = CloudEvent<ConfigUpdateData>.fromJson(
53+
json,
54+
(data) => ConfigUpdateData.fromJson(data),
55+
);
56+
57+
// Execute handler
58+
await handler(event);
59+
60+
// Return success
61+
return Response.ok('');
62+
} on FormatException catch (e) {
63+
return Response(400, body: 'Invalid CloudEvent: ${e.message}');
64+
} catch (e) {
65+
return Response(500, body: 'Error processing Remote Config update: $e');
66+
}
67+
});
68+
}
69+
70+
/// Checks if the CloudEvent type is a Remote Config update event.
71+
bool _isRemoteConfigEvent(String type) =>
72+
type == 'google.firebase.remoteconfig.remoteConfig.v1.updated';
73+
}

0 commit comments

Comments
 (0)