Skip to content

Commit 49729c9

Browse files
authored
feat: add database support
feat: add database support
2 parents 3e01e83 + 1fe3d85 commit 49729c9

20 files changed

+2267
-26
lines changed

example/basic/database.rules.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"rules": {
3+
".read": true,
4+
".write": true
5+
}
6+
}

example/basic/firebase.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,14 @@
1616
"firestore": {
1717
"port": 8080
1818
},
19+
"database": {
20+
"port": 9000
21+
},
1922
"ui": {
2023
"enabled": false
2124
}
25+
},
26+
"database": {
27+
"rules": "database.rules.json"
2228
}
2329
}

example/basic/lib/main.dart

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,75 @@ void main(List<String> args) {
137137
},
138138
);
139139

140+
// ==========================================================================
141+
// Realtime Database trigger examples
142+
// ==========================================================================
143+
144+
// Database onValueCreated - triggers when data is created
145+
firebase.database.onValueCreated(
146+
ref: 'messages/{messageId}',
147+
(event) async {
148+
final data = event.data?.val();
149+
print('Database value created: messages/${event.params['messageId']}');
150+
print(' Data: $data');
151+
print(' Instance: ${event.instance}');
152+
print(' Ref: ${event.ref}');
153+
},
154+
);
155+
156+
// Database onValueUpdated - triggers when data is updated
157+
firebase.database.onValueUpdated(
158+
ref: 'messages/{messageId}',
159+
(event) async {
160+
final before = event.data?.before?.val();
161+
final after = event.data?.after?.val();
162+
print('Database value updated: messages/${event.params['messageId']}');
163+
print(' Before: $before');
164+
print(' After: $after');
165+
},
166+
);
167+
168+
// Database onValueDeleted - triggers when data is deleted
169+
firebase.database.onValueDeleted(
170+
ref: 'messages/{messageId}',
171+
(event) async {
172+
final data = event.data?.val();
173+
print('Database value deleted: messages/${event.params['messageId']}');
174+
print(' Final data: $data');
175+
},
176+
);
177+
178+
// Database onValueWritten - triggers on any write (create, update, delete)
179+
firebase.database.onValueWritten(
180+
ref: 'messages/{messageId}',
181+
(event) async {
182+
final before = event.data?.before;
183+
final after = event.data?.after;
184+
print('Database value written: messages/${event.params['messageId']}');
185+
if (before == null || !before.exists()) {
186+
print(' Operation: CREATE');
187+
print(' New data: ${after?.val()}');
188+
} else if (after == null || !after.exists()) {
189+
print(' Operation: DELETE');
190+
print(' Deleted data: ${before.val()}');
191+
} else {
192+
print(' Operation: UPDATE');
193+
print(' Before: ${before.val()}');
194+
print(' After: ${after.val()}');
195+
}
196+
},
197+
);
198+
199+
// Nested path database trigger
200+
firebase.database.onValueWritten(
201+
ref: 'users/{userId}/status',
202+
(event) async {
203+
final after = event.data?.after?.val();
204+
print('User status changed: ${event.params['userId']}');
205+
print(' New status: $after');
206+
},
207+
);
208+
140209
print('Functions registered successfully!');
141210
});
142211
}

example/nodejs_reference/index.js

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
const { onRequest } = require("firebase-functions/v2/https");
88
const { onMessagePublished } = require("firebase-functions/v2/pubsub");
9+
const { onDocumentCreated, onDocumentUpdated, onDocumentDeleted, onDocumentWritten } = require("firebase-functions/v2/firestore");
10+
const { onValueCreated, onValueUpdated, onValueDeleted, onValueWritten } = require("firebase-functions/v2/database");
911
const { defineString, defineInt, defineBoolean } = require("firebase-functions/params");
1012

