Skip to content

add an example use of firestore #240

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 11, 2025
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
35 changes: 35 additions & 0 deletions pkgs/googleapis_firestore_v1/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,41 @@ See also:

This package is a generated gRPC client used to access the Firestore API.

## Example

See below for a hello-world example. For a more complete example, including
use of `firestore.listen()`, see
https://github.com/dart-lang/labs/blob/main/pkgs/googleapis_firestore_v1/example/example.dart.

```dart
import 'package:googleapis_firestore_v1/google/firestore/v1/firestore.pbgrpc.dart';
import 'package:grpc/grpc.dart' as grpc;

void main(List<String> args) async {
final projectId = args[0];

// set up a connection
final channel = grpc.ClientChannel(FirestoreClient.defaultHost);
final auth = await grpc.applicationDefaultCredentialsAuthenticator(
FirestoreClient.oauthScopes,
);
final firestore = FirestoreClient(channel, options: auth.toCallOptions);

// make a request
final request = ListCollectionIdsRequest(
parent: 'projects/$projectId/databases/(default)/documents',
);
final result = await firestore.listCollectionIds(request);
print('collectionIds:');
for (var collectionId in result.collectionIds) {
print('- $collectionId');
}

// close the channel
await channel.shutdown();
}
```

## Status: Experimental

**NOTE**: This package is currently experimental and published under the
Expand Down
5 changes: 0 additions & 5 deletions pkgs/googleapis_firestore_v1/analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1 @@
include: package:lints/recommended.yaml

analyzer:
errors:
# TODO: Remove once google/protobuf.dart/pull/1000 is published.
implementation_imports: ignore
221 changes: 221 additions & 0 deletions pkgs/googleapis_firestore_v1/example/example.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:async';
import 'dart:io';

import 'package:args/command_runner.dart';
import 'package:googleapis_firestore_v1/google/firestore/v1/common.pb.dart';
import 'package:googleapis_firestore_v1/google/firestore/v1/document.pb.dart';
import 'package:googleapis_firestore_v1/google/firestore/v1/firestore.pbgrpc.dart';
import 'package:grpc/grpc.dart' as grpc;

void main(List<String> args) async {
final commandRunner = createCommandRunner();
final result = await commandRunner.run(args);
exit(result ?? 0);
}

CommandRunner<int> createCommandRunner() {
final runner = CommandRunner<int>(
'firestore',
'A sample app exercising the Firestore APIs',
)..argParser.addOption(
'project',
valueHelp: 'projectId',
help: 'The projectId to use when querying a firestore database.',
);

runner.addCommand(ListCommand());
runner.addCommand(PeekCommand());
runner.addCommand(PokeCommand());
runner.addCommand(ListenCommand());

return runner;
}

class ListCommand extends FirestoreCommand {
@override
final String name = 'list';

@override
final String description = 'List collections for the given project.';

@override
Future<int> runCommand(FirestoreClient firestore) async {
final request = ListCollectionIdsRequest(parent: documentsPath);
print('Listing collections for ${request.parent}…');
final result = await firestore.listCollectionIds(request);
print('');
for (var collectionId in result.collectionIds) {
print('- $collectionId');
}
print('');
print('${result.collectionIds.length} collections.');
return 0;
}
}

class PeekCommand extends FirestoreCommand {
@override
final String name = 'peek';

@override
final String description =
"Print the value of collection 'demo', document 'demo', field 'item'.";

@override
Future<int> runCommand(FirestoreClient firestore) async {
final request = GetDocumentRequest(name: '$documentsPath/demo/demo');
print('reading ${request.name}…');
final document = await firestore.getDocument(request);
final value = document.fields['item'];
if (value == null) {
print("No value for document field 'item'.");
} else {
print("value: '${value.stringValue}'");
}
final updateTime = document.updateTime.toDateTime();
print('updated: $updateTime');

return 0;
}
}

