Skip to content

Commit 6e8db70

Browse files
committed
README update
1 parent 8a4312b commit 6e8db70

File tree

1 file changed

+335
-23
lines changed

1 file changed

+335
-23
lines changed

README.md

Lines changed: 335 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,26 @@ Write Firebase Cloud Functions in Dart with full type safety and performance.
77

88
## Status: Alpha (v0.1.0)
99

10-
This package is in active development. Phase 1 includes:
11-
- ✅ HTTPS triggers (onRequest, onCall, onCallWithData)
12-
- ✅ Pub/Sub triggers (onMessagePublished)
13-
- ✅ CloudEvent foundation
14-
- ✅ Build-time code generation
10+
This package provides a complete Dart implementation of Firebase Cloud Functions with support for:
11+
12+
| Trigger Type | Status | Functions |
13+
|-------------|--------|-----------|
14+
| **HTTPS** | ✅ Complete | `onRequest`, `onCall`, `onCallWithData` |
15+
| **Pub/Sub** | ✅ Complete | `onMessagePublished` |
16+
| **Firestore** | ✅ Complete | `onDocumentCreated`, `onDocumentUpdated`, `onDocumentDeleted`, `onDocumentWritten` |
17+
| **Realtime Database** | ✅ Complete | `onValueCreated`, `onValueUpdated`, `onValueDeleted`, `onValueWritten` |
18+
| **Firebase Alerts** | ✅ Complete | Crashlytics, Billing, Performance alerts |
19+
| **Identity Platform** | ✅ Complete | `beforeUserCreated`, `beforeUserSignedIn` (+ `beforeEmailSent`, `beforeSmsSent`*) |
1520

1621
## Features
1722

18-
- **Type-safe**: Leverage Dart's strong type system
23+
- **Type-safe**: Leverage Dart's strong type system with typed callable functions and CloudEvents
1924
- **Fast**: Compiled Dart code with efficient Shelf HTTP server
20-
- **Familiar API**: Similar to Firebase Functions Node.js SDK
21-
- **Testing**: Built with testing in mind
25+
- **Familiar API**: Similar to Firebase Functions Node.js SDK v2
26+
- **Streaming**: Server-Sent Events (SSE) support for callable functions
27+
- **Parameterized**: Deploy-time configuration with `defineString`, `defineInt`, `defineBoolean`
28+
- **Conditional Config**: CEL expressions for environment-based options
29+
- **Error Handling**: Built-in typed error classes matching the Node.js SDK
2230
- **Hot Reload**: Fast development with build_runner watch
2331

2432
## Installation
@@ -33,45 +41,349 @@ dependencies:
3341
3442
## Quick Start
3543
36-
### HTTPS Function
37-
3844
```dart
3945
import 'package:firebase_functions/firebase_functions.dart';
40-
import 'package:shelf/shelf.dart';
4146

4247
void main(List<String> args) {
4348
fireUp(args, (firebase) {
44-
firebase.https.onRequest(
45-
name: 'hello',
46-
(request) async {
47-
return Response.ok('Hello from Dart!');
48-
},
49-
);
49+
// Register your functions here
5050
});
5151
}
5252
```
5353

54-
### Callable Function
54+
## HTTPS Functions
55+
56+
### onRequest - Raw HTTP Handler
57+
58+
```dart
59+
firebase.https.onRequest(
60+
name: 'hello',
61+
(request) async {
62+
return Response.ok('Hello from Dart!');
63+
},
64+
);
65+
```
66+
67+
### onCall - Untyped Callable
5568

5669
```dart
5770
firebase.https.onCall(
5871
name: 'greet',
5972
(request, response) async {
60-
final name = request.data['name'] as String;
61-
return CallableResult({'message': 'Hello $name!'});
73+
final data = request.data as Map<String, dynamic>?;
74+
final name = data?['name'] ?? 'World';
75+
return CallableResult({'message': 'Hello, $name!'});
76+
},
77+
);
78+
```
79+
80+
### onCallWithData - Type-safe Callable
81+
82+
```dart
83+
firebase.https.onCallWithData<GreetRequest, GreetResponse>(
84+
name: 'greetTyped',
85+
fromJson: GreetRequest.fromJson,
86+
(request, response) async {
87+
return GreetResponse(message: 'Hello, ${request.data.name}!');
88+
},
89+
);
90+
```
91+
92+
### Streaming Support
93+
94+
```dart
95+
firebase.https.onCall(
96+
name: 'countdown',
97+
options: const CallableOptions(
98+
heartBeatIntervalSeconds: HeartBeatIntervalSeconds(5),
99+
),
100+
(request, response) async {
101+
if (request.acceptsStreaming) {
102+
for (var i = 10; i >= 0; i--) {
103+
await response.sendChunk({'count': i});
104+
await Future.delayed(Duration(milliseconds: 100));
105+
}
106+
}
107+
return CallableResult({'message': 'Countdown complete!'});
108+
},
109+
);
110+
```
111+
112+
### Error Handling
113+
114+
```dart
115+
firebase.https.onCall(
116+
name: 'divide',
117+
(request, response) async {
118+
final data = request.data as Map<String, dynamic>?;
119+
final a = data?['a'] as num?;
120+
final b = data?['b'] as num?;
121+
122+
if (a == null || b == null) {
123+
throw InvalidArgumentError('Both "a" and "b" are required');
124+
}
125+
if (b == 0) {
126+
throw FailedPreconditionError('Cannot divide by zero');
127+
}
128+
129+
return CallableResult({'result': a / b});
62130
},
63131
);
64132
```
65133