1113
// =============================================================================
@@ -69,3 +71,127 @@ exports.onMessagePublished_mytopic = onMessagePublished(
6971
console.log(" Attributes:", message.attributes);
7072
}
7173
);
74+
75+
// =============================================================================
76+
// Firestore trigger examples
77+
// =============================================================================
78+
79+
exports.onDocumentCreated_users_userId = onDocumentCreated(
80+
"users/{userId}",
81+
(event) => {
82+
const data = event.data?.data();
83+
console.log("Document created: users/" + event.params.userId);
84+
console.log(" Name:", data?.name);
85+
console.log(" Email:", data?.email);
86+
}
87+
);
88+
89+
exports.onDocumentUpdated_users_userId = onDocumentUpdated(
90+
"users/{userId}",
91+
(event) => {
92+
const before = event.data?.before?.data();
93+
const after = event.data?.after?.data();
94+
console.log("Document updated: users/" + event.params.userId);
95+
console.log(" Before:", before);
96+
console.log(" After:", after);
97+
}
98+
);
99+
100+
exports.onDocumentDeleted_users_userId = onDocumentDeleted(
101+
"users/{userId}",
102+
(event) => {
103+
const data = event.data?.data();
104+
console.log("Document deleted: users/" + event.params.userId);
105+
console.log(" Final data:", data);
106+
}
107+
);
108+
109+
exports.onDocumentWritten_users_userId = onDocumentWritten(
110+
"users/{userId}",
111+
(event) => {
112+
const before = event.data?.before?.data();
113+
const after = event.data?.after?.data();
114+
console.log("Document written: users/" + event.params.userId);
115+
if (!before && after) {
116+
console.log(" Operation: CREATE");
117+
} else if (before && after) {
118+
console.log(" Operation: UPDATE");
119+
} else if (before && !after) {
120+
console.log(" Operation: DELETE");
121+
}
122+
}
123+
);
124+
125+
exports.onDocumentCreated_posts_postId_comments_commentId = onDocumentCreated(
126+
"posts/{postId}/comments/{commentId}",
127+
(event) => {
128+
const data = event.data?.data();
129+
console.log("Comment created: posts/" + event.params.postId + "/comments/" + event.params.commentId);
130+
console.log(" Text:", data?.text);
131+
console.log(" Author:", data?.author);
132+
}
133+
);
134+
135+
// =============================================================================
136+
// Realtime Database trigger examples
137+
// =============================================================================
138+
139+
exports.onValueCreated_messages_messageId = onValueCreated(
140+
"/messages/{messageId}",
141+
(event) => {
142+
const data = event.data?.val();
143+
console.log("Database value created: messages/" + event.params.messageId);
144+
console.log(" Data:", data);
145+
console.log(" Instance:", event.instance);
146+
console.log(" Ref:", event.ref);
147+
}
148+
);
149+
150+
exports.onValueUpdated_messages_messageId = onValueUpdated(
151+
"/messages/{messageId}",
152+
(event) => {
153+
const before = event.data?.before?.val();
154+
const after = event.data?.after?.val();
155+
console.log("Database value updated: messages/" + event.params.messageId);
156+
console.log(" Before:", before);
157+
console.log(" After:", after);
158+
}
159+
);
160+
161+
exports.onValueDeleted_messages_messageId = onValueDeleted(
162+
"/messages/{messageId}",
163+
(event) => {
164+
const data = event.data?.val();
165+
console.log("Database value deleted: messages/" + event.params.messageId);
166+
console.log(" Final data:", data);
167+
}
168+
);
169+
170+
exports.onValueWritten_messages_messageId = onValueWritten(
171+
"/messages/{messageId}",
172+
(event) => {
173+
const before = event.data?.before;
174+
const after = event.data?.after;
175+
console.log("Database value written: messages/" + event.params.messageId);
176+
if (!before?.exists() && after?.exists()) {
177+
console.log(" Operation: CREATE");
178+
console.log(" New data:", after.val());
179+
} else if (before?.exists() && !after?.exists()) {
180+
console.log(" Operation: DELETE");
181+
console.log(" Deleted data:", before.val());
182+
} else {
183+
console.log(" Operation: UPDATE");
184+
console.log(" Before:", before?.val());
185+
console.log(" After:", after?.val());
186+
}
187+
}
188+
);
189+
190+
exports.onValueWritten_users_userId_status = onValueWritten(
191+
"/users/{userId}/status",
192+
(event) => {
193+
const after = event.data?.after?.val();
194+
console.log("User status changed:", event.params.userId);
195+
console.log(" New status:", after);
196+
}
197+
);

