Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions example/basic/database.rules.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"rules": {
".read": true,
".write": true
}
}
6 changes: 6 additions & 0 deletions example/basic/firebase.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,14 @@
"firestore": {
"port": 8080
},
"database": {
"port": 9000
},
"ui": {
"enabled": false
}
},
"database": {
"rules": "database.rules.json"
}
}
69 changes: 69 additions & 0 deletions example/basic/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,75 @@ void main(List<String> args) {
},
);

// ==========================================================================
// Realtime Database trigger examples
// ==========================================================================

// Database onValueCreated - triggers when data is created
firebase.database.onValueCreated(
ref: 'messages/{messageId}',
(event) async {
final data = event.data?.val();
print('Database value created: messages/${event.params['messageId']}');
print(' Data: $data');
print(' Instance: ${event.instance}');
print(' Ref: ${event.ref}');
},
);

// Database onValueUpdated - triggers when data is updated
firebase.database.onValueUpdated(
ref: 'messages/{messageId}',
(event) async {
final before = event.data?.before?.val();
final after = event.data?.after?.val();
print('Database value updated: messages/${event.params['messageId']}');
print(' Before: $before');
print(' After: $after');
},
);

// Database onValueDeleted - triggers when data is deleted
firebase.database.onValueDeleted(
ref: 'messages/{messageId}',
(event) async {
final data = event.data?.val();
print('Database value deleted: messages/${event.params['messageId']}');
print(' Final data: $data');
},
);

// Database onValueWritten - triggers on any write (create, update, delete)
firebase.database.onValueWritten(
ref: 'messages/{messageId}',
(event) async {
final before = event.data?.before;
final after = event.data?.after;
print('Database value written: messages/${event.params['messageId']}');
if (before == null || !before.exists()) {
print(' Operation: CREATE');
print(' New data: ${after?.val()}');
} else if (after == null || !after.exists()) {
print(' Operation: DELETE');
print(' Deleted data: ${before.val()}');
} else {
print(' Operation: UPDATE');
print(' Before: ${before.val()}');
print(' After: ${after.val()}');
}
},
);

// Nested path database trigger
firebase.database.onValueWritten(
ref: 'users/{userId}/status',
(event) async {
final after = event.data?.after?.val();
print('User status changed: ${event.params['userId']}');
print(' New status: $after');
},
);

print('Functions registered successfully!');
});
}
126 changes: 126 additions & 0 deletions example/nodejs_reference/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

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

// =============================================================================
Expand Down Expand Up @@ -69,3 +71,127 @@ exports.onMessagePublished_mytopic = onMessagePublished(
console.log(" Attributes:", message.attributes);
}
);

// =============================================================================
// Firestore trigger examples
// =============================================================================

exports.onDocumentCreated_users_userId = onDocumentCreated(
"users/{userId}",
(event) => {
const data = event.data?.data();
console.log("Document created: users/" + event.params.userId);
console.log(" Name:", data?.name);
console.log(" Email:", data?.email);
}
);

exports.onDocumentUpdated_users_userId = onDocumentUpdated(
"users/{userId}",
(event) => {
const before = event.data?.before?.data();
const after = event.data?.after?.data();
console.log("Document updated: users/" + event.params.userId);
console.log(" Before:", before);
console.log(" After:", after);
}
);

exports.onDocumentDeleted_users_userId = onDocumentDeleted(
"users/{userId}",
(event) => {
const data = event.data?.data();
console.log("Document deleted: users/" + event.params.userId);
console.log(" Final data:", data);
}
);

exports.onDocumentWritten_users_userId = onDocumentWritten(
"users/{userId}",
(event) => {
const before = event.data?.before?.data();
const after = event.data?.after?.data();
console.log("Document written: users/" + event.params.userId);
if (!before && after) {
console.log(" Operation: CREATE");
} else if (before && after) {
console.log(" Operation: UPDATE");
} else if (before && !after) {
console.log(" Operation: DELETE");
}
}
);

