| // Copyright (c) 2014, 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. |
| |
| /// This library provides a low-level API for accessing Google's Cloud |
| /// Datastore. |
| /// |
| /// For more information on Cloud Datastore, please refer to the following |
| /// developers page: https://cloud.google.com/datastore/docs |
| library; |
| |
| import 'dart:async'; |
| |
| import 'package:http/http.dart' as http; |
| import 'package:retry/retry.dart'; |
| |
| import 'common.dart' show Page; |
| import 'service_scope.dart' as ss; |
| import 'src/datastore_impl.dart' show DatastoreImpl; |
| import 'src/retry_datastore_impl.dart'; |
| |
| const Symbol _datastoreKey = #gcloud.datastore; |
| |
| /// Access the [Datastore] object available in the current service scope. |
| /// |
| /// The returned object will be the one which was previously registered with |
| /// [registerDatastoreService] within the current (or a parent) service scope. |
| /// |
| /// Accessing this getter outside of a service scope will result in an error. |
| /// See the `package:gcloud/service_scope.dart` library for more information. |
| Datastore get datastoreService => ss.lookup(_datastoreKey) as Datastore; |
| |
| /// Registers the [Datastore] object within the current service scope. |
| /// |
| /// The provided `datastore` object will be available via the top-level |
| /// `datastore` getter. |
| /// |
| /// Calling this function outside of a service scope will result in an error. |
| /// Calling this function more than once inside the same service scope is not |
| /// allowed. |
| void registerDatastoreService(Datastore datastore) { |
| ss.register(_datastoreKey, datastore); |
| } |
| |
| class ApplicationError implements Exception { |
| final String message; |
| ApplicationError(this.message); |
| |
| @override |
| String toString() => 'ApplicationError: $message'; |
| } |
| |
| class DatastoreError implements Exception { |
| final String message; |
| |
| DatastoreError([String? message]) |
| : message = message ?? 'DatastoreError: An unknown error occurred'; |
| |
| @override |
| String toString() => message; |
| } |
| |
| class UnknownDatastoreError extends DatastoreError { |
| UnknownDatastoreError(error) : super('An unknown error occurred ($error).'); |
| } |
| |
| class TransactionAbortedError extends DatastoreError { |
| TransactionAbortedError() : super('The transaction was aborted.'); |
| } |
| |
| class TimeoutError extends DatastoreError { |
| TimeoutError() : super('The operation timed out.'); |
| } |
| |
| /// Thrown when a query would require an index which was not set. |
| /// |
| /// An application needs to specify indices in a `index.yaml` file and needs to |
| /// create indices using the `gcloud preview datastore create-indexes` command. |
| class NeedIndexError extends DatastoreError { |
| NeedIndexError() : super('An index is needed for the query to succeed.'); |
| } |
| |
| class PermissionDeniedError extends DatastoreError { |
| PermissionDeniedError() : super('Permission denied.'); |
| } |
| |
| class InternalError extends DatastoreError { |
| InternalError() : super('Internal service error.'); |
| } |
| |
| class QuotaExceededError extends DatastoreError { |
| QuotaExceededError(error) : super('Quota was exceeded ($error).'); |
| } |
| |
| /// A datastore Entity |
| /// |
| /// An entity is identified by a unique `key` and consists of a number of |
| /// `properties`. If a property should not be indexed, it needs to be included |
| /// in the `unIndexedProperties` set. |
| /// |
| /// The `properties` field maps names to values. Values can be of a primitive |
| /// type or of a composed type. |
| /// |
| /// The following primitive types are supported: |
| /// bool, int, double, String, DateTime, BlobValue, Key |
| /// |
| /// It is possible to have a `List` of values. The values must be primitive. |
| /// Lists inside lists are not supported. |
| /// |
| /// Whether a property is indexed or not applies to all values (this is only |
| /// relevant if the value is a list of primitive values). |
| class Entity { |
| final Key key; |
| final Map<String, Object?> properties; |
| final Set<String> unIndexedProperties; |
| |
| Entity(this.key, this.properties, |
| {this.unIndexedProperties = const <String>{}}); |
| } |
| |
| /// A complete or partial key. |
| /// |
| /// A key can uniquely identify a datastore `Entity`s. It consists of a |
| /// partition and path. The path consists of one or more `KeyElement`s. |
| /// |
| /// A key may be incomplete. This is useful when inserting `Entity`s which IDs |
| /// should be automatically allocated. |
| /// |
| /// Example of a fully populated [Key]: |
| /// |
| /// var fullKey = new Key([new KeyElement('Person', 1), |
| /// new KeyElement('Address', 2)]); |
| /// |
| /// Example of a partially populated [Key] / an incomplete [Key]: |
| /// |
| /// var partialKey = new Key([new KeyElement('Person', 1), |
| /// new KeyElement('Address', null)]); |
| class Key { |
| /// The partition of this `Key`. |
| final Partition partition; |
| |
| /// The path of `KeyElement`s. |
| final List<KeyElement> elements; |
| |
| Key(this.elements, {this.partition = Partition.DEFAULT}); |
| |
| factory Key.fromParent(String kind, int id, {Key? parent}) { |
| var partition = Partition.DEFAULT; |
| var elements = <KeyElement>[]; |
| if (parent != null) { |
| partition = parent.partition; |
| elements.addAll(parent.elements); |
| } |
| elements.add(KeyElement(kind, id)); |
| return Key(elements, partition: partition); |
| } |
| |
| @override |
| int get hashCode => |
| elements.fold(partition.hashCode, (a, b) => a ^ b.hashCode); |
| |
| @override |
| bool operator ==(Object other) { |
| if (identical(this, other)) return true; |
| |
| if (other is Key && |
| partition == other.partition && |
| elements.length == other.elements.length) { |
| for (var i = 0; i < elements.length; i++) { |
| if (elements[i] != other.elements[i]) return false; |
| } |
| return true; |
| } |
| return false; |
| } |
| |
| @override |
| String toString() { |
| var namespaceString = |
| partition.namespace == null ? 'null' : "'${partition.namespace}'"; |
| return "Key(namespace=$namespaceString, path=[${elements.join(', ')}])"; |
| } |
| } |
| |
| /// A datastore partition. |
| /// |
| /// A partition is used for partitioning a dataset into multiple namespaces. |
| /// The default namespace is `null`. Using empty Strings as namespaces is |
| /// invalid. |
| /// |
| // TODO(Issue #6): Add dataset-id here. |
| class Partition { |
| // ignore: constant_identifier_names |
| static const Partition DEFAULT = Partition._default(); |
| |
| /// The namespace of this partition. |
| /// |
| /// The default namespace is `null`. |
| final String? namespace; |
| |
| Partition(this.namespace) { |
| if (namespace == '') { |
| throw ArgumentError("'namespace' must not be empty"); |
| } |
| } |
| |
| const Partition._default() : namespace = null; |
| |
| @override |
| int get hashCode => namespace.hashCode; |
| |
| @override |
| bool operator ==(Object other) => |
| other is Partition && namespace == other.namespace; |
| } |
| |
| /// An element in a `Key`s path. |
| class KeyElement { |
| /// The kind of this element. |
| final String kind; |
| |
| /// The ID of this element. It must be either an `int` or a `String. |
| /// |
| /// This may be `null`, in which case it does not identify an Entity. It is |
| /// possible to insert [Entity]s with incomplete keys and let Datastore |
| /// automatically select a unused integer ID. |
| final dynamic id; |
| |
| KeyElement(this.kind, this.id) { |
| if (id != null) { |
| if (id is! int && id is! String) { |
| throw ArgumentError("'id' must be either null, a String or an int"); |
| } |
| } |
| } |
| |
| @override |
| int get hashCode => kind.hashCode ^ id.hashCode; |
| |
| @override |
| bool operator ==(Object other) => |
| other is KeyElement && kind == other.kind && id == other.id; |
| |
| @override |
| String toString() => '$kind.$id'; |
| } |
| |
| /// A relation used in query filters. |
| class FilterRelation { |
| // ignore: constant_identifier_names |
| static const FilterRelation LessThan = FilterRelation._('<'); |
| // ignore: constant_identifier_names |
| static const FilterRelation LessThanOrEqual = FilterRelation._('<='); |
| // ignore: constant_identifier_names |
| static const FilterRelation GreaterThan = FilterRelation._('>'); |
| |
| /// Old misspelled name for [GreaterThan], retained for compatibility. |
| /// |
| /// @nodoc |
| @Deprecated('Use FilterRelation.GreaterThan instead') |
| static const FilterRelation GreatherThan = GreaterThan; |
| // ignore: constant_identifier_names |
| static const FilterRelation GreaterThanOrEqual = FilterRelation._('>='); |
| |
| /// Old misspelled name for [GreaterThanOrEqual], retained for compatibility. |
| /// |
| /// @nodoc |
| @Deprecated('Use FilterRelation.GreaterThanOrEqual instead') |
| static const FilterRelation GreatherThanOrEqual = GreaterThanOrEqual; |
| // ignore: constant_identifier_names |
| static const FilterRelation Equal = FilterRelation._('=='); |
| |
| final String name; |
| |
| const FilterRelation._(this.name); |
| |
| @override |
| String toString() => name; |
| } |
| |
| /// A filter used in queries. |
| class Filter { |
| /// The relation used for comparing `name` with `value`. |
| final FilterRelation relation; |
| |
| /// The name of the datastore property used in the comparison. |
| final String name; |
| |
| /// The value used for comparing against the property named by `name`. |
| final Object value; |
| |
| Filter(this.relation, this.name, this.value); |
| } |
| |
| /// The direction of a order. |
| /// |
| // TODO(Issue #6): Make this class Private and add the two statics to the |
| /// 'Order' class. |
| /// [i.e. so one can write Order.Ascending, Order.Descending]. |
| class OrderDirection { |
| // ignore: constant_identifier_names |
| static const OrderDirection Ascending = OrderDirection._('Ascending'); |
| // ignore: constant_identifier_names |
| static const OrderDirection Descending = OrderDirection._('Descending'); |
| |
| /// Old misspelled name for [Descending], retained for compatibility. |
| /// |
| /// @nodoc |
| @Deprecated('Use OrderDirection.Descending instead') |
| static const OrderDirection Decending = Descending; |
| |
| final String name; |
| |
| const OrderDirection._(this.name); |
| } |
| |
| /// A order used in queries. |
| class Order { |
| /// The direction of the order. |
| final OrderDirection direction; |
| |
| /// The name of the property used for the order. |
| final String propertyName; |
| |
| // TODO(Issue #6): Make [direction] the second argument and make it optional. |
| Order(this.direction, this.propertyName); |
| } |
| |
| /// A datastore query. |
| /// |
| /// A query consists of filters (kind, ancestor and property filters), one or |
| /// more orders and a offset/limit pair. |
| /// |
| /// All fields may be optional. |
| /// |
| /// Example of building a [Query]: |
| /// var person = ....; |
| /// var query = new Query(ancestorKey: personKey, kind: 'Address') |
| class Query { |
| /// Restrict the result set to entities of this kind. |
| final String? kind; |
| |
| /// Restrict the result set to entities which have this ancestorKey / parent. |
| final Key? ancestorKey; |
| |
| /// Restrict the result set by a list of property [Filter]s. |
| final List<Filter>? filters; |
| |
| /// Order the matching entities following the given property [Order]s. |
| final List<Order>? orders; |
| |
| /// Skip the first [offset] entities in the result set. |
| final int? offset; |
| |
| /// Limit the number of entities returned to [limit]. |
| final int? limit; |
| |
| Query({ |
| this.ancestorKey, |
| this.kind, |
| this.filters, |
| this.orders, |
| this.offset, |
| this.limit, |
| }); |
| } |
| |
| /// The result of a commit. |
| class CommitResult { |
| /// If the commit included `autoIdInserts`, this list will be the fully |
| /// populated Keys, including the automatically allocated integer IDs. |
| final List<Key> autoIdInsertKeys; |
| |
| CommitResult(this.autoIdInsertKeys); |
| } |
| |
| /// A blob value which can be used as a property value in `Entity`s. |
| class BlobValue { |
| /// The binary data of this blob. |
| final List<int> bytes; |
| |
| BlobValue(this.bytes); |
| } |
| |
| /// An opaque token returned by the `beginTransaction` method of a [Datastore]. |
| /// |
| /// This token can be passed to the `commit` and `lookup` calls if they should |
| /// operate within this transaction. |
| abstract class Transaction {} |
| |
| /// Interface used to talk to the Google Cloud Datastore service. |
| /// |
| /// It can be used to insert/update/delete [Entity]s, lookup/query [Entity]s |
| /// and allocate IDs from the auto ID allocation policy. |
| abstract class Datastore { |
| /// List of required OAuth2 scopes for Datastore operation. |
| // ignore: constant_identifier_names |
| static const Scopes = DatastoreImpl.scopes; |
| |
| /// Access Datastore using an authenticated client. |
| /// |
| /// The [client] is an authenticated HTTP client. This client must |
| /// provide access to at least the scopes in `Datastore.Scopes`. |
| /// |
| /// The [project] is the name of the Google Cloud project. |
| /// |
| /// Returns an object providing access to Datastore. The passed-in [client] |
| /// will not be closed automatically. The caller is responsible for closing |
| /// it. |
| factory Datastore(http.Client client, String project) { |
| return DatastoreImpl(client, project); |
| } |
| |
| /// Retry Datastore operations where the issue seems to be transient. |
| /// |
| /// The [delegate] is the configured [Datastore] implementation that will be |
| /// used. |
| /// |
| /// The operations will be retried at maximum of [maxAttempts]. |
| factory Datastore.withRetry( |
| Datastore delegate, { |
| int? maxAttempts, |
| }) { |
| return RetryDatastoreImpl( |
| delegate, |
| RetryOptions(maxAttempts: maxAttempts ?? 3), |
| ); |
| } |
| |
| /// Allocate integer IDs for the partially populated [keys] given as argument. |
| /// |
| /// The returned [Key]s will be fully populated with the allocated IDs. |
| Future<List<Key>> allocateIds(List<Key> keys); |
| |
| /// Starts a new transaction and returns an opaque value representing it. |
| /// |
| /// If [crossEntityGroup] is `true`, the transaction can work on up to 5 |
| /// entity groups. Otherwise the transaction will be limited to only operate |
| /// on a single entity group. |
| Future<Transaction> beginTransaction({bool crossEntityGroup = false}); |
| |
| /// Make modifications to the datastore. |
| /// |
| /// - `inserts` are [Entity]s which have a fully populated [Key] and should |
| /// be either added to the datastore or updated. |
| /// |
| /// - `autoIdInserts` are [Entity]s which do not have a fully populated [Key] |
| /// and should be added to the dataset, automatically assigning integer |
| /// IDs. |
| /// The returned [CommitResult] will contain the fully populated keys. |
| /// |
| /// - `deletes` are a list of fully populated [Key]s which uniquely identify |
| /// the [Entity]s which should be deleted. |
| /// |
| /// If a [transaction] is given, all modifications will be done within that |
| /// transaction. |
| /// |
| /// This method might complete with a [TransactionAbortedError] error. |
| /// Users must take care of retrying transactions. |
| // TODO(Issue #6): Consider splitting `inserts` into insert/update/upsert. |
| Future<CommitResult> commit( |
| {List<Entity> inserts, |
| List<Entity> autoIdInserts, |
| List<Key> deletes, |
| Transaction transaction}); |
| |
| /// Roll a started transaction back. |
| Future rollback(Transaction transaction); |
| |
| /// Looks up the fully populated [keys] in the datastore and returns either |
| /// the [Entity] corresponding to the [Key] or `null`. The order in the |
| /// returned [Entity]s is the same as in [keys]. |
| /// |
| /// If a [transaction] is given, the lookup will be within this transaction. |
| Future<List<Entity?>> lookup(List<Key> keys, {Transaction transaction}); |
| |
| /// Runs a query on the dataset and returns a [Page] of matching [Entity]s. |
| /// |
| /// The [Page] instance returned might not contain all matching [Entity]s - |
| /// in which case `isLast` is set to `false`. The page's `next` method can |
| /// be used to page through the whole result set. |
| /// The maximum number of [Entity]s returned within a single page is |
| /// implementation specific. |
| /// |
| /// - `query` is used to restrict the number of returned [Entity]s and may |
| /// may specify an order. |
| /// |
| /// - `partition` can be used to specify the namespace used for the lookup. |
| /// |
| /// If a [transaction] is given, the query will be within this transaction. |
| /// But note that arbitrary queries within a transaction are not possible. |
| /// A transaction is limited to a very small number of entity groups. Usually |
| /// queries with transactions are restricted by providing an ancestor filter. |
| /// |
| /// Outside of transactions, the result set might be stale. Queries are by |
| /// default eventually consistent. |
| Future<Page<Entity>> query(Query query, |
| {Partition partition, Transaction transaction}); |
| } |