lib/builder.dart

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ class _TypeCheckers {
2626
static final pubsubNamespace = TypeChecker.fromRuntime(ff.PubSubNamespace);
2727
static final firestoreNamespace =
2828
TypeChecker.fromRuntime(ff.FirestoreNamespace);
29+
static final databaseNamespace =
30+
TypeChecker.fromRuntime(ff.DatabaseNamespace);
2931
}
3032

3133
/// The main builder that generates functions.yaml.
@@ -132,6 +134,16 @@ class _FirebaseFunctionsVisitor extends RecursiveAstVisitor<void> {
132134
}
133135
}
134136

137+
// Check for Database function declarations
138+
if (target != null && _isDatabaseNamespace(target)) {
139+
if (methodName == 'onValueCreated' ||
140+
methodName == 'onValueUpdated' ||
141+
methodName == 'onValueDeleted' ||
142+
methodName == 'onValueWritten') {
143+
_extractDatabaseFunction(node, methodName);
144+
}
145+
}
146+
135147
// Check for parameter definitions (top-level function calls with no target)
136148
if (target == null && _isParamDefinition(methodName)) {
137149
_extractParameterFromMethod(node, methodName);
@@ -236,6 +248,13 @@ class _FirebaseFunctionsVisitor extends RecursiveAstVisitor<void> {
236248
return _TypeCheckers.firestoreNamespace.isExactlyType(staticType);
237249
}
238250

251+
/// Checks if the target is firebase.database.
252+
bool _isDatabaseNamespace(Expression target) {
253+
final staticType = target.staticType;
254+
if (staticType == null) return false;
255+
return _TypeCheckers.databaseNamespace.isExactlyType(staticType);
256+
}
257+
239258
/// Checks if this is a parameter definition function.
240259
bool _isParamDefinition(String name) =>
241260
name == 'defineString' ||
@@ -336,6 +355,44 @@ class _FirebaseFunctionsVisitor extends RecursiveAstVisitor<void> {
336355
);
337356
}
338357

358+
/// Extracts a Database function declaration.
359+
void _extractDatabaseFunction(MethodInvocation node, String methodName) {
360+
// Extract ref path from named argument
361+
final refArg = _findNamedArg(node, 'ref');
362+
if (refArg == null) return;
363+
364+
final refPath = _extractStringLiteral(refArg);
365+
if (refPath == null) return;
366+
367+
// Extract options if present (for instance)
368+
final optionsArg = _findNamedArg(node, 'options');
369+
String? instance;
370+
371+
if (optionsArg is InstanceCreationExpression) {
372+
instance = _extractStringField(optionsArg, 'instance');
373+
}
374+
375+
// Generate function name from ref path and event type
376+
// Similar to how we do it in database_namespace.dart
377+
final sanitizedPath = refPath
378+
.replaceAll(RegExp(r'^/+|/+$'), '') // Remove leading/trailing slashes
379+
.replaceAll('/', '_')
380+
.replaceAll('{', '')
381+
.replaceAll('}', '')
382+
.replaceAll('-', '');
383+
final functionName = '${methodName}_$sanitizedPath';
384+
385+
endpoints[functionName] = _EndpointSpec(
386+
name: functionName,
387+
type: 'database',
388+
databaseEventType: methodName,
389+
refPath: refPath,
390+
instance: instance ?? '*',
391+
options: optionsArg is InstanceCreationExpression ? optionsArg : null,
392+
variableToParamName: _variableToParamName,
393+
);
394+
}
395+
339396
/// Extracts a parameter definition from FunctionExpressionInvocation.
340397
void _extractParameter(
341398
FunctionExpressionInvocation node,
@@ -538,16 +595,22 @@ class _EndpointSpec {
538595
this.documentPath,
539596
this.database,
540597
this.namespace,
598+
this.databaseEventType,
599+
this.refPath,
600+
this.instance,
541601
this.options,
542602
this.variableToParamName = const {},
543603
});
544604
final String name;
545-
final String type; // 'https', 'callable', 'pubsub', 'firestore'
605+
final String type; // 'https', 'callable', 'pubsub', 'firestore', 'database'
546606
final String? topic; // For Pub/Sub functions
547607
final String? firestoreEventType; // For Firestore: onDocumentCreated, etc.
548608
final String? documentPath; // For Firestore: users/{userId}
549609
final String? database; // For Firestore: (default) or database name
550610
final String? namespace; // For Firestore: (default) or namespace
611+
final String? databaseEventType; // For Database: onValueCreated, etc.
612+
final String? refPath; // For Database: /users/{userId}
613+
final String? instance; // For Database: database instance or '*'
551614
final InstanceCreationExpression? options;
552615
final Map<String, String> variableToParamName;
553616