66-
### Pub/Sub Trigger
134+
Available error types: `InvalidArgumentError`, `FailedPreconditionError`, `NotFoundError`, `AlreadyExistsError`, `PermissionDeniedError`, `ResourceExhaustedError`, `UnauthenticatedError`, `UnavailableError`, `InternalError`, `DeadlineExceededError`, `CancelledError`.
135+
136+
## Pub/Sub Triggers
67137

68138
```dart
69139
firebase.pubsub.onMessagePublished(
70140
topic: 'my-topic',
71141
(event) async {
72142
final message = event.data;
73-
print('Received: ${message.textData}');
74-
print('Attributes: ${message.attributes}');
143+
print('ID: ${message?.messageId}');
144+
print('Data: ${message?.textData}');
145+
print('Attributes: ${message?.attributes}');
146+
},
147+
);
148+
```
149+
150+
## Firestore Triggers
151+
152+
```dart
153+
// Document created
154+
firebase.firestore.onDocumentCreated(
155+
document: 'users/{userId}',
156+
(event) async {
157+
final data = event.data?.data();
158+
print('Created: users/${event.params['userId']}');
159+
print('Name: ${data?['name']}');
160+
},
161+
);
162+
163+
// Document updated
164+
firebase.firestore.onDocumentUpdated(
165+
document: 'users/{userId}',
166+
(event) async {
167+
final before = event.data?.before?.data();
168+
final after = event.data?.after?.data();
169+
print('Before: $before');
170+
print('After: $after');
171+
},
172+
);
173+
174+
// Document deleted
175+
firebase.firestore.onDocumentDeleted(
176+
document: 'users/{userId}',
177+
(event) async {
178+
final data = event.data?.data();
179+
print('Deleted data: $data');
180+
},
181+
);
182+
183+
// All write operations
184+
firebase.firestore.onDocumentWritten(
185+
document: 'users/{userId}',
186+
(event) async {
187+
final before = event.data?.before?.data();
188+
final after = event.data?.after?.data();
189+
// Determine operation type
190+
if (before == null && after != null) print('CREATE');
191+
if (before != null && after != null) print('UPDATE');
192+
if (before != null && after == null) print('DELETE');
193+
},
194+
);
195+
196+
// Nested collections
197+
firebase.firestore.onDocumentCreated(
198+
document: 'posts/{postId}/comments/{commentId}',
199+
(event) async {
200+
print('Post: ${event.params['postId']}');
201+
print('Comment: ${event.params['commentId']}');
202+
},
203+
);
204+
```
205+
206+
## Realtime Database Triggers
207+
208+
```dart
209+
// Value created
210+
firebase.database.onValueCreated(
211+
ref: 'messages/{messageId}',
212+
(event) async {
213+
final data = event.data?.val();
214+
print('Created: ${event.params['messageId']}');
215+
print('Data: $data');
216+
print('Instance: ${event.instance}');
217+
},
218+
);
219+
220+
// Value updated
221+
firebase.database.onValueUpdated(
222+
ref: 'messages/{messageId}',
223+
(event) async {
224+
final before = event.data?.before?.val();
225+
final after = event.data?.after?.val();
226+
print('Before: $before');
227+
print('After: $after');
228+
},
229+
);
230+
231+
// Value deleted
232+
firebase.database.onValueDeleted(
233+
ref: 'messages/{messageId}',
234+
(event) async {
235+
final data = event.data?.val();
236+
print('Deleted: $data');
237+
},
238+
);
239+
240+
// All write operations
241+
firebase.database.onValueWritten(
242+
ref: 'users/{userId}/status',
243+
(event) async {
244+
final before = event.data?.before;
245+
final after = event.data?.after;
246+
if (before == null || !before.exists()) print('CREATE');
247+
else if (after == null || !after.exists()) print('DELETE');
248+
else print('UPDATE');
249+
},
250+
);
251+
```
252+
253+
## Firebase Alerts
254+
255+
```dart
256+
// Crashlytics fatal issues
257+
firebase.alerts.crashlytics.onNewFatalIssuePublished(
258+
(event) async {
259+
final issue = event.data?.payload.issue;
260+
print('Issue: ${issue?.title}');
261+
print('App: ${event.appId}');
262+
},
263+
);
264+
265+
// Billing plan updates
266+
firebase.alerts.billing.onPlanUpdatePublished(
267+
(event) async {
268+
final payload = event.data?.payload;
269+
print('New Plan: ${payload?.billingPlan}');
270+
print('Updated By: ${payload?.principalEmail}');
271+
},
272+
);
273+
274+
// Performance threshold alerts
275+
firebase.alerts.performance.onThresholdAlertPublished(
276+
options: const AlertOptions(appId: '1:123456789:ios:abcdef'),
277+
(event) async {
278+
final payload = event.data?.payload;
279+
print('Metric: ${payload?.metricType}');
280+
print('Threshold: ${payload?.thresholdValue}');
281+
print('Actual: ${payload?.violationValue}');
282+
},
283+
);
284+
```
285+
286+
## Identity Platform (Auth Blocking)
287+
288+
```dart
289+
// Before user created
290+
firebase.identity.beforeUserCreated(
291+
options: const BlockingOptions(idToken: true, accessToken: true),
292+
(AuthBlockingEvent event) async {
293+
final user = event.data;
294+
295+
// Block certain email domains
296+
if (user?.email?.endsWith('@blocked.com') ?? false) {
297+
throw PermissionDeniedError('Email domain not allowed');
298+
}
299+
300+
// Set custom claims
301+
if (user?.email?.endsWith('@admin.com') ?? false) {
302+
return const BeforeCreateResponse(
303+
customClaims: {'admin': true},
304+
);
305+
}
306+
307+
return null;
308+
},
309+
);
310+
311+
// Before user signed in
312+
firebase.identity.beforeUserSignedIn(
313+
options: const BlockingOptions(idToken: true),
314+
(AuthBlockingEvent event) async {
315+
return BeforeSignInResponse(
316+
sessionClaims: {
317+
'lastLogin': DateTime.now().toIso8601String(),
318+
'signInIp': event.ipAddress,
319+
},
320+
);
321+
},
322+
);
323+
```
324+
325+
> **Note**: `beforeEmailSent` and `beforeSmsSent` are also available but cannot be tested with the Firebase Auth emulator (emulator only supports `beforeUserCreated` and `beforeUserSignedIn`). They work in production deployments.
326+
327+
## Parameters & Configuration
328+
329+
### Defining Parameters
330+
331+
```dart
332+
final welcomeMessage = defineString(
333+
'WELCOME_MESSAGE',
334+
ParamOptions(
335+
defaultValue: 'Hello from Dart!',
336+
label: 'Welcome Message',
337+
description: 'The greeting message returned by the function',
338+
),
339+
);
340+
341+
final minInstances = defineInt(
342+
'MIN_INSTANCES',
343+
ParamOptions(defaultValue: 0),
344+
);
345+
346+
final isProduction = defineBoolean(
347+
'IS_PRODUCTION',
348+
ParamOptions(defaultValue: false),
349+
);
350+
```
351+
352+
### Using Parameters at Runtime
353+
354+
```dart
355+
firebase.https.onRequest(
356+
name: 'hello',
357+
(request) async {
358+
return Response.ok(welcomeMessage.value());
359+
},
360+
);
361+
```
362+
363+
### Using Parameters in Options (Deploy-time)
364+
365+
```dart
366+
firebase.https.onRequest(
367+
name: 'configured',
368+
options: HttpsOptions(
369+
minInstances: DeployOption.param(minInstances),
370+
),
371+
handler,
372+
);
373+
```
374+
375+
### Conditional Configuration
376+
377+
```dart
378+
firebase.https.onRequest(
379+
name: 'api',
380+
options: HttpsOptions(
381+
// 2GB in production, 512MB in development
382+
memory: Memory.expression(isProduction.thenElse(2048, 512)),
383+
),
384+
(request) async {
385+
final env = isProduction.value() ? 'production' : 'development';
386+
return Response.ok('Running in $env mode');
75387
},
76388
);
77389
```

0 commit comments

Comments
 (0)