diff --git a/pkgs/googleapis_firestore_v1/README.md b/pkgs/googleapis_firestore_v1/README.md index b5f2199d..e694e242 100644 --- a/pkgs/googleapis_firestore_v1/README.md +++ b/pkgs/googleapis_firestore_v1/README.md @@ -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 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 diff --git a/pkgs/googleapis_firestore_v1/analysis_options.yaml b/pkgs/googleapis_firestore_v1/analysis_options.yaml index 8df38276..572dd239 100644 --- a/pkgs/googleapis_firestore_v1/analysis_options.yaml +++ b/pkgs/googleapis_firestore_v1/analysis_options.yaml @@ -1,6 +1 @@ include: package:lints/recommended.yaml - -analyzer: - errors: - # TODO: Remove once google/protobuf.dart/pull/1000 is published. - implementation_imports: ignore diff --git a/pkgs/googleapis_firestore_v1/example/example.dart b/pkgs/googleapis_firestore_v1/example/example.dart new file mode 100644 index 00000000..9885b8d9 --- /dev/null +++ b/pkgs/googleapis_firestore_v1/example/example.dart @@ -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 args) async { + final commandRunner = createCommandRunner(); + final result = await commandRunner.run(args); + exit(result ?? 0); +} + +CommandRunner createCommandRunner() { + final runner = CommandRunner( + '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 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 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} '; + + @override + Future 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 runCommand(FirestoreClient firestore) async { + print('Listening for changes to demo.item field (hit ctrl-c to stop).'); + print(''); + + final StreamController 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 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', + }, + ), + ); + + 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 { + String get projectId => globalResults!.option('project')!; + + String get databaseId => 'projects/$projectId/databases/(default)'; + + String get documentsPath => '$databaseId/documents'; + + @override + Future 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 runCommand(FirestoreClient firestore); +} diff --git a/pkgs/googleapis_firestore_v1/pubspec.yaml b/pkgs/googleapis_firestore_v1/pubspec.yaml index 8b439c70..743aa261 100644 --- a/pkgs/googleapis_firestore_v1/pubspec.yaml +++ b/pkgs/googleapis_firestore_v1/pubspec.yaml @@ -4,7 +4,7 @@ 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 dependencies: fixnum: ^1.1.0 @@ -12,6 +12,7 @@ dependencies: 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.