blob: cf7eaac9201bbf71b92be0ae5b4f1be2bb203a98 [file] [log] [blame] [edit]
// 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});
}