class PokeCommand extends FirestoreCommand {
@override
final String name = 'poke';

@override
final String description =
"Set the value of collection 'demo', document 'demo', field 'item'.";

@override
String get invocation => '${super.invocation} <new value>';

@override
Future<int> runCommand(FirestoreClient firestore) async {
final rest = argResults!.rest;
if (rest.isEmpty) {
print(usage);
return 1;
}

final newValue = rest.first;
print("Updating demo/demo.item to '$newValue'…");

final updatedDocument = Document(
name: '$documentsPath/demo/demo',
fields: [
MapEntry('item', Value(stringValue: newValue)),
],
);
await firestore.updateDocument(
UpdateDocumentRequest(
document: updatedDocument,
updateMask: DocumentMask(fieldPaths: ['item']),
),
);

return 0;
}
}

class ListenCommand extends FirestoreCommand {
@override
final String name = 'listen';

@override
final String description =
"Listen for changes to the 'item' field of document 'demo' in "
"collection 'demo'.";

@override
Future<int> runCommand(FirestoreClient firestore) async {
print('Listening for changes to demo.item field (hit ctrl-c to stop).');
print('');

final StreamController<ListenRequest> requestSink = StreamController();

// Listen for 'ctrl-c' and close the firestore.listen stream.
ProcessSignal.sigint.watch().listen((_) => requestSink.close());

final ListenRequest listenRequest = ListenRequest(
database: databaseId,
addTarget: Target(
documents: Target_DocumentsTarget(
documents: [
'$documentsPath/demo/demo',
],
),
targetId: 1,
),
);

final Stream<ListenResponse> stream = firestore.listen(
requestSink.stream,
// TODO(devoncarew): We add this routing header because our protoc grpc
// generator doesn't yet parse the http proto annotations.
options: grpc.CallOptions(
metadata: {
'x-goog-request-params': 'database=$databaseId',
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll want to replace this manual routing header w/ improvements to the grpc / protobuf generator so that it adds the necessary headers.

},
),
);

requestSink.add(listenRequest);

final streamClosed = Completer();

stream.listen(
(event) {
print('==============');
print('documentChange: ${event.documentChange.toString().trim()}');
print('==============');
print('');
},
onError: (e) {
print('\nError listening to document changes: $e');
streamClosed.complete();
},
onDone: () {
print('\nListening stream done.');
streamClosed.complete();
},
);

await streamClosed.future;

return 0;
}
}

abstract class FirestoreCommand extends Command<int> {
String get projectId => globalResults!.option('project')!;

String get databaseId => 'projects/$projectId/databases/(default)';

String get documentsPath => '$databaseId/documents';

@override
Future<int> run() async {
final projectId = globalResults!.option('project');
if (projectId == null) {
print('A --project option is required.');
return 1;
}

final channel = grpc.ClientChannel(FirestoreClient.defaultHost);
final auth = await grpc.applicationDefaultCredentialsAuthenticator(
FirestoreClient.oauthScopes,
);

final firestore = FirestoreClient(channel, options: auth.toCallOptions);
final result = await runCommand(firestore);
await channel.shutdown();
return result;
}

Future<int> runCommand(FirestoreClient firestore);
}
3 changes: 2 additions & 1 deletion pkgs/googleapis_firestore_v1/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ version: 0.1.0-wip
repository: https://github.com/dart-lang/labs/tree/main/pkgs/googleapis_firestore_v1

environment:
sdk: ^3.7.0
sdk: ^3.6.0
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason for downgrading?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good question; I actually forget. perhaps to retain the current (pre-3.7) formatting?

I'll keep this as is and roll separately w/ a general proto-regenerate PR.


dependencies:
fixnum: ^1.1.0
grpc: ^4.0.0
protobuf: ^4.1.0

dev_dependencies:
args: ^2.7.0
lints: ^6.0.0
path: ^1.9.0
# We use a pinned version to make the gRPC library generation hermetic.
Expand Down