| // Copyright 2019 The Flutter Authors. 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:convert'; |
| |
| import 'package:googleapis/firestore/v1.dart' as g; |
| import 'package:meta/meta.dart'; |
| |
| /// Defines the `documentId` for a given document [T]. |
| /// |
| /// The path to a document in Firestore follows the pattern: |
| /// ```txt |
| /// /projects/<project-id>/databases/<database-id>/documents/<collection-id>/<document-id> |
| /// ``` |
| /// |
| /// This type is the `<document-id>` for a particular collection type [T]. |
| /// |
| /// ## Implementing |
| /// |
| /// There are two ways to use this type: (1) [fromDocumentId] or (2) `extends`. |
| /// |
| /// For simple cases, or for creating an ID from an existing string: |
| /// ```dart |
| /// AppDocumentId<Commit>.fromDocumentId(commitSha); |
| /// ``` |
| /// |
| /// For more complex cases, or deriving the ID from multiple distinct fields: |
| /// ```dart |
| /// final class TaskId extends AppDocumentId<Task> { |
| /// // ... fields ... |
| /// |
| /// @override |
| /// String get documentId => '$field1_$field2_$field3'; |
| /// } |
| /// ``` |
| @immutable |
| abstract base class AppDocumentId<T extends AppDocument<T>> { |
| const AppDocumentId(); |
| |
| /// Create an [AppDocumentId] from an existing [documentId]. |
| const factory AppDocumentId.fromDocumentId( |
| String documentId, { |
| required AppDocumentMetadata<T> runtimeMetadata, |
| }) = _AppDocumentId; |
| |
| /// The `<document-id>` portion of a [g.Document.name]. |
| String get documentId; |
| |
| /// Describes the document type [T] in Firestore. |
| AppDocumentMetadata<T> get runtimeMetadata; |
| |
| @override |
| @nonVirtual |
| bool operator ==(Object other) { |
| return other is AppDocumentId<T> && documentId == other.documentId; |
| } |
| |
| @override |
| @nonVirtual |
| int get hashCode => documentId.hashCode; |
| |
| @override |
| @nonVirtual |
| String toString() { |
| return 'AppDocumentId<$T>: $documentId'; |
| } |
| } |
| |
| final class _AppDocumentId<T extends AppDocument<T>> extends AppDocumentId<T> { |
| const _AppDocumentId(this.documentId, {required this.runtimeMetadata}); |
| |
| @override |
| final String documentId; |
| |
| @override |
| final AppDocumentMetadata<T> runtimeMetadata; |
| } |
| |
| /// Metadata about an [AppDocument]. |
| @immutable |
| final class AppDocumentMetadata<T extends AppDocument<T>> { |
| AppDocumentMetadata({ |
| required this.collectionId, |
| required T Function(g.Document) fromDocument, |
| }) : _fromDocument = fromDocument; |
| |
| /// The collection ID of the document type [T]. |
| final String collectionId; |
| |
| /// Creates a new instance of [T] from the provided [document]. |
| T fromDocument(g.Document document) => _fromDocument(document); |
| final T Function(g.Document) _fromDocument; |
| } |
| |
| /// Provides methods across [g.Document] sub-types in `model/firestore/*.dart`. |
| @internal |
| abstract class AppDocument<T extends AppDocument<T>> implements g.Document { |
| /// Makes a new app document with potentially a shallow clone of the [from] |
| /// document. |
| AppDocument([g.Document? from]) |
| : _fields = {...?from?.fields}, |
| name = from?.name, |
| createTime = from?.createTime, |
| updateTime = from?.updateTime; |
| |
| @override |
| Map<String, g.Value> get fields => _fields; |
| |
| @override |
| set fields(Map<String, g.Value>? fields) { |
| _fields.clear(); |
| if (fields != null) { |
| _fields.addAll(fields); |
| } |
| } |
| |
| final Map<String, g.Value> _fields; |
| |
| @override |
| String? name; |
| |
| @override |
| String? updateTime; |
| |
| @override |
| String? createTime; |
| |
| @override |
| Map<String, Object?> toJson() { |
| // Copied from [g.Document.toJson]. |
| return { |
| 'fields': fields, |
| if (createTime != null) 'createTime': createTime!, |
| if (name != null) 'name': name!, |
| if (updateTime != null) 'updateTime': updateTime!, |
| }; |
| } |
| |
| /// Returns the raw JSON representation without following [g.Document.toJson]. |
| /// |
| /// Should be used for testing only. |
| @nonVirtual |
| @visibleForTesting |
| Map<String, Object?> toJsonRaw() { |
| return _fields.map((k, v) => MapEntry(k, _valueToJson(v))); |
| } |
| |
| /// Metadata that informs other parts of the app about how to use this entity. |
| AppDocumentMetadata<T> get runtimeMetadata; |
| |
| static Object? _valueToJson(g.Value value) { |
| // Listen, I don't like this, you don't like this, but it's only used to |
| // give beautiful toString() representations for logs and testing, so you'll |
| // let it slide. |
| // |
| // Basically, toJson() does: { |
| // if (isString) 'stringValue': stringValue, |
| // if (isDouble) 'doubleValue': doubleValue, |
| // } |
| // |
| // So instead of copying that, we'll just use what they do. |
| return value.toJson().values.firstOrNull; |
| } |
| |
| @override |
| @nonVirtual |
| String toString() { |
| return '$runtimeType ${const JsonEncoder.withIndent(' ').convert(toJsonRaw())}'; |
| } |
| } |