OLD | NEW |
(Empty) | |
| 1 // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a |
| 3 // BSD-style license that can be found in the LICENSE file. |
| 4 |
| 5 /// This library provides a low-level API for accessing Google's Cloud |
| 6 /// Datastore. |
| 7 /// |
| 8 /// For more information on Cloud Datastore, please refer to the following |
| 9 /// developers page: https://cloud.google.com/datastore/docs |
| 10 library gcloud.datastore; |
| 11 |
| 12 import 'dart:async'; |
| 13 |
| 14 import 'common.dart' show Page; |
| 15 import 'service_scope.dart' as ss; |
| 16 |
| 17 const Symbol _datastoreKey = #_gcloud.datastore; |
| 18 |
| 19 /// Access the [Datastore] object available in the current service scope. |
| 20 /// |
| 21 /// The returned object will be the one which was previously registered with |
| 22 /// [registerDatastoreService] within the current (or a parent) service scope. |
| 23 /// |
| 24 /// Accessing this getter outside of a service scope will result in an error. |
| 25 /// See the `package:gcloud/service_scope.dart` library for more information. |
| 26 Datastore get datastoreService => ss.lookup(_datastoreKey); |
| 27 |
| 28 /// Registers the [Datastore] object within the current service scope. |
| 29 /// |
| 30 /// The provided `datastore` object will be avilable via the top-level |
| 31 /// `datastore` getter. |
| 32 /// |
| 33 /// Calling this function outside of a service scope will result in an error. |
| 34 /// Calling this function more than once inside the same service scope is not |
| 35 /// allowed. |
| 36 void registerDatastoreService(Datastore datastore) { |
| 37 ss.register(_datastoreKey, datastore); |
| 38 } |
| 39 |
| 40 class ApplicationError implements Exception { |
| 41 final String message; |
| 42 ApplicationError(this.message); |
| 43 |
| 44 String toString() => "ApplicationError: $message"; |
| 45 } |
| 46 |
| 47 |
| 48 class DatastoreError implements Exception { |
| 49 final String message; |
| 50 |
| 51 DatastoreError([String message]) : message = |
| 52 (message != null ?message : 'DatastoreError: An unknown error occured'); |
| 53 |
| 54 String toString() => '$message'; |
| 55 } |
| 56 |
| 57 class UnknownDatastoreError extends DatastoreError { |
| 58 UnknownDatastoreError(error) : super("An unknown error occured ($error)."); |
| 59 } |
| 60 |
| 61 class TransactionAbortedError extends DatastoreError { |
| 62 TransactionAbortedError() : super("The transaction was aborted."); |
| 63 } |
| 64 |
| 65 class TimeoutError extends DatastoreError { |
| 66 TimeoutError() : super("The operation timed out."); |
| 67 } |
| 68 |
| 69 /// Thrown when a query would require an index which was not set. |
| 70 /// |
| 71 /// An application needs to specify indices in a `index.yaml` file and needs to |
| 72 /// create indices using the `gcloud preview datastore create-indexes` command. |
| 73 class NeedIndexError extends DatastoreError { |
| 74 NeedIndexError() |
| 75 : super("An index is needed for the query to succeed."); |
| 76 } |
| 77 |
| 78 class PermissionDeniedError extends DatastoreError { |
| 79 PermissionDeniedError() : super("Permission denied."); |
| 80 } |
| 81 |
| 82 class InternalError extends DatastoreError { |
| 83 InternalError() : super("Internal service error."); |
| 84 } |
| 85 |
| 86 class QuotaExceededError extends DatastoreError { |
| 87 QuotaExceededError(error) : super("Quota was exceeded ($error)."); |
| 88 } |
| 89 |
| 90 /// A datastore Entity |
| 91 /// |
| 92 /// An entity is identified by a unique `key` and consists of a number of |
| 93 /// `properties`. If a property should not be indexed, it needs to be included |
| 94 /// in the `unIndexedProperties` set. |
| 95 /// |
| 96 /// The `properties` field maps names to values. Values can be of a primitive |
| 97 /// type or of a composed type. |
| 98 /// |
| 99 /// The following primitive types are supported: |
| 100 /// bool, int, double, String, DateTime, BlobValue, Key |
| 101 /// |
| 102 /// It is possible to have a `List` of values. The values must be primitive. |
| 103 /// Lists inside lists are not supported. |
| 104 /// |
| 105 /// Whether a property is indexed or not applies to all values (this is only |
| 106 /// relevant if the value is a list of primitive values). |
| 107 class Entity { |
| 108 final Key key; |
| 109 final Map<String, Object> properties; |
| 110 final Set<String> unIndexedProperties; |
| 111 |
| 112 Entity(this.key, this.properties, {this.unIndexedProperties}); |
| 113 } |
| 114 |
| 115 /// A complete or partial key. |
| 116 /// |
| 117 /// A key can uniquely identifiy a datastore `Entity`s. It consists of a |
| 118 /// partition and path. The path consists of one or more `KeyElement`s. |
| 119 /// |
| 120 /// A key may be incomplete. This is usesfull when inserting `Entity`s which IDs |
| 121 /// should be automatically allocated. |
| 122 /// |
| 123 /// Example of a fully populated [Key]: |
| 124 /// |
| 125 /// var fullKey = new Key([new KeyElement('Person', 1), |
| 126 /// new KeyElement('Address', 2)]); |
| 127 /// |
| 128 /// Example of a partially populated [Key] / an imcomplete [Key]: |
| 129 /// |
| 130 /// var partialKey = new Key([new KeyElement('Person', 1), |
| 131 /// new KeyElement('Address', null)]); |
| 132 class Key { |
| 133 /// The partition of this `Key`. |
| 134 final Partition partition; |
| 135 |
| 136 /// The path of `KeyElement`s. |
| 137 final List<KeyElement> elements; |
| 138 |
| 139 Key(this.elements, {Partition partition}) |
| 140 : this.partition = (partition == null) ? Partition.DEFAULT : partition; |
| 141 |
| 142 factory Key.fromParent(String kind, int id, {Key parent}) { |
| 143 var partition; |
| 144 var elements = []; |
| 145 if (parent != null) { |
| 146 partition = parent.partition; |
| 147 elements.addAll(parent.elements); |
| 148 } |
| 149 elements.add(new KeyElement(kind, id)); |
| 150 return new Key(elements, partition: partition); |
| 151 } |
| 152 |
| 153 int get hashCode => |
| 154 elements.fold(partition.hashCode, (a, b) => a ^ b.hashCode); |
| 155 |
| 156 bool operator==(Object other) { |
| 157 if (identical(this, other)) return true; |
| 158 |
| 159 if (other is Key && |
| 160 partition == other.partition && |
| 161 elements.length == other.elements.length) { |
| 162 for (int i = 0; i < elements.length; i++) { |
| 163 if (elements[i] != other.elements[i]) return false; |
| 164 } |
| 165 return true; |
| 166 } |
| 167 return false; |
| 168 } |
| 169 |
| 170 String toString() { |
| 171 var namespaceString = |
| 172 partition.namespace == null ? 'null' : "'${partition.namespace}'"; |
| 173 return "Key(namespace=$namespaceString, path=[${elements.join(', ')}])"; |
| 174 } |
| 175 } |
| 176 |
| 177 /// A datastore partition. |
| 178 /// |
| 179 /// A partition is used for partitioning a dataset into multiple namespaces. |
| 180 /// The default namespace is `null`. Using empty Strings as namespaces is |
| 181 /// invalid. |
| 182 /// |
| 183 // TODO(Issue #6): Add dataset-id here. |
| 184 class Partition { |
| 185 static const Partition DEFAULT = const Partition._default(); |
| 186 |
| 187 /// The namespace of this partition. |
| 188 final String namespace; |
| 189 |
| 190 Partition(this.namespace) { |
| 191 if (namespace == '') { |
| 192 throw new ArgumentError("'namespace' must not be empty"); |
| 193 } |
| 194 } |
| 195 |
| 196 const Partition._default() : this.namespace = null; |
| 197 |
| 198 int get hashCode => namespace.hashCode; |
| 199 |
| 200 bool operator==(Object other) => |
| 201 other is Partition && namespace == other.namespace; |
| 202 } |
| 203 |
| 204 /// An element in a `Key`s path. |
| 205 class KeyElement { |
| 206 /// The kind of this element. |
| 207 final String kind; |
| 208 |
| 209 /// The ID of this element. It must be either an `int` or a `String. |
| 210 /// |
| 211 /// This may be `null`, in which case it does not identify an Entity. It is |
| 212 /// possible to insert [Entity]s with incomplete keys and let Datastore |
| 213 /// automatically select a unused integer ID. |
| 214 final id; |
| 215 |
| 216 KeyElement(this.kind, this.id) { |
| 217 if (kind == null) { |
| 218 throw new ArgumentError("'kind' must not be null"); |
| 219 } |
| 220 if (id != null) { |
| 221 if (id is! int && id is! String) { |
| 222 throw new ArgumentError("'id' must be either null, a String or an int"); |
| 223 } |
| 224 } |
| 225 } |
| 226 |
| 227 int get hashCode => kind.hashCode ^ id.hashCode; |
| 228 |
| 229 bool operator==(Object other) => |
| 230 other is KeyElement && kind == other.kind && id == other.id; |
| 231 |
| 232 String toString() => "$kind.$id"; |
| 233 } |
| 234 |
| 235 /// A relation used in query filters. |
| 236 class FilterRelation { |
| 237 static const FilterRelation LessThan = const FilterRelation._('<'); |
| 238 static const FilterRelation LessThanOrEqual = const FilterRelation._('<='); |
| 239 static const FilterRelation GreatherThan = const FilterRelation._('>'); |
| 240 static const FilterRelation GreatherThanOrEqual = |
| 241 const FilterRelation._('>='); |
| 242 static const FilterRelation Equal = const FilterRelation._('=='); |
| 243 static const FilterRelation In = const FilterRelation._('IN'); |
| 244 |
| 245 final String name; |
| 246 |
| 247 const FilterRelation._(this.name); |
| 248 |
| 249 String toString() => name; |
| 250 } |
| 251 |
| 252 /// A filter used in queries. |
| 253 class Filter { |
| 254 /// The relation used for comparing `name` with `value`. |
| 255 final FilterRelation relation; |
| 256 |
| 257 /// The name of the datastore property used in the comparision. |
| 258 final String name; |
| 259 |
| 260 /// The value used for comparing against the property named by `name`. |
| 261 final Object value; |
| 262 |
| 263 Filter(this.relation, this.name, this.value); |
| 264 } |
| 265 |
| 266 /// The direction of a order. |
| 267 /// |
| 268 // TODO(Issue #6): Make this class Private and add the two statics to the |
| 269 /// 'Order' class. |
| 270 /// [i.e. so one can write Order.Ascending, Order.Descending]. |
| 271 class OrderDirection { |
| 272 static const OrderDirection Ascending = const OrderDirection._('Ascending'); |
| 273 static const OrderDirection Decending = const OrderDirection._('Decending'); |
| 274 |
| 275 final String name; |
| 276 |
| 277 const OrderDirection._(this.name); |
| 278 } |
| 279 |
| 280 /// A order used in queries. |
| 281 class Order { |
| 282 /// The direction of the order. |
| 283 final OrderDirection direction; |
| 284 |
| 285 /// The name of the property used for the order. |
| 286 final String propertyName; |
| 287 |
| 288 // TODO(Issue #6): Make [direction] the second argument and make it optional. |
| 289 Order(this.direction, this.propertyName); |
| 290 } |
| 291 |
| 292 /// A datastore query. |
| 293 /// |
| 294 /// A query consists of filters (kind, ancestor and property filters), one or |
| 295 /// more orders and a offset/limit pair. |
| 296 /// |
| 297 /// All fields may be optional. |
| 298 /// |
| 299 /// Example of building a [Query]: |
| 300 /// var person = ....; |
| 301 /// var query = new Query(ancestorKey: personKey, kind: 'Address') |
| 302 class Query { |
| 303 /// Restrict the result set to entities of this kind. |
| 304 final String kind; |
| 305 |
| 306 /// Restrict the result set to entities which have this ancestorKey / parent. |
| 307 final Key ancestorKey; |
| 308 |
| 309 /// Restrict the result set by a list of property [Filter]s. |
| 310 final List<Filter> filters; |
| 311 |
| 312 /// Order the matching entities following the given property [Order]s. |
| 313 final List<Order> orders; |
| 314 |
| 315 /// Skip the first [offset] entities in the result set. |
| 316 final int offset; |
| 317 |
| 318 /// Limit the number of entities returned to [limit]. |
| 319 final int limit; |
| 320 |
| 321 Query({this.ancestorKey, this.kind, this.filters, this.orders, |
| 322 this.offset, this.limit}); |
| 323 } |
| 324 |
| 325 /// The result of a commit. |
| 326 class CommitResult { |
| 327 /// If the commit included `autoIdInserts`, this list will be the fully |
| 328 /// populated Keys, including the automatically allocated integer IDs. |
| 329 final List<Key> autoIdInsertKeys; |
| 330 |
| 331 CommitResult(this.autoIdInsertKeys); |
| 332 } |
| 333 |
| 334 /// A blob value which can be used as a property value in `Entity`s. |
| 335 class BlobValue { |
| 336 /// The binary data of this blob. |
| 337 final List<int> bytes; |
| 338 |
| 339 BlobValue(this.bytes); |
| 340 } |
| 341 |
| 342 /// An opaque token returned by the `beginTransaction` method of a [Datastore]. |
| 343 /// |
| 344 /// This token can be passed to the `commit` and `lookup` calls if they should |
| 345 /// operate within this transaction. |
| 346 abstract class Transaction { } |
| 347 |
| 348 /// Interface used to talk to the Google Cloud Datastore service. |
| 349 /// |
| 350 /// It can be used to insert/update/delete [Entity]s, lookup/query [Entity]s |
| 351 /// and allocate IDs from the auto ID allocation policy. |
| 352 abstract class Datastore { |
| 353 /// Allocate integer IDs for the partially populated [keys] given as argument. |
| 354 /// |
| 355 /// The returned [Key]s will be fully populated with the allocated IDs. |
| 356 Future<List<Key>> allocateIds(List<Key> keys); |
| 357 |
| 358 /// Starts a new transaction and returns an opaque value representing it. |
| 359 /// |
| 360 /// If [crossEntityGroup] is `true`, the transaction can work on up to 5 |
| 361 /// entity groups. Otherwise the transaction will be limited to only operate |
| 362 /// on a single entity group. |
| 363 Future<Transaction> beginTransaction({bool crossEntityGroup: false}); |
| 364 |
| 365 /// Make modifications to the datastore. |
| 366 /// |
| 367 /// - `inserts` are [Entity]s which have a fully populated [Key] and should |
| 368 /// be either added to the datastore or updated. |
| 369 /// |
| 370 /// - `autoIdInserts` are [Entity]s which do not have a fully populated [Key] |
| 371 /// and should be added to the dataset, automatically assiging integer IDs. |
| 372 /// The returned [CommitResult] will contain the fuly populated keys. |
| 373 /// |
| 374 /// - `deletes` are a list of fully populated [Key]s which uniquely identify |
| 375 /// the [Entity]s which should be deleted. |
| 376 /// |
| 377 /// If a [transaction] is given, all modifications will be done within that |
| 378 /// transaction. |
| 379 /// |
| 380 /// This method might complete with a [TransactionAbortedError] error. |
| 381 /// Users must take care of retrying transactions. |
| 382 // TODO(Issue #6): Consider splitting `inserts` into insert/update/upsert. |
| 383 Future<CommitResult> commit({List<Entity> inserts, |
| 384 List<Entity> autoIdInserts, |
| 385 List<Key> deletes, |
| 386 Transaction transaction}); |
| 387 |
| 388 /// Roll a started transaction back. |
| 389 Future rollback(Transaction transaction); |
| 390 |
| 391 /// Looks up the fully populated [keys] in the datastore and returns either |
| 392 /// the [Entity] corresponding to the [Key] or `null`. The order in the |
| 393 /// returned [Entity]s is the same as in [keys]. |
| 394 /// |
| 395 /// If a [transaction] is given, the lookup will be within this transaction. |
| 396 Future<List<Entity>> lookup(List<Key> keys, {Transaction transaction}); |
| 397 |
| 398 /// Runs a query on the dataset and returns a [Page] of matching [Entity]s. |
| 399 /// |
| 400 /// The [Page] instance returned might not contain all matching [Entity]s - |
| 401 /// in which case `isLast` is set to `false`. The page's `next` method can |
| 402 /// be used to page through the whole result set. |
| 403 /// The maximum number of [Entity]s returned within a single page is |
| 404 /// implementation specific. |
| 405 /// |
| 406 /// - `query` is used to restrict the number of returned [Entity]s and may |
| 407 /// may specify an order. |
| 408 /// |
| 409 /// - `partition` can be used to specify the namespace used for the lookup. |
| 410 /// |
| 411 /// If a [transaction] is given, the query will be within this transaction. |
| 412 /// But note that arbitrary queries within a transaction are not possible. |
| 413 /// A transaction is limited to a very small number of entity groups. Usually |
| 414 /// queries with transactions are restricted by providing an ancestor filter. |
| 415 /// |
| 416 /// Outside of transactions, the result set might be stale. Queries are by |
| 417 /// default eventually consistent. |
| 418 Future<Page<Entity>> query( |
| 419 Query query, {Partition partition, Transaction transaction}); |
| 420 } |
OLD | NEW |