| Index: pkg/gcloud/lib/datastore.dart
|
| diff --git a/pkg/gcloud/lib/datastore.dart b/pkg/gcloud/lib/datastore.dart
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..3f0dcf9c4e9ff1d2987e1ae32811df45294827bb
|
| --- /dev/null
|
| +++ b/pkg/gcloud/lib/datastore.dart
|
| @@ -0,0 +1,420 @@
|
| +// 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 gcloud.datastore;
|
| +
|
| +import 'dart:async';
|
| +
|
| +import 'common.dart' show Page;
|
| +import 'service_scope.dart' as ss;
|
| +
|
| +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);
|
| +
|
| +/// Registers the [Datastore] object within the current service scope.
|
| +///
|
| +/// The provided `datastore` object will be avilable 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);
|
| +
|
| + String toString() => "ApplicationError: $message";
|
| +}
|
| +
|
| +
|
| +class DatastoreError implements Exception {
|
| + final String message;
|
| +
|
| + DatastoreError([String message]) : message =
|
| + (message != null ?message : 'DatastoreError: An unknown error occured');
|
| +
|
| + String toString() => '$message';
|
| +}
|
| +
|
| +class UnknownDatastoreError extends DatastoreError {
|
| + UnknownDatastoreError(error) : super("An unknown error occured ($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});
|
| +}
|
| +
|
| +/// A complete or partial key.
|
| +///
|
| +/// A key can uniquely identifiy 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 usesfull 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 imcomplete [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, {Partition partition})
|
| + : this.partition = (partition == null) ? Partition.DEFAULT : partition;
|
| +
|
| + factory Key.fromParent(String kind, int id, {Key parent}) {
|
| + var partition;
|
| + var elements = [];
|
| + if (parent != null) {
|
| + partition = parent.partition;
|
| + elements.addAll(parent.elements);
|
| + }
|
| + elements.add(new KeyElement(kind, id));
|
| + return new Key(elements, partition: partition);
|
| + }
|
| +
|
| + int get hashCode =>
|
| + elements.fold(partition.hashCode, (a, b) => a ^ b.hashCode);
|
| +
|
| + bool operator==(Object other) {
|
| + if (identical(this, other)) return true;
|
| +
|
| + if (other is Key &&
|
| + partition == other.partition &&
|
| + elements.length == other.elements.length) {
|
| + for (int i = 0; i < elements.length; i++) {
|
| + if (elements[i] != other.elements[i]) return false;
|
| + }
|
| + return true;
|
| + }
|
| + return false;
|
| + }
|
| +
|
| + 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 {
|
| + static const Partition DEFAULT = const Partition._default();
|
| +
|
| + /// The namespace of this partition.
|
| + final String namespace;
|
| +
|
| + Partition(this.namespace) {
|
| + if (namespace == '') {
|
| + throw new ArgumentError("'namespace' must not be empty");
|
| + }
|
| + }
|
| +
|
| + const Partition._default() : this.namespace = null;
|
| +
|
| + int get hashCode => namespace.hashCode;
|
| +
|
| + 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 id;
|
| +
|
| + KeyElement(this.kind, this.id) {
|
| + if (kind == null) {
|
| + throw new ArgumentError("'kind' must not be null");
|
| + }
|
| + if (id != null) {
|
| + if (id is! int && id is! String) {
|
| + throw new ArgumentError("'id' must be either null, a String or an int");
|
| + }
|
| + }
|
| + }
|
| +
|
| + int get hashCode => kind.hashCode ^ id.hashCode;
|
| +
|
| + bool operator==(Object other) =>
|
| + other is KeyElement && kind == other.kind && id == other.id;
|
| +
|
| + String toString() => "$kind.$id";
|
| +}
|
| +
|
| +/// A relation used in query filters.
|
| +class FilterRelation {
|
| + static const FilterRelation LessThan = const FilterRelation._('<');
|
| + static const FilterRelation LessThanOrEqual = const FilterRelation._('<=');
|
| + static const FilterRelation GreatherThan = const FilterRelation._('>');
|
| + static const FilterRelation GreatherThanOrEqual =
|
| + const FilterRelation._('>=');
|
| + static const FilterRelation Equal = const FilterRelation._('==');
|
| + static const FilterRelation In = const FilterRelation._('IN');
|
| +
|
| + final String name;
|
| +
|
| + const FilterRelation._(this.name);
|
| +
|
| + 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 comparision.
|
| + 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 {
|
| + static const OrderDirection Ascending = const OrderDirection._('Ascending');
|
| + static const OrderDirection Decending = const OrderDirection._('Decending');
|
| +
|
| + 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 {
|
| + /// 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 assiging integer IDs.
|
| + /// The returned [CommitResult] will contain the fuly 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});
|
| +}
|
|
|