Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(52)

Side by Side Diff: pkg/appengine/lib/src/api_impl/raw_datastore_v3_impl.dart

Issue 804973002: Add appengine/gcloud/mustache dependencies. (Closed) Base URL: git@github.com:dart-lang/pub-dartlang-dart.git@master
Patch Set: Added AUTHORS/LICENSE/PATENTS files Created 6 years ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(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 library raw_datastore_v3_impl;
6
7 import 'dart:async';
8 import 'dart:convert' show UTF8;
9
10 import 'package:fixnum/fixnum.dart';
11 import 'package:gcloud/common.dart';
12 import 'package:gcloud/datastore.dart' as raw;
13
14 import '../appengine_context.dart';
15
16 import '../../api/errors.dart' as errors;
17 import '../protobuf_api/rpc/rpc_service.dart';
18 import '../protobuf_api/internal/datastore_v3.pb.dart';
19 import '../protobuf_api/datastore_v3_service.dart';
20
21 // TODO(gcloud Issue #3): Unified exception handing.
22 buildDatastoreException(RpcApplicationError error) {
23 var errorCode = Error_ErrorCode.valueOf(error.code);
24 switch (errorCode) {
25 case Error_ErrorCode.BAD_REQUEST:
26 return new raw.ApplicationError("Bad request: ${error.message}");
27 case Error_ErrorCode.CONCURRENT_TRANSACTION:
28 return new raw.TransactionAbortedError();
29 case Error_ErrorCode.INTERNAL_ERROR:
30 return new raw.InternalError();
31 case Error_ErrorCode.NEED_INDEX:
32 return new raw.NeedIndexError();
33 case Error_ErrorCode.TIMEOUT:
34 return new raw.TimeoutError();
35 case Error_ErrorCode.PERMISSION_DENIED:
36 return new raw.PermissionDeniedError();
37 case Error_ErrorCode.BIGTABLE_ERROR:
38 case Error_ErrorCode.COMMITTED_BUT_STILL_APPLYING:
39 case Error_ErrorCode.CAPABILITY_DISABLED:
40 case Error_ErrorCode.TRY_ALTERNATE_BACKEND:
41 case Error_ErrorCode.SAFE_TIME_TOO_OLD:
42 return new Exception(error);
43 }
44 }
45
46 Future catchAndReThrowDatastoreException(Future future) {
47 return future.catchError((error, stack) {
48 throw buildDatastoreException(error);
49 }, test: (error) => error is RpcApplicationError);
50 }
51
52 class TransactionImpl extends raw.Transaction {
53 final Transaction _rpcTransaction;
54
55 TransactionImpl(this._rpcTransaction);
56 }
57
58 class Codec {
59 static final ORDER_DIRECTION_MAPPING = {
60 raw.OrderDirection.Ascending : Query_Order_Direction.ASCENDING,
61 raw.OrderDirection.Decending : Query_Order_Direction.DESCENDING,
62 };
63
64 static final FILTER_RELATION_MAPPING = {
65 raw.FilterRelation.LessThan : Query_Filter_Operator.LESS_THAN,
66 raw.FilterRelation.LessThanOrEqual :
67 Query_Filter_Operator.LESS_THAN_OR_EQUAL,
68 raw.FilterRelation.Equal : Query_Filter_Operator.EQUAL,
69 raw.FilterRelation.GreatherThan : Query_Filter_Operator.GREATER_THAN,
70 raw.FilterRelation.GreatherThanOrEqual :
71 Query_Filter_Operator.GREATER_THAN_OR_EQUAL,
72 raw.FilterRelation.In : Query_Filter_Operator.IN,
73 };
74
75 final String _application;
76
77 Codec(this._application);
78
79 raw.Entity decodeEntity(EntityProto pb) {
80 var properties = {};
81 var unIndexedProperties = new Set<String>();
82
83 decodeProperties(List<Property> pbList, {bool indexed: true}) {
84 for (var propertyPb in pbList) {
85 var name = propertyPb.name;
86 var value = decodeValue(propertyPb);
87
88 if (!indexed) {
89 unIndexedProperties.add(name);
90 }
91
92 // TODO: This is a hackisch way of detecting whether to construct a list
93 // or not. We may be able to use the [propertyPb.multiple] flag, but
94 // we could run into issues if we get the same name twice where the flag
95 // is false. (Or the flag is sometimes set and sometimes not).
96 if (!properties.containsKey(name)) {
97 properties[name] = decodeValue(propertyPb);
98 } else {
99 var oldValue = properties[name];
100 if (oldValue is List) {
101 properties[name].add(value);
102 } else {
103 properties[name] = [oldValue, value];
104 }
105 }
106 }
107 }
108 decodeProperties(pb.property, indexed: true);
109 decodeProperties(pb.rawProperty, indexed: false);
110
111 return new raw.Entity(decodeKey(pb.key),
112 properties,
113 unIndexedProperties: unIndexedProperties);
114 }
115
116 Object decodeValue(Property propertyPb) {
117 var pb = propertyPb.value;
118 if (pb.hasBytesValue()) {
119 switch (propertyPb.meaning) {
120 case Property_Meaning.BYTESTRING:
121 case Property_Meaning.BLOB:
122 return new raw.BlobValue(pb.bytesValue);
123 case Property_Meaning.TEXT:
124 default:
125 return UTF8.decode(pb.bytesValue);
126 }
127 } else if (pb.hasInt64Value()) {
128 var intValue = pb.int64Value.toInt();
129 switch (propertyPb.meaning) {
130 case Property_Meaning.GD_WHEN:
131 return new DateTime.fromMillisecondsSinceEpoch(
132 intValue ~/ 1000, isUtc: true);
133 default:
134 return intValue;
135 }
136 } else if (pb.hasBooleanValue()) {
137 return pb.booleanValue;
138 } else if (pb.hasPointValue()) {
139 throw new UnimplementedError("Point values are not supported yet.");
140 } else if (pb.hasDoubleValue()) {
141 return pb.doubleValue;
142 } else if (pb.hasUserValue()) {
143 throw new UnimplementedError("User values are not supported yet.");
144 } else if (pb.hasReferenceValue()) {
145 return decodeKeyValue(pb.referenceValue);
146 }
147 // "If no value field is set, the value is interpreted as a null"
148 return null;
149 }
150
151 PropertyValue_ReferenceValue encodeKeyValue(raw.Key key) {
152 var referencePb = new PropertyValue_ReferenceValue();
153 var partition = key.partition;
154 if (partition != null && partition.namespace != null) {
155 referencePb.nameSpace = partition.namespace;
156 }
157 referencePb.app = _application;
158 referencePb.pathElement.addAll(key.elements.map((raw.KeyElement part) {
159 var pathElementPb = new PropertyValue_ReferenceValue_PathElement();
160 pathElementPb.type = part.kind;
161 if (part.id != null) {
162 if (part.id is int) {
163 pathElementPb.id = new Int64(part.id);
164 } else {
165 pathElementPb.name = part.id;
166 }
167 }
168 return pathElementPb;
169 }));
170 return referencePb;
171 }
172
173 raw.Key decodeKeyValue(PropertyValue_ReferenceValue pb) {
174 var keyElements = [];
175 for (var part in pb.pathElement) {
176 var id;
177 if (part.hasName()) {
178 id = part.name;
179 } else if (part.hasId()) {
180 id = part.id.toInt();
181 } else {
182 throw new errors.ProtocolError(
183 'Invalid ReferenceValue: no int/string id.');
184 }
185 keyElements.add(new raw.KeyElement(part.type, id));
186 }
187 var partition = new raw.Partition(pb.hasNameSpace() ? pb.nameSpace : null);
188 return new raw.Key(keyElements, partition: partition);
189 }
190
191 raw.Key decodeKey(Reference pb) {
192 var keyElements = [];
193 for (var part in pb.path.element) {
194 var id;
195 if (part.hasName()) {
196 id = part.name;
197 } else if (part.hasId()) {
198 id = part.id.toInt();
199 } else {
200 throw new errors.ProtocolError(
201 'Invalid ReferenceValue: no int/string id.');
202 }
203 keyElements.add(new raw.KeyElement(part.type, id));
204 }
205 var partition = new raw.Partition(pb.hasNameSpace() ? pb.nameSpace : null);
206 return new raw.Key(keyElements, partition: partition);
207 }
208
209 EntityProto encodeEntity(raw.Entity entity) {
210 var pb = new EntityProto();
211 pb.key = encodeKey(entity.key, enforceId: false);
212 if (entity.key.elements.length > 1) {
213 pb.entityGroup =
214 _encodePath([entity.key.elements.first], enforceId: true);
215 } else {
216 pb.entityGroup = new Path();
217 }
218
219 var unIndexedProperties = entity.unIndexedProperties;
220 for (var property in entity.properties.keys) {
221 bool indexProperty = (unIndexedProperties == null ||
222 !unIndexedProperties.contains(property));
223 var value = entity.properties[property];
224 if (value != null && value is List) {
225 for (var entry in value) {
226 var pbProperty = encodeProperty(
227 property, entry, multiple: true, indexProperty: indexProperty);
228 if (indexProperty) {
229 pb.property.add(pbProperty);
230 } else {
231 pb.rawProperty.add(pbProperty);
232 }
233 }
234 } else {
235 var pbProperty = encodeProperty(
236 property, value, indexProperty: indexProperty);
237 if (indexProperty) {
238 pb.property.add(pbProperty);
239 } else {
240 pb.rawProperty.add(pbProperty);
241 }
242 }
243 }
244 return pb;
245 }
246
247 Reference encodeKey(raw.Key key, {bool enforceId: true}) {
248 var partition = key.partition;
249
250 var pb = new Reference();
251 if (partition != null && partition.namespace != null) {
252 pb.nameSpace = partition.namespace;
253 }
254 pb.app = _application;
255 pb.path = _encodePath(key.elements, enforceId: enforceId);
256 return pb;
257 }
258
259 Path _encodePath(List<raw.KeyElement> elements, {bool enforceId: true}) {
260 var pathPb = new Path();
261 for (var part in elements) {
262 var partPb = new Path_Element();
263 partPb.type = part.kind;
264 if (part.id != null) {
265 if (part.id is String) {
266 partPb.name = part.id;
267 } else if (part.id is int) {
268 partPb.id = new Int64(part.id);
269 } else {
270 throw new raw.ApplicationError(
271 'Only strings and integers are supported as IDs '
272 '(was: ${part.id.runtimeType}).');
273 }
274 } else {
275 if (enforceId) {
276 throw new raw.ApplicationError(
277 'Error while encoding entity key: id was null.');
278 }
279 }
280 pathPb.element.add(partPb);
281 }
282 return pathPb;
283 }
284
285 Property encodeProperty(String name, Object value,
286 {bool multiple: false, bool indexProperty: false}) {
287 var pb = new Property();
288 pb.name = name;
289 pb.multiple = multiple;
290
291 if (value == null) {
292 // "If no value field is set, the value is interpreted as a null"
293 pb.value = new PropertyValue();
294 } else if (value is String) {
295 pb.value = new PropertyValue()..bytesValue = UTF8.encode(value);
296 if (!indexProperty) {
297 pb.meaning = Property_Meaning.TEXT;
298 }
299 } else if (value is int) {
300 pb.value = new PropertyValue()..int64Value = new Int64(value);
301 } else if (value is double) {
302 pb.value = new PropertyValue()..doubleValue = value;
303 } else if (value is bool) {
304 pb.value = new PropertyValue()..booleanValue = value;
305 } else if (value is raw.Key) {
306 pb.value = new PropertyValue()..referenceValue = encodeKeyValue(value);
307 } else if (value is raw.BlobValue) {
308 pb.value = new PropertyValue()..bytesValue = value.bytes;
309 if (indexProperty) {
310 pb.meaning = Property_Meaning.BYTESTRING;
311 } else {
312 pb.meaning = Property_Meaning.BLOB;
313 }
314 } else if (value is DateTime) {
315 var usSinceEpoch = new Int64(value.toUtc().millisecondsSinceEpoch * 1000);
316 pb.value = new PropertyValue()..int64Value = usSinceEpoch;
317 pb.meaning = Property_Meaning.GD_WHEN;
318 } else {
319 throw new raw.ApplicationError(
320 'Cannot encode unsupported ${value.runtimeType} type.');
321 }
322 return pb;
323 }
324 }
325
326 class DatastoreV3RpcImpl implements raw.Datastore {
327 final DataStoreV3ServiceClientRPCStub _clientRPCStub;
328 final AppengineContext _appengineContext;
329 final Codec _codec;
330
331 DatastoreV3RpcImpl(RPCService rpcService,
332 AppengineContext appengineContext,
333 String ticket)
334 : _clientRPCStub = new DataStoreV3ServiceClientRPCStub(rpcService,
335 ticket),
336 _appengineContext = appengineContext,
337 _codec = new Codec(appengineContext.fullQualifiedApplicationId);
338
339 Future<List<raw.Key>> allocateIds(List<raw.Key> keys) {
340 // TODO: We may be able to group keys if they have the same parent+kind
341 // and add a size > 1.
342 var requests = [];
343 for (var key in keys) {
344 var request = new AllocateIdsRequest();
345 request.modelKey = _codec.encodeKey(key, enforceId: false);
346 request.size = new Int64(1);
347 requests.add(_clientRPCStub.AllocateIds(request));
348 }
349 return catchAndReThrowDatastoreException(
350 Future.wait(requests).then((List<AllocateIdsResponse> responses) {
351 var result = [];
352 for (int i = 0; i < keys.length; i++) {
353 var key = keys[i];
354 var response = responses[i];
355 var id = response.start.toInt();
356
357 if ((response.end - response.start) != 0) {
358 throw new errors.ProtocolError(
359 "Server responded with invalid allocatedId range: "
360 "start=${response.start}, end=${response.end}");
361 }
362
363 var parts = key.elements.take(key.elements.length - 1).toList();
364 parts.add(new raw.KeyElement(key.elements.last.kind, id));
365 result.add(new raw.Key(parts, partition: key.partition));
366 }
367 return result;
368 }));
369 }
370
371 Future<raw.Transaction> beginTransaction({bool crossEntityGroup: false}) {
372 var request = new BeginTransactionRequest();
373 request.allowMultipleEg = crossEntityGroup;
374 request.app = _appengineContext.fullQualifiedApplicationId;
375
376 return catchAndReThrowDatastoreException(
377 _clientRPCStub.BeginTransaction(request).then((Transaction t) {
378 return new TransactionImpl(t);
379 }));
380 }
381
382 Future<raw.CommitResult> commit({List<raw.Entity> inserts,
383 List<raw.Entity> autoIdInserts,
384 List<raw.Key> deletes,
385 TransactionImpl transaction}) {
386 Future insertFuture, deleteFuture;
387
388 Transaction rpcTransaction;
389 if (transaction != null) rpcTransaction = transaction._rpcTransaction;
390
391 // Inserts
392 bool needInserts = inserts != null && inserts.length > 0;
393 bool needAutoIdInserts = autoIdInserts != null && autoIdInserts.length > 0;
394 int totalNumberOfInserts = 0;
395 if (needInserts || needAutoIdInserts) {
396 var request = new PutRequest();
397 if (rpcTransaction != null) request.transaction = rpcTransaction;
398
399 if (needAutoIdInserts) {
400 totalNumberOfInserts += autoIdInserts.length;
401 for (var entity in autoIdInserts) {
402 request.entity.add(_codec.encodeEntity(entity));
403 }
404 }
405
406 if (needInserts) {
407 totalNumberOfInserts += inserts.length;
408 for (var entity in inserts) {
409 request.entity.add(_codec.encodeEntity(entity));
410 }
411 }
412
413 insertFuture = _clientRPCStub.Put(request).then((PutResponse response) {
414 if (response.key.length != totalNumberOfInserts) {
415 // TODO(gcloud Issue #3): Unified exception handing.
416 throw new Exception(
417 "Tried to insert $totalNumberOfInserts entities, but response "
418 "seems to indicate we inserted ${response.key.length} entities.");
419 }
420 if (needAutoIdInserts) {
421 // NOTE: Auto id inserts are at the beginning.
422 return response.key.take(autoIdInserts.length).map(_codec.decodeKey)
423 .toList();
424 } else {
425 return [];
426 }
427 });
428 } else {
429 insertFuture = new Future.value();
430 }
431
432 // Deletes
433 if (deletes != null && deletes.length > 0) {
434 var request = new DeleteRequest();
435 if (rpcTransaction != null) request.transaction = rpcTransaction;
436
437 for (var key in deletes) {
438 request.key.add(_codec.encodeKey(key));
439 }
440 deleteFuture = _clientRPCStub.Delete(request).then((_) => null);
441 } else {
442 deleteFuture = new Future.value();
443 }
444
445 return catchAndReThrowDatastoreException(
446 Future.wait([insertFuture, deleteFuture]).then((results) {
447 var result = new raw.CommitResult(results[0]);
448 if (rpcTransaction == null) return result;
449 return _clientRPCStub.Commit(rpcTransaction).then((_) => result);
450 }));
451 }
452
453 Future rollback(TransactionImpl transaction) {
454 return catchAndReThrowDatastoreException(
455 _clientRPCStub.Rollback(transaction._rpcTransaction)
456 .then((_) => null));
457 }
458
459 Future<List<raw.Entity>> lookup(
460 List<raw.Key> keys, {TransactionImpl transaction}) {
461 var request = new GetRequest();
462 // Make sure we don't get results out-of-order.
463 request.allowDeferred = false;
464 if (transaction != null) {
465 request.transaction = transaction._rpcTransaction;
466 request.strong = true;
467 }
468 for (var key in keys) {
469 request.key.add(_codec.encodeKey(key));
470 }
471 return catchAndReThrowDatastoreException(
472 _clientRPCStub.Get(request).then((GetResponse response) {
473 return response.entity.map((GetResponse_Entity pb) {
474 if (pb.hasEntity()) return _codec.decodeEntity(pb.entity);
475 return null;
476 }).toList();
477 }));
478 }
479
480 Future<Page<raw.Entity>> query(
481 raw.Query query, {raw.Partition partition, TransactionImpl transaction}) {
482 if (query.kind == null && query.ancestorKey == null) {
483 throw new raw.ApplicationError(
484 "You must specify a kind or ancestorKey in a query");
485 }
486
487 Transaction rpcTransaction = transaction != null
488 ? transaction._rpcTransaction : null;
489
490 var request = new Query();
491 if (partition != null && partition.namespace != null) {
492 request.nameSpace = partition.namespace;
493 }
494 request.app = _appengineContext.fullQualifiedApplicationId;
495 if (rpcTransaction != null) {
496 request.transaction = rpcTransaction;
497 request.strong = true;
498 }
499 if (query.kind != null) {
500 request.kind = query.kind;
501 }
502 if (query.ancestorKey != null) {
503 request.ancestor = _codec.encodeKey(query.ancestorKey);
504 }
505
506 if (query.offset != null) {
507 request.offset = query.offset;
508 }
509 if (query.limit != null) {
510 request.limit = query.limit;
511 }
512
513 if (query.filters != null) {
514 for (var filter in query.filters) {
515 var queryFilter = new Query_Filter();
516 queryFilter.op = Codec.FILTER_RELATION_MAPPING[filter.relation];
517 if (filter.relation == raw.FilterRelation.In) {
518 if (filter.value == null || filter.value is! List) {
519 throw new raw.ApplicationError('Filters with list entry checks '
520 'must have a list value for membership checking.');
521 }
522 for (var listValue in filter.value) {
523 queryFilter.property.add(_codec.encodeProperty(
524 filter.name, listValue, indexProperty: true));
525 }
526 } else {
527 queryFilter.property.add(_codec.encodeProperty(
528 filter.name, filter.value, indexProperty: true));
529 }
530 request.filter.add(queryFilter);
531 }
532 }
533 if (query.orders != null) {
534 for (var order in query.orders) {
535 var queryOrder = new Query_Order();
536 queryOrder.direction = Codec.ORDER_DIRECTION_MAPPING[order.direction];
537 queryOrder.property = order.propertyName;
538 request.order.add(queryOrder);
539 }
540 }
541
542 return catchAndReThrowDatastoreException(
543 _clientRPCStub.RunQuery(request).then((QueryResult result) {
544 return QueryPageImpl.fromQueryResult(
545 _clientRPCStub, _codec, query.offset, 0, query.limit, result);
546 }));
547 }
548 }
549
550 // NOTE: We're never calling datastore_v3.DeleteCursor() here.
551 // - we don't know when this is safe, due to the Page<> interface
552 // - the devappserver2/apiServer does not implement `DeleteCursor()`
553 class QueryPageImpl implements Page<raw.Entity> {
554 final DataStoreV3ServiceClientRPCStub _clientRPCStub;
555 final Codec _codec;
556 final Cursor _cursor;
557
558 final List<raw.Entity> _entities;
559 final bool _isLast;
560
561 // This is `Query.offset` and will be carried across page walking.
562 final int _offset;
563
564 // This is always non-`null` and contains the number of entities that were
565 // skiped so far.
566 final int _alreadySkipped;
567
568 // If not `null` this will hold the remaining number of entities we are
569 // allowed to receive according to `Query.limit`.
570 final int _remainingNumberOfEntities;
571
572 QueryPageImpl(this._clientRPCStub,
573 this._codec,
574 this._cursor,
575 this._entities,
576 this._isLast,
577 this._offset,
578 this._alreadySkipped,
579 this._remainingNumberOfEntities);
580
581 static QueryPageImpl fromQueryResult(
582 DataStoreV3ServiceClientRPCStub clientRPCStub,
583 Codec codec,
584 int offset,
585 int alreadySkipped,
586 int remainingNumberOfEntities,
587 QueryResult queryResult) {
588 // If we have an offset: Check that in total we haven't skipped too many.
589 if (offset != null &&
590 offset > 0 &&
591 queryResult.hasSkippedResults() &&
592 queryResult.skippedResults > (offset - alreadySkipped)) {
593 throw new raw.DatastoreError(
594 'Datastore was supposed to skip ${offset} entities, '
595 'but response indicated '
596 '${queryResult.skippedResults + alreadySkipped} entities were '
597 'skipped (which is more).');
598 }
599
600 // If we have a limit: Check that in total we haven't gotten too many.
601 if (remainingNumberOfEntities != null &&
602 remainingNumberOfEntities > 0 &&
603 queryResult.result.length > remainingNumberOfEntities) {
604 throw new raw.DatastoreError(
605 'Datastore returned more entitites (${queryResult.result.length}) '
606 'then the limit was ($remainingNumberOfEntities).');
607 }
608
609 // If we have a limit: Calculate the remaining limit.
610 int remainingEntities;
611 if (remainingNumberOfEntities != null && remainingNumberOfEntities > 0) {
612 remainingEntities = remainingNumberOfEntities - queryResult.result.length;
613 }
614
615 // Determine if this is the last query batch.
616 bool isLast = !(queryResult.hasMoreResults() && queryResult.moreResults);
617
618 // If we have an offset: Calculate the new number of skipped entities.
619 int skipped = alreadySkipped;
620 if (offset != null && offset > 0 && queryResult.hasSkippedResults()) {
621 skipped += queryResult.skippedResults;
622 }
623
624 var entities = queryResult.result.map(codec.decodeEntity).toList();
625 return new QueryPageImpl(
626 clientRPCStub, codec, queryResult.cursor, entities, isLast,
627 offset, skipped, remainingEntities);
628 }
629
630 bool get isLast => _isLast;
631
632 List<raw.Entity> get items => _entities;
633
634 Future<Page<raw.Entity>> next({int pageSize}) {
635 if (isLast) {
636 return new Future.sync(() {
637 throw new ArgumentError('Cannot call next() on last page.');
638 });
639 }
640
641 var nextRequest = new NextRequest();
642 nextRequest.cursor = _cursor;
643
644 if (pageSize != null && pageSize > 0) {
645 nextRequest.count = pageSize;
646 }
647
648 if (_offset != null && (_offset - _alreadySkipped) > 0) {
649 nextRequest.offset = _offset - _alreadySkipped;
650 } else {
651 nextRequest.offset = 0;
652 }
653
654 return catchAndReThrowDatastoreException(
655 _clientRPCStub.Next(nextRequest).then((QueryResult result) {
656 return QueryPageImpl.fromQueryResult(
657 _clientRPCStub, _codec, _offset, _alreadySkipped,
658 _remainingNumberOfEntities, result);
659 }));
660 }
661 }
OLDNEW
« no previous file with comments | « pkg/appengine/lib/src/api_impl/modules_impl.dart ('k') | pkg/appengine/lib/src/api_impl/raw_memcache_impl.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698