@@ -1261,6 +1324,27 @@ String _generateYaml(
12611324
buffer.writeln(' document: "${endpoint.documentPath}"');
12621325
}
12631326

1327+
buffer.writeln(' retry: false');
1328+
} else if (endpoint.type == 'database' &&
1329+
endpoint.databaseEventType != null &&
1330+
endpoint.refPath != null) {
1331+
// Map Dart method name to Database CloudEvent type
1332+
final eventType = _mapDatabaseEventType(endpoint.databaseEventType!);
1333+
1334+
buffer.writeln(' eventTrigger:');
1335+
buffer.writeln(' eventType: "$eventType"');
1336+
// Database triggers use empty eventFilters
1337+
buffer.writeln(' eventFilters: {}');
1338+
1339+
// Both ref and instance go in eventFilterPathPatterns
1340+
// The ref path should not have a leading slash to match Node.js format
1341+
final normalizedRef = endpoint.refPath!.startsWith('/')
1342+
? endpoint.refPath!.substring(1)
1343+
: endpoint.refPath!;
1344+
buffer.writeln(' eventFilterPathPatterns:');
1345+
buffer.writeln(' ref: "$normalizedRef"');
1346+
buffer.writeln(' instance: "${endpoint.instance ?? '*'}"');
1347+
12641348
buffer.writeln(' retry: false');
12651349
}
12661350
}
@@ -1278,6 +1362,15 @@ String _mapFirestoreEventType(String methodName) => switch (methodName) {
12781362
_ => throw ArgumentError('Unknown Firestore event type: $methodName'),
12791363
};
12801364

1365+
/// Maps Database method name to CloudEvent event type.
1366+
String _mapDatabaseEventType(String methodName) => switch (methodName) {
1367+
'onValueCreated' => 'google.firebase.database.ref.v1.created',
1368+
'onValueUpdated' => 'google.firebase.database.ref.v1.updated',
1369+
'onValueDeleted' => 'google.firebase.database.ref.v1.deleted',
1370+
'onValueWritten' => 'google.firebase.database.ref.v1.written',
1371+
_ => throw ArgumentError('Unknown Database event type: $methodName'),
1372+
};
1373+
12811374
/// Converts a value to YAML format.
12821375
String _yamlValue(dynamic value) {
12831376
if (value is String) {

lib/firebase_functions.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ export 'src/common/expression.dart';
6767
export 'src/common/on_init.dart' show onInit;
6868
export 'src/common/options.dart';
6969
export 'src/common/params.dart';
70+
// Database triggers
71+
export 'src/database/database.dart';
7072
// Core firebase instance
7173
export 'src/firebase.dart' show Firebase;
7274
// Firestore triggers

0 commit comments

Comments
 (0)