exports.onDocumentCreated_posts_postId_comments_commentId = onDocumentCreated(
"posts/{postId}/comments/{commentId}",
(event) => {
const data = event.data?.data();
console.log("Comment created: posts/" + event.params.postId + "/comments/" + event.params.commentId);
console.log(" Text:", data?.text);
console.log(" Author:", data?.author);
}
);

// =============================================================================
// Realtime Database trigger examples
// =============================================================================

exports.onValueCreated_messages_messageId = onValueCreated(
"/messages/{messageId}",
(event) => {
const data = event.data?.val();
console.log("Database value created: messages/" + event.params.messageId);
console.log(" Data:", data);
console.log(" Instance:", event.instance);
console.log(" Ref:", event.ref);
}
);

exports.onValueUpdated_messages_messageId = onValueUpdated(
"/messages/{messageId}",
(event) => {
const before = event.data?.before?.val();
const after = event.data?.after?.val();
console.log("Database value updated: messages/" + event.params.messageId);
console.log(" Before:", before);
console.log(" After:", after);
}
);

exports.onValueDeleted_messages_messageId = onValueDeleted(
"/messages/{messageId}",
(event) => {
const data = event.data?.val();
console.log("Database value deleted: messages/" + event.params.messageId);
console.log(" Final data:", data);
}
);

exports.onValueWritten_messages_messageId = onValueWritten(
"/messages/{messageId}",
(event) => {
const before = event.data?.before;
const after = event.data?.after;
console.log("Database value written: messages/" + event.params.messageId);
if (!before?.exists() && after?.exists()) {
console.log(" Operation: CREATE");
console.log(" New data:", after.val());
} else if (before?.exists() && !after?.exists()) {
console.log(" Operation: DELETE");
console.log(" Deleted data:", before.val());
} else {
console.log(" Operation: UPDATE");
console.log(" Before:", before?.val());
console.log(" After:", after?.val());
}
}
);

exports.onValueWritten_users_userId_status = onValueWritten(
"/users/{userId}/status",
(event) => {
const after = event.data?.after?.val();
console.log("User status changed:", event.params.userId);
console.log(" New status:", after);
}
);
95 changes: 94 additions & 1 deletion lib/builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ class _TypeCheckers {
static final pubsubNamespace = TypeChecker.fromRuntime(ff.PubSubNamespace);
static final firestoreNamespace =
TypeChecker.fromRuntime(ff.FirestoreNamespace);
static final databaseNamespace =
TypeChecker.fromRuntime(ff.DatabaseNamespace);
}

/// The main builder that generates functions.yaml.
Expand Down Expand Up @@ -132,6 +134,16 @@ class _FirebaseFunctionsVisitor extends RecursiveAstVisitor<void> {
}
}

// Check for Database function declarations
if (target != null && _isDatabaseNamespace(target)) {
if (methodName == 'onValueCreated' ||
methodName == 'onValueUpdated' ||
methodName == 'onValueDeleted' ||
methodName == 'onValueWritten') {
_extractDatabaseFunction(node, methodName);
}
}

// Check for parameter definitions (top-level function calls with no target)
if (target == null && _isParamDefinition(methodName)) {
_extractParameterFromMethod(node, methodName);
Expand Down Expand Up @@ -236,6 +248,13 @@ class _FirebaseFunctionsVisitor extends RecursiveAstVisitor<void> {
return _TypeCheckers.firestoreNamespace.isExactlyType(staticType);
}

/// Checks if the target is firebase.database.
bool _isDatabaseNamespace(Expression target) {
final staticType = target.staticType;
if (staticType == null) return false;
return _TypeCheckers.databaseNamespace.isExactlyType(staticType);
}

/// Checks if this is a parameter definition function.
bool _isParamDefinition(String name) =>
name == 'defineString' ||
Expand Down Expand Up @@ -336,6 +355,44 @@ class _FirebaseFunctionsVisitor extends RecursiveAstVisitor<void> {
);
}

/// Extracts a Database function declaration.
void _extractDatabaseFunction(MethodInvocation node, String methodName) {
// Extract ref path from named argument
final refArg = _findNamedArg(node, 'ref');
if (refArg == null) return;

final refPath = _extractStringLiteral(refArg);
if (refPath == null) return;

// Extract options if present (for instance)
final optionsArg = _findNamedArg(node, 'options');
String? instance;

if (optionsArg is InstanceCreationExpression) {
instance = _extractStringField(optionsArg, 'instance');
}

// Generate function name from ref path and event type
// Similar to how we do it in database_namespace.dart
final sanitizedPath = refPath
.replaceAll(RegExp(r'^/+|/+$'), '') // Remove leading/trailing slashes
.replaceAll('/', '_')
.replaceAll('{', '')
.replaceAll('}', '')
.replaceAll('-', '');
final functionName = '${methodName}_$sanitizedPath';

endpoints[functionName] = _EndpointSpec(
name: functionName,
type: 'database',
databaseEventType: methodName,
refPath: refPath,
instance: instance ?? '*',
options: optionsArg is InstanceCreationExpression ? optionsArg : null,
variableToParamName: _variableToParamName,
);
}

/// Extracts a parameter definition from FunctionExpressionInvocation.
void _extractParameter(
FunctionExpressionInvocation node,
Expand Down Expand Up @@ -538,16 +595,22 @@ class _EndpointSpec {
this.documentPath,
this.database,
this.namespace,
this.databaseEventType,
this.refPath,
this.instance,
this.options,
this.variableToParamName = const {},
});
final String name;
final String type; // 'https', 'callable', 'pubsub', 'firestore'
final String type; // 'https', 'callable', 'pubsub', 'firestore', 'database'
final String? topic; // For Pub/Sub functions
final String? firestoreEventType; // For Firestore: onDocumentCreated, etc.
final String? documentPath; // For Firestore: users/{userId}
final String? database; // For Firestore: (default) or database name
final String? namespace; // For Firestore: (default) or namespace
final String? databaseEventType; // For Database: onValueCreated, etc.
final String? refPath; // For Database: /users/{userId}
final String? instance; // For Database: database instance or '*'
final InstanceCreationExpression? options;
final Map<String, String> variableToParamName;

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

buffer.writeln(' retry: false');
} else if (endpoint.type == 'database' &&
endpoint.databaseEventType != null &&
endpoint.refPath != null) {
// Map Dart method name to Database CloudEvent type
final eventType = _mapDatabaseEventType(endpoint.databaseEventType!);

buffer.writeln(' eventTrigger:');
buffer.writeln(' eventType: "$eventType"');
// Database triggers use empty eventFilters
buffer.writeln(' eventFilters: {}');

// Both ref and instance go in eventFilterPathPatterns
// The ref path should not have a leading slash to match Node.js format
final normalizedRef = endpoint.refPath!.startsWith('/')
? endpoint.refPath!.substring(1)
: endpoint.refPath!;
buffer.writeln(' eventFilterPathPatterns:');
buffer.writeln(' ref: "$normalizedRef"');
buffer.writeln(' instance: "${endpoint.instance ?? '*'}"');

buffer.writeln(' retry: false');
}
}
Expand All @@ -1278,6 +1362,15 @@ String _mapFirestoreEventType(String methodName) => switch (methodName) {
_ => throw ArgumentError('Unknown Firestore event type: $methodName'),
};

/// Maps Database method name to CloudEvent event type.
String _mapDatabaseEventType(String methodName) => switch (methodName) {
'onValueCreated' => 'google.firebase.database.ref.v1.created',
'onValueUpdated' => 'google.firebase.database.ref.v1.updated',
'onValueDeleted' => 'google.firebase.database.ref.v1.deleted',
'onValueWritten' => 'google.firebase.database.ref.v1.written',
_ => throw ArgumentError('Unknown Database event type: $methodName'),
};

/// Converts a value to YAML format.
String _yamlValue(dynamic value) {
if (value is String) {
Expand Down
2 changes: 2 additions & 0 deletions lib/firebase_functions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ export 'src/common/expression.dart';
export 'src/common/on_init.dart' show onInit;
export 'src/common/options.dart';
export 'src/common/params.dart';
// Database triggers
export 'src/database/database.dart';
// Core firebase instance
export 'src/firebase.dart' show Firebase;
// Firestore triggers
Expand Down
Loading